diff --git a/sdk/node/Libplanet.Node.Executable/BlockChainRendererTracer.cs b/sdk/node/Libplanet.Node.Executable/BlockChainRendererTracer.cs new file mode 100644 index 00000000000..7fc3566939b --- /dev/null +++ b/sdk/node/Libplanet.Node.Executable/BlockChainRendererTracer.cs @@ -0,0 +1,28 @@ +using Libplanet.Node.Services; + +namespace Libplanet.Node.API; + +internal sealed class BlockChainRendererTracer( + IRendererService rendererService, ILogger logger) + : IHostedService +{ + private readonly ILogger _logger = logger; + private IDisposable? _observer; + + public Task StartAsync(CancellationToken cancellationToken) + { + rendererService.RenderBlockEnd.Subscribe( + info => _logger.LogInformation( + "-Pattern2- #{Height} Block end: {Hash}", + info.NewTip.Index, + info.NewTip.Hash)); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _observer?.Dispose(); + _observer = null; + return Task.CompletedTask; + } +} diff --git a/sdk/node/Libplanet.Node.Executable/Explorer/ExplorerOptions.cs b/sdk/node/Libplanet.Node.Executable/Explorer/ExplorerOptions.cs index f09b95a6832..7a06d6b8715 100644 --- a/sdk/node/Libplanet.Node.Executable/Explorer/ExplorerOptions.cs +++ b/sdk/node/Libplanet.Node.Executable/Explorer/ExplorerOptions.cs @@ -4,7 +4,7 @@ namespace Libplanet.Node.API.Explorer; [Options(Position)] -public sealed class ExplorerOptions : OptionsBase, IEnabledOptions +public sealed class ExplorerOptions : OptionsBase { public const string Position = "Explorer"; diff --git a/sdk/node/Libplanet.Node.Executable/Program.cs b/sdk/node/Libplanet.Node.Executable/Program.cs index 48aac2560f7..f825da970f3 100644 --- a/sdk/node/Libplanet.Node.Executable/Program.cs +++ b/sdk/node/Libplanet.Node.Executable/Program.cs @@ -1,3 +1,4 @@ +using Libplanet.Node.API; using Libplanet.Node.API.Explorer; using Libplanet.Node.API.Services; using Libplanet.Node.Extensions; @@ -27,6 +28,7 @@ builder.Services.AddGrpc(); builder.Services.AddGrpcReflection(); builder.Services.AddLibplanetNode(builder.Configuration); +builder.Services.AddHostedService(); if (builder.IsExplorerEnabled()) { diff --git a/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs b/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs index 27aa98428e1..e9634e78421 100644 --- a/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs +++ b/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs @@ -48,6 +48,8 @@ public static ILibplanetNodeBuilder AddLibplanetNode( services.AddSingleton(s => (IStoreService)s.GetRequiredService()); services.AddSingleton(); services.AddSingleton(s => (IActionService)s.GetRequiredService()); + services.AddSingleton(); + services.AddSingleton(s => (IRendererService)s.GetRequiredService()); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/sdk/node/Libplanet.Node.Tests/BlockChainUtility.cs b/sdk/node/Libplanet.Node.Tests/BlockChainUtility.cs index 2ca17d2a828..a399be00c24 100644 --- a/sdk/node/Libplanet.Node.Tests/BlockChainUtility.cs +++ b/sdk/node/Libplanet.Node.Tests/BlockChainUtility.cs @@ -1,6 +1,8 @@ +using Libplanet.Action; using Libplanet.Blockchain; using Libplanet.Crypto; using Libplanet.Types.Blocks; +using Libplanet.Types.Tx; namespace Libplanet.Node.Tests; @@ -14,8 +16,8 @@ public static async Task AppendBlockAsync(BlockChain blockChain, PrivateK var tip = blockChain.Tip; var height = tip.Index + 1; var block = blockChain.ProposeBlock( - privateKey, - blockChain.GetBlockCommit(tip.Hash)); + proposer: privateKey, + lastCommit: blockChain.GetBlockCommit(tip.Hash)); blockChain.Append( block, blockChain.GetBlockCommit(tip.Hash), @@ -30,4 +32,32 @@ public static async Task AppendBlockAsync(BlockChain blockChain, PrivateK return block; } + + public static void StageTransaction( + BlockChain blockChain, IAction[] actions) + => StageTransaction(blockChain, new PrivateKey(), actions); + + public static void StageTransaction( + BlockChain blockChain, PrivateKey privateKey, IAction[] actions) + { + var transaction = CreateTransaction(blockChain, privateKey, actions); + blockChain.StageTransaction(transaction); + } + + public static Transaction CreateTransaction( + BlockChain blockChain, IAction[] actions) + => CreateTransaction(blockChain, new PrivateKey(), actions); + + public static Transaction CreateTransaction( + BlockChain blockChain, PrivateKey privateKey, IAction[] actions) + { + var genesisBlock = blockChain.Genesis; + var nonce = blockChain.GetNextTxNonce(privateKey.Address); + var values = actions.Select(item => item.PlainValue).ToArray(); + return Transaction.Create( + nonce: nonce, + privateKey: privateKey, + genesisHash: genesisBlock.Hash, + actions: new TxActionList(values)); + } } diff --git a/sdk/node/Libplanet.Node.Tests/DumbAction.cs b/sdk/node/Libplanet.Node.Tests/DumbAction.cs new file mode 100644 index 00000000000..544314501ec --- /dev/null +++ b/sdk/node/Libplanet.Node.Tests/DumbAction.cs @@ -0,0 +1,36 @@ +using System.Diagnostics; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; + +namespace Libplanet.Node.Tests; + +public class DumbAction : IAction +{ + public string ErrorMessage { get; set; } = string.Empty; + + public IValue PlainValue => Dictionary.Empty + .Add("error_message", ErrorMessage); + + public void LoadPlainValue(IValue plainValue) + { + if (plainValue is Dictionary dictionary) + { + ErrorMessage = (Text)dictionary["error_message"]; + } + else + { + throw new UnreachableException("The plain value of DumbAction must be a dictionary."); + } + } + + public IWorld Execute(IActionContext context) + { + if (ErrorMessage != string.Empty) + { + throw new InvalidOperationException(ErrorMessage); + } + + return context.PreviousState; + } +} diff --git a/sdk/node/Libplanet.Node.Tests/DumbActionLoader.cs b/sdk/node/Libplanet.Node.Tests/DumbActionLoader.cs new file mode 100644 index 00000000000..1a8894d27d2 --- /dev/null +++ b/sdk/node/Libplanet.Node.Tests/DumbActionLoader.cs @@ -0,0 +1,21 @@ +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.Loader; +using Libplanet.Action.Sys; + +namespace Libplanet.Node.Tests; + +public sealed class DumbActionLoader : IActionLoader +{ + public IAction LoadAction(long index, IValue value) + { + if (Registry.IsSystemAction(value)) + { + return Registry.Deserialize(value); + } + + var action = new DumbAction(); + action.LoadPlainValue(value); + return action; + } +} diff --git a/sdk/node/Libplanet.Node.Tests/Services/BlockChainServiceTest.cs b/sdk/node/Libplanet.Node.Tests/Services/BlockChainServiceTest.cs index 662821647cd..7e3a67e8316 100644 --- a/sdk/node/Libplanet.Node.Tests/Services/BlockChainServiceTest.cs +++ b/sdk/node/Libplanet.Node.Tests/Services/BlockChainServiceTest.cs @@ -16,20 +16,4 @@ public void Create_Test() Assert.Equal(1, blockChain.Count); } - - [Fact] - public async Task BlockAppended_TestAsync() - { - var serviceProvider = TestUtility.CreateServiceProvider(); - var blockChainService = serviceProvider.GetRequiredService(); - var blockChain = blockChainService.BlockChain; - - var args = await Assert.RaisesAsync( - handler => blockChainService.BlockAppended += handler, - handler => blockChainService.BlockAppended -= handler, - async () => await BlockChainUtility.AppendBlockAsync(blockChain)); - - Assert.Equal(args.Arguments.Block, blockChain.Tip); - Assert.Equal(2, blockChain.Count); - } } diff --git a/sdk/node/Libplanet.Node.Tests/Services/RendererServiceTest.cs b/sdk/node/Libplanet.Node.Tests/Services/RendererServiceTest.cs new file mode 100644 index 00000000000..782fca91ae5 --- /dev/null +++ b/sdk/node/Libplanet.Node.Tests/Services/RendererServiceTest.cs @@ -0,0 +1,108 @@ +using Libplanet.Action; +using Libplanet.Node.Options; +using Libplanet.Node.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Libplanet.Node.Tests.Services; + +public class RendererServiceTest +{ + [Fact] + public async Task RenderBlock_TestAsync() + { + var serviceProvider = TestUtility.CreateServiceProvider(); + var blockChainService = serviceProvider.GetRequiredService(); + var rendererService = serviceProvider.GetRequiredService(); + var blockChain = blockChainService.BlockChain; + + using var observer = new TestObserver(rendererService.RenderBlock); + await Assert.RaisesAnyAsync( + attach: handler => observer.Next += handler, + detach: handler => observer.Next -= handler, + testCode: async () => await BlockChainUtility.AppendBlockAsync(blockChain)); + } + + [Fact] + public async Task RenderAction_TestAsync() + { + var settings = new Dictionary + { + [$"{ActionOptions.Position}:{nameof(ActionOptions.ModulePath)}"] + = typeof(DumbActionLoader).Assembly.Location, + [$"{ActionOptions.Position}:{nameof(ActionOptions.ActionLoaderType)}"] + = typeof(DumbActionLoader).FullName, + }; + + var serviceProvider = TestUtility.CreateServiceProvider(settings); + var blockChainService = serviceProvider.GetRequiredService(); + var rendererService = serviceProvider.GetRequiredService(); + var blockChain = blockChainService.BlockChain; + + var actions = new IAction[] + { + new DumbAction(), + new DumbAction(), + new DumbAction(), + }; + + using var observer = new TestObserver(rendererService.RenderAction); + await Assert.RaisesAnyAsync( + attach: handler => observer.Next += handler, + detach: handler => observer.Next -= handler, + testCode: async () => + { + BlockChainUtility.StageTransaction(blockChain, actions); + await BlockChainUtility.AppendBlockAsync(blockChain); + }); + } + + [Fact] + public async Task RenderActionError_TestAsync() + { + var settings = new Dictionary + { + [$"{ActionOptions.Position}:{nameof(ActionOptions.ModulePath)}"] + = typeof(DumbActionLoader).Assembly.Location, + [$"{ActionOptions.Position}:{nameof(ActionOptions.ActionLoaderType)}"] + = typeof(DumbActionLoader).FullName, + }; + + var serviceProvider = TestUtility.CreateServiceProvider(settings); + var blockChainService = serviceProvider.GetRequiredService(); + var rendererService = serviceProvider.GetRequiredService(); + var blockChain = blockChainService.BlockChain; + var errorMessage = "123"; + + var actions = new IAction[] + { + new DumbAction() { ErrorMessage = errorMessage }, + }; + + using var observer = new TestObserver( + rendererService.RenderActionError); + var errorInfo = await Assert.RaisesAnyAsync( + attach: handler => observer.Next += handler, + detach: handler => observer.Next -= handler, + testCode: async () => + { + BlockChainUtility.StageTransaction(blockChain, actions); + await BlockChainUtility.AppendBlockAsync(blockChain); + }); + Assert.Equal(errorMessage, errorInfo.Arguments.Exception.InnerException!.Message); + } + + [Fact] + public async Task RenderBlockEnd_TestAsync() + { + var serviceProvider = TestUtility.CreateServiceProvider(); + var blockChainService = serviceProvider.GetRequiredService(); + var rendererService = serviceProvider.GetRequiredService(); + var blockChain = blockChainService.BlockChain; + + using var observer = new TestObserver(rendererService.RenderBlockEnd); + await Assert.RaisesAnyAsync( + attach: handler => observer.Next += handler, + detach: handler => observer.Next -= handler, + testCode: async () => await BlockChainUtility.AppendBlockAsync(blockChain)); + } +} diff --git a/sdk/node/Libplanet.Node.Tests/Services/SwarmServiceTest.cs b/sdk/node/Libplanet.Node.Tests/Services/SwarmServiceTest.cs index 4817a9fc0bd..93cfadbfc14 100644 --- a/sdk/node/Libplanet.Node.Tests/Services/SwarmServiceTest.cs +++ b/sdk/node/Libplanet.Node.Tests/Services/SwarmServiceTest.cs @@ -1,6 +1,7 @@ using Libplanet.Node.Options; using Libplanet.Node.Services; using Microsoft.Extensions.DependencyInjection; +using R3; namespace Libplanet.Node.Tests.Services; @@ -33,9 +34,10 @@ public async Task Start_TestAsync() var swarmService = serviceProvider.GetRequiredService(); var swarmServiceHost = serviceProvider.GetRequiredService(); - await Assert.RaisesAnyAsync( - handler => swarmService.Started += handler, - handler => swarmService.Started -= handler, + using var observer = new TestObserver(swarmService.Started); + await Assert.RaisesAnyAsync( + attach: handler => observer.Next += handler, + detach: handler => observer.Next -= handler, async () => await swarmServiceHost.StartAsync(default)); Assert.True(swarmService.IsRunning); } @@ -60,9 +62,10 @@ public async Task Stop_TestAsync() var swarmServiceHost = serviceProvider.GetRequiredService(); await swarmServiceHost.StartAsync(default); - await Assert.RaisesAnyAsync( - handler => swarmService.Stopped += handler, - handler => swarmService.Stopped -= handler, + using var observer = new TestObserver(swarmService.Stopped); + await Assert.RaisesAnyAsync( + attach: handler => observer.Next += handler, + detach: handler => observer.Next -= handler, async () => await swarmServiceHost.StopAsync(default)); Assert.False(swarmService.IsRunning); } diff --git a/sdk/node/Libplanet.Node.Tests/TestObserver.cs b/sdk/node/Libplanet.Node.Tests/TestObserver.cs new file mode 100644 index 00000000000..43afb991ecb --- /dev/null +++ b/sdk/node/Libplanet.Node.Tests/TestObserver.cs @@ -0,0 +1,32 @@ +namespace Libplanet.Node.Tests; + +internal sealed class TestObserver : IObserver, IDisposable +{ + private IDisposable? _subscription; + + public TestObserver(IObservable observable) + { + _subscription = observable.Subscribe(this); + } + + public event EventHandler? Completed; + + public event EventHandler? Error; + + public event EventHandler? Next; + + public void Dispose() + { + if (_subscription is not null) + { + _subscription.Dispose(); + _subscription = null; + } + } + + void IObserver.OnCompleted() => Completed?.Invoke(this, EventArgs.Empty); + + void IObserver.OnError(Exception error) => Error?.Invoke(this, EventArgs.Empty); + + void IObserver.OnNext(T value) => Next?.Invoke(this, value); +} diff --git a/sdk/node/Libplanet.Node/Actions/PluginLoader.cs b/sdk/node/Libplanet.Node/Actions/PluginLoader.cs index e4b9be28750..352c1e0e12e 100644 --- a/sdk/node/Libplanet.Node/Actions/PluginLoader.cs +++ b/sdk/node/Libplanet.Node/Actions/PluginLoader.cs @@ -1,4 +1,6 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.Loader; using Libplanet.Action; using Libplanet.Action.Loader; @@ -37,8 +39,33 @@ private static T Create(Assembly assembly, string typeName) return obj; } + private static bool TryGetLoadedAssembly( + string modulePath, [MaybeNullWhen(false)] out Assembly assembly) + { + var loadedAssemblies = AssemblyLoadContext.All + .SelectMany(context => context.Assemblies) + .ToList(); + var comparison = StringComparison.OrdinalIgnoreCase; + var comparer = new Predicate( + assembly => string.Equals(assembly.Location, modulePath, comparison)); + + if (loadedAssemblies.Find(comparer) is Assembly loadedAssembly) + { + assembly = loadedAssembly; + return true; + } + + assembly = null; + return false; + } + private static Assembly LoadAssembly(string modulePath) { + if (TryGetLoadedAssembly(modulePath, out var assembly)) + { + return assembly; + } + var loadContext = new PluginLoadContext(modulePath); if (File.Exists(modulePath)) { diff --git a/sdk/node/Libplanet.Node/DumbAction.cs b/sdk/node/Libplanet.Node/DumbAction.cs deleted file mode 100644 index 1df69398acd..00000000000 --- a/sdk/node/Libplanet.Node/DumbAction.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Bencodex.Types; -using Libplanet.Action; -using Libplanet.Action.State; - -namespace Libplanet.Node; - -public sealed class DumbAction : IAction -{ - public IValue PlainValue => Dictionary.Empty; - - public void LoadPlainValue(IValue plainValue) - { - // Do nothing. - } - - public IWorld Execute(IActionContext context) => - context.PreviousState; -} diff --git a/sdk/node/Libplanet.Node/Libplanet.Node.csproj b/sdk/node/Libplanet.Node/Libplanet.Node.csproj index a40c4b8d379..c4f5e4198eb 100644 --- a/sdk/node/Libplanet.Node/Libplanet.Node.csproj +++ b/sdk/node/Libplanet.Node/Libplanet.Node.csproj @@ -9,6 +9,7 @@ + diff --git a/sdk/node/Libplanet.Node/Options/IEnabledOptions.cs b/sdk/node/Libplanet.Node/Options/IEnabledOptions.cs deleted file mode 100644 index 7b1328fe1ed..00000000000 --- a/sdk/node/Libplanet.Node/Options/IEnabledOptions.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Libplanet.Node.Options; - -public interface IEnabledOptions -{ - bool IsEnabled { get; } -} diff --git a/sdk/node/Libplanet.Node/Options/Schema/OptionsSchemaGenerator.cs b/sdk/node/Libplanet.Node/Options/Schema/OptionsSchemaGenerator.cs index eeddd6fcd8c..a0f7dcb334e 100644 --- a/sdk/node/Libplanet.Node/Options/Schema/OptionsSchemaGenerator.cs +++ b/sdk/node/Libplanet.Node/Options/Schema/OptionsSchemaGenerator.cs @@ -1,8 +1,4 @@ -using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Globalization; -using System.Reflection; -using System.Runtime.Serialization; using Libplanet.Node.DataAnnotations; using Namotion.Reflection; using NJsonSchema; diff --git a/sdk/node/Libplanet.Node/Options/SoloOptions.cs b/sdk/node/Libplanet.Node/Options/SoloOptions.cs index 85ac33c3017..f275a276de0 100644 --- a/sdk/node/Libplanet.Node/Options/SoloOptions.cs +++ b/sdk/node/Libplanet.Node/Options/SoloOptions.cs @@ -4,7 +4,7 @@ namespace Libplanet.Node.Options; [Options(Position)] -public class SoloOptions : OptionsBase, IEnabledOptions +public class SoloOptions : OptionsBase { public const string Position = "Solo"; diff --git a/sdk/node/Libplanet.Node/Options/SwarmOptions.cs b/sdk/node/Libplanet.Node/Options/SwarmOptions.cs index b51b41aeeed..2df82ced0ab 100644 --- a/sdk/node/Libplanet.Node/Options/SwarmOptions.cs +++ b/sdk/node/Libplanet.Node/Options/SwarmOptions.cs @@ -4,7 +4,7 @@ namespace Libplanet.Node.Options; [Options(Position)] -public sealed class SwarmOptions : AppProtocolOptionsBase, IEnabledOptions +public sealed class SwarmOptions : AppProtocolOptionsBase { public const string Position = "Swarm"; diff --git a/sdk/node/Libplanet.Node/Options/ValidatorOptions.cs b/sdk/node/Libplanet.Node/Options/ValidatorOptions.cs index 68059fba4b5..c6a336abeec 100644 --- a/sdk/node/Libplanet.Node/Options/ValidatorOptions.cs +++ b/sdk/node/Libplanet.Node/Options/ValidatorOptions.cs @@ -4,7 +4,7 @@ namespace Libplanet.Node.Options; [Options(Position)] -public sealed class ValidatorOptions : AppProtocolOptionsBase, IEnabledOptions +public sealed class ValidatorOptions : AppProtocolOptionsBase { public const string Position = "Validator"; diff --git a/sdk/node/Libplanet.Node/Options/ValidatorOptionsConfigurator.cs b/sdk/node/Libplanet.Node/Options/ValidatorOptionsConfigurator.cs index 3f391d21552..e100cdc62bd 100644 --- a/sdk/node/Libplanet.Node/Options/ValidatorOptionsConfigurator.cs +++ b/sdk/node/Libplanet.Node/Options/ValidatorOptionsConfigurator.cs @@ -1,5 +1,3 @@ -using Libplanet.Common; -using Libplanet.Crypto; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/sdk/node/Libplanet.Node/Services/BlockChainService.cs b/sdk/node/Libplanet.Node/Services/BlockChainService.cs index 48237101f28..fa635248dd1 100644 --- a/sdk/node/Libplanet.Node/Services/BlockChainService.cs +++ b/sdk/node/Libplanet.Node/Services/BlockChainService.cs @@ -1,8 +1,6 @@ -using System.Collections.Concurrent; using System.Collections.Immutable; using System.Diagnostics; using System.Numerics; -using System.Security.Cryptography; using Bencodex; using Bencodex.Types; using Libplanet.Action; @@ -11,84 +9,35 @@ using Libplanet.Blockchain; using Libplanet.Blockchain.Policies; using Libplanet.Blockchain.Renderers; -using Libplanet.Common; using Libplanet.Crypto; using Libplanet.Node.Options; using Libplanet.Store; using Libplanet.Types.Blocks; using Libplanet.Types.Consensus; using Libplanet.Types.Tx; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Libplanet.Node.Services; -internal sealed class BlockChainService : IBlockChainService, IActionRenderer +internal sealed class BlockChainService( + IOptions genesisOptions, + IStoreService storeService, + IActionService actionService, + PolicyService policyService, + RendererService rendererService) : IBlockChainService { private static readonly Codec _codec = new(); - private readonly SynchronizationContext _synchronizationContext; - private readonly ILogger _logger; - private readonly BlockChain _blockChain; - private readonly ConcurrentDictionary _eventByTxId = []; - private readonly ConcurrentDictionary _exceptionByAction = []; - - public BlockChainService( - IOptions genesisOptions, - IStoreService storeService, - IActionService actionService, - PolicyService policyService, - ILogger logger) - { - _synchronizationContext = SynchronizationContext.Current ?? new(); - _logger = logger; - _blockChain = CreateBlockChain( - genesisOptions: genesisOptions.Value, - store: storeService.Store, - stateStore: storeService.StateStore, - actionLoader: actionService.ActionLoader, - policyActionsRegistry: actionService.PolicyActionsRegistry, - stagePolicy: policyService.StagePolicy, - renderers: [this]); - } - - public event EventHandler? BlockAppended; + private readonly BlockChain _blockChain = CreateBlockChain( + genesisOptions: genesisOptions.Value, + store: storeService.Store, + stateStore: storeService.StateStore, + actionLoader: actionService.ActionLoader, + policyActionsRegistry: actionService.PolicyActionsRegistry, + stagePolicy: policyService.StagePolicy, + renderers: [rendererService]); public BlockChain BlockChain => _blockChain; - void IRenderer.RenderBlock(Block oldTip, Block newTip) - { - } - - void IActionRenderer.RenderAction( - IValue action, ICommittedActionContext context, HashDigest nextState) - { - } - - void IActionRenderer.RenderActionError( - IValue action, ICommittedActionContext context, Exception exception) - { - _exceptionByAction.AddOrUpdate(action, exception, (_, _) => exception); - } - - void IActionRenderer.RenderBlockEnd(Block oldTip, Block newTip) - { - _synchronizationContext.Post(Action, state: null); - - void Action(object? state) - { - foreach (var transaction in newTip.Transactions) - { - if (_eventByTxId.TryGetValue(transaction.Id, out var manualResetEvent)) - { - manualResetEvent.Set(); - } - } - - _logger.LogInformation("#{Height}: Block appended", newTip.Index); - BlockAppended?.Invoke(this, new(newTip)); - } - } - private static BlockChain CreateBlockChain( GenesisOptions genesisOptions, IStore store, diff --git a/sdk/node/Libplanet.Node/Services/BlockEventArgs.cs b/sdk/node/Libplanet.Node/Services/BlockEventArgs.cs deleted file mode 100644 index f722df3b3df..00000000000 --- a/sdk/node/Libplanet.Node/Services/BlockEventArgs.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Libplanet.Types.Blocks; - -namespace Libplanet.Node.Services; - -public sealed class BlockEventArgs(Block block) : EventArgs -{ - public Block Block { get; } = block; -} diff --git a/sdk/node/Libplanet.Node/Services/IActionLoaderProvider.cs b/sdk/node/Libplanet.Node/Services/IActionLoaderProvider.cs deleted file mode 100644 index 2d0918342a6..00000000000 --- a/sdk/node/Libplanet.Node/Services/IActionLoaderProvider.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Libplanet.Action.Loader; - -namespace Libplanet.Node.Services; - -public interface IActionLoaderProvider -{ - IActionLoader GetActionLoader(); -} diff --git a/sdk/node/Libplanet.Node/Services/IBlockChainService.cs b/sdk/node/Libplanet.Node/Services/IBlockChainService.cs index 12b543bf49b..bc70ea2954b 100644 --- a/sdk/node/Libplanet.Node/Services/IBlockChainService.cs +++ b/sdk/node/Libplanet.Node/Services/IBlockChainService.cs @@ -4,7 +4,5 @@ namespace Libplanet.Node.Services; public interface IBlockChainService { - event EventHandler? BlockAppended; - BlockChain BlockChain { get; } } diff --git a/sdk/node/Libplanet.Node/Services/IRendererService.cs b/sdk/node/Libplanet.Node/Services/IRendererService.cs new file mode 100644 index 00000000000..2bf191341e9 --- /dev/null +++ b/sdk/node/Libplanet.Node/Services/IRendererService.cs @@ -0,0 +1,12 @@ +namespace Libplanet.Node.Services; + +public interface IRendererService +{ + IObservable RenderBlock { get; } + + IObservable RenderAction { get; } + + IObservable RenderActionError { get; } + + IObservable RenderBlockEnd { get; } +} diff --git a/sdk/node/Libplanet.Node/Services/ISwarmService.cs b/sdk/node/Libplanet.Node/Services/ISwarmService.cs index c2e8da534c3..07cc6782037 100644 --- a/sdk/node/Libplanet.Node/Services/ISwarmService.cs +++ b/sdk/node/Libplanet.Node/Services/ISwarmService.cs @@ -1,12 +1,13 @@ using Libplanet.Net; +using R3; namespace Libplanet.Node.Services; public interface ISwarmService { - public event EventHandler? Started; + IObservable Started { get; } - public event EventHandler? Stopped; + IObservable Stopped { get; } Swarm Swarm { get; } diff --git a/sdk/node/Libplanet.Node/Services/RenderActionErrorInfo.cs b/sdk/node/Libplanet.Node/Services/RenderActionErrorInfo.cs new file mode 100644 index 00000000000..eb917d1e3d3 --- /dev/null +++ b/sdk/node/Libplanet.Node/Services/RenderActionErrorInfo.cs @@ -0,0 +1,9 @@ +using Bencodex.Types; +using Libplanet.Action; + +namespace Libplanet.Node.Services; + +public readonly record struct RenderActionErrorInfo( + IValue Action, + ICommittedActionContext Context, + Exception Exception); diff --git a/sdk/node/Libplanet.Node/Services/RenderActionInfo.cs b/sdk/node/Libplanet.Node/Services/RenderActionInfo.cs new file mode 100644 index 00000000000..f433c13bf25 --- /dev/null +++ b/sdk/node/Libplanet.Node/Services/RenderActionInfo.cs @@ -0,0 +1,11 @@ +using System.Security.Cryptography; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Common; + +namespace Libplanet.Node.Services; + +public readonly record struct RenderActionInfo( + IValue Action, + ICommittedActionContext Context, + HashDigest NextState); diff --git a/sdk/node/Libplanet.Node/Services/RenderBlockInfo.cs b/sdk/node/Libplanet.Node/Services/RenderBlockInfo.cs new file mode 100644 index 00000000000..7d2c23cda11 --- /dev/null +++ b/sdk/node/Libplanet.Node/Services/RenderBlockInfo.cs @@ -0,0 +1,7 @@ +using Libplanet.Types.Blocks; + +namespace Libplanet.Node.Services; + +public readonly record struct RenderBlockInfo( + Block OldTip, + Block NewTip); diff --git a/sdk/node/Libplanet.Node/Services/RendererService.cs b/sdk/node/Libplanet.Node/Services/RendererService.cs new file mode 100644 index 00000000000..8925167cb04 --- /dev/null +++ b/sdk/node/Libplanet.Node/Services/RendererService.cs @@ -0,0 +1,117 @@ +using System.Security.Cryptography; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Blockchain.Renderers; +using Libplanet.Common; +using Libplanet.Types.Blocks; +using Microsoft.Extensions.Logging; +using R3; + +namespace Libplanet.Node.Services; + +internal sealed class RendererService : IRendererService, IActionRenderer, IDisposable +{ + private readonly Subject _renderBlock = new(); + private readonly Subject _renderAction = new(); + private readonly Subject _renderActionError = new(); + private readonly Subject _renderBlockEnd = new(); + private readonly SynchronizationContext _synchronizationContext; + private readonly ILogger _logger; + + private IObservable? _renderBlockObservable; + private IObservable? _renderActionObservable; + private IObservable? _renderActionErrorObservable; + private IObservable? _renderBlockEndObservable; + + public RendererService( + SynchronizationContext synchronizationContext, + ILogger logger) + { + _synchronizationContext = synchronizationContext; + _logger = logger; + _renderBlockObservable = _renderBlock.AsSystemObservable(); + _renderActionObservable = _renderAction.AsSystemObservable(); + _renderActionErrorObservable = _renderActionError.AsSystemObservable(); + _renderBlockEndObservable = _renderBlock.AsSystemObservable(); + } + + IObservable IRendererService.RenderBlock + => _renderBlockObservable ??= _renderBlock.AsSystemObservable(); + + IObservable IRendererService.RenderAction + => _renderActionObservable ??= _renderAction.AsSystemObservable(); + + IObservable IRendererService.RenderActionError + => _renderActionErrorObservable ??= _renderActionError.AsSystemObservable(); + + IObservable IRendererService.RenderBlockEnd + => _renderBlockEndObservable ??= _renderBlock.AsSystemObservable(); + + public void Dispose() + { + _renderBlock.Dispose(); + _renderAction.Dispose(); + _renderActionError.Dispose(); + _renderBlockEnd.Dispose(); + } + + void IActionRenderer.RenderAction( + IValue action, ICommittedActionContext context, HashDigest nextState) + { + _synchronizationContext.Post( + state => + { + _renderAction.OnNext(new(action, context, nextState)); + _logger.LogDebug( + "Rendered an action: {Action} {Context} {NextState}", + action, + context, + nextState); + }, + null); + } + + void IActionRenderer.RenderActionError( + IValue action, ICommittedActionContext context, Exception exception) + { + _synchronizationContext.Post( + state => + { + _renderActionError.OnNext(new(action, context, exception)); + _logger.LogError( + exception, + "Failed to render an action: {Action} {Context}", + action, + context); + }, + null); + } + + void IRenderer.RenderBlock(Block oldTip, Block newTip) + { + _synchronizationContext.Post( + state => + { + _renderBlock.OnNext(new(oldTip, newTip)); + _logger.LogDebug( + "Rendered a block: {OldTip} {NewTip}", + oldTip, + newTip); + }, + null); + } + + void IActionRenderer.RenderBlockEnd(Block oldTip, Block newTip) + { + _synchronizationContext.Post( + state => + { + _renderBlockEnd.OnNext(new(oldTip, newTip)); + _logger.LogDebug( + "Rendered a block end: {OldTip} {NewTip}", + oldTip, + newTip); + }, + null); + } +} diff --git a/sdk/node/Libplanet.Node/Services/SwarmService.cs b/sdk/node/Libplanet.Node/Services/SwarmService.cs index eed37c4357d..8b872d05fe9 100644 --- a/sdk/node/Libplanet.Node/Services/SwarmService.cs +++ b/sdk/node/Libplanet.Node/Services/SwarmService.cs @@ -1,4 +1,3 @@ -using System.Collections.Immutable; using System.Net; using Libplanet.Common; using Libplanet.Crypto; @@ -9,6 +8,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using R3; namespace Libplanet.Node.Services; @@ -22,15 +22,22 @@ internal sealed class SwarmService( private readonly SwarmOptions _options = options.Value; private readonly ValidatorOptions _validatorOptions = validatorOptions.Value; private readonly ILogger _logger = logger; + private readonly Subject _started = new(); + private readonly Subject _stopped = new(); + + private IObservable? _startedObservable; + private IObservable? _stoppedObservable; private Swarm? _swarm; private Task _startTask = Task.CompletedTask; private Seed? _blocksyncSeed; private Seed? _consensusSeed; - public event EventHandler? Started; + IObservable ISwarmService.Started + => _startedObservable ??= _started.AsSystemObservable(); - public event EventHandler? Stopped; + IObservable ISwarmService.Stopped + => _stoppedObservable ??= _stopped.AsSystemObservable(); public bool IsRunning => _swarm is not null; @@ -114,7 +121,7 @@ public async Task StartAsync(CancellationToken cancellationToken) _logger.LogDebug("Node.Swarm is starting: {Address}", _swarm.Address); await _swarm.BootstrapAsync(cancellationToken: default); _logger.LogDebug("Node.Swarm is bootstrapped: {Address}", _swarm.Address); - Started?.Invoke(this, EventArgs.Empty); + _started.OnNext(Unit.Default); } public async Task StopAsync(CancellationToken cancellationToken) @@ -145,7 +152,7 @@ public async Task StopAsync(CancellationToken cancellationToken) _blocksyncSeed = null; } - Stopped?.Invoke(this, EventArgs.Empty); + _stopped.OnNext(Unit.Default); } public async ValueTask DisposeAsync()