diff --git a/src/client/LibplanetConsole.Client/Commands/TxCommand.cs b/src/client/LibplanetConsole.Client/Commands/TxCommand.cs index 3689765e..4a03b21c 100644 --- a/src/client/LibplanetConsole.Client/Commands/TxCommand.cs +++ b/src/client/LibplanetConsole.Client/Commands/TxCommand.cs @@ -5,7 +5,7 @@ namespace LibplanetConsole.Client.Commands; [CommandSummary("Sends a transaction using a string")] -internal sealed class TxCommand(IClient client, IBlockChain blockChain) : CommandAsyncBase +internal sealed class TxCommand(IClient client) : CommandAsyncBase { [CommandPropertyRequired] [CommandSummary("Specifies the text to send")] @@ -17,7 +17,7 @@ protected override async Task OnExecuteAsync(CancellationToken cancellationToken { Value = Text, }; - await blockChain.SendTransactionAsync([action], cancellationToken); + await client.SendTransactionAsync([action], cancellationToken); await Out.WriteLineAsync($"{client.Address.ToShortString()}: {Text}"); } } diff --git a/src/client/LibplanetConsole.Client/IClient.cs b/src/client/LibplanetConsole.Client/IClient.cs index f52a3f1c..38bdb1d7 100644 --- a/src/client/LibplanetConsole.Client/IClient.cs +++ b/src/client/LibplanetConsole.Client/IClient.cs @@ -23,4 +23,6 @@ public interface IClient : IVerifier Task StartAsync(CancellationToken cancellationToken); Task StopAsync(CancellationToken cancellationToken); + + Task SendTransactionAsync(IAction[] actions, CancellationToken cancellationToken); } diff --git a/src/console/LibplanetConsole.Console.Executable/EntryCommands/InitializeCommand.cs b/src/console/LibplanetConsole.Console.Executable/EntryCommands/InitializeCommand.cs index 8a61dafd..9574add2 100644 --- a/src/console/LibplanetConsole.Console.Executable/EntryCommands/InitializeCommand.cs +++ b/src/console/LibplanetConsole.Console.Executable/EntryCommands/InitializeCommand.cs @@ -27,6 +27,11 @@ public InitializeCommand() [NonNegative] public int Port { get; set; } + [CommandProperty] + [CommandSummary("Specifies the private key of the genesis")] + [PrivateKey] + public string PrivateKey { get; set; } = string.Empty; + #if DEBUG [CommandProperty(InitValue = 1)] #else @@ -61,12 +66,6 @@ public InitializeCommand() [CommandSummary("If set, the command will not output any information")] public bool Quiet { get; set; } - [CommandProperty] - [CommandSummary("Specifies the private key of the genesis block")] - [PrivateKey] - [Category("Genesis")] - public string GenesisKey { get; set; } = string.Empty; - [CommandProperty("timestamp")] [CommandSummary("Specifies the timestamp of the genesis block")] [Category("Genesis")] @@ -124,7 +123,7 @@ private async Task ExecuteAsync(CancellationToken cancellationToken) { using var progress = new CommandProgress(); var portGenerator = new PortGenerator(Port); - var genesisKey = PrivateKeyUtility.ParseOrRandom(GenesisKey); + var genesisKey = PrivateKeyUtility.ParseOrRandom(PrivateKey); var ports = portGenerator.Next(); var nodeOptions = GetNodeOptions(portGenerator, ports); var clientOptions = GetClientOptions(portGenerator); @@ -142,6 +141,7 @@ private async Task ExecuteAsync(CancellationToken cancellationToken) var apvPrivateKey = PrivateKeyUtility.ParseOrRandom(APVPrivateKey); var repository = new Repository(ports, nodeOptions, clientOptions) { + PrivateKey = genesisKey, Port = Port, Genesis = Repository.CreateGenesis(genesisOptions), AppProtocolVersion = Repository.CreateAppProtocolVersion( diff --git a/src/console/LibplanetConsole.Console.Executable/EntryCommands/RunCommand.cs b/src/console/LibplanetConsole.Console.Executable/EntryCommands/RunCommand.cs index f5472a01..4129da26 100644 --- a/src/console/LibplanetConsole.Console.Executable/EntryCommands/RunCommand.cs +++ b/src/console/LibplanetConsole.Console.Executable/EntryCommands/RunCommand.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using JSSoft.Commands; using LibplanetConsole.Common; +using LibplanetConsole.Common.DataAnnotations; using LibplanetConsole.DataAnnotations; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Options; @@ -22,6 +23,11 @@ internal sealed class RunCommand [NonNegative] public int Port { get; init; } + [CommandProperty] + [CommandSummary("Specifies the private key for the genesis")] + [PrivateKey] + public string PrivateKey { get; init; } = string.Empty; + #if DEBUG [CommandProperty(InitValue = 1)] #else @@ -111,11 +117,14 @@ void IConfigureOptions.Configure(ApplicationOptions options) var ports = _ports ?? throw new InvalidOperationException("PortGroup is not initialized."); var nodeOptions = GetNodeOptions(GetNodes(), portGenerator); var clientOptions = GetClientOptions(GetClients(), portGenerator); + var privateKey = PrivateKeyUtility.ParseOrRandom(PrivateKey); var repository = new Repository(ports, nodeOptions, clientOptions) { + PrivateKey = privateKey, ActionProviderModulePath = ActionProviderModulePath, ActionProviderType = ActionProviderType, }; + options.PrivateKey = PrivateKeyUtility.ToString(privateKey); options.LogPath = GetFullPath(LogPath); options.Nodes = repository.Nodes; options.Clients = repository.Clients; @@ -134,7 +143,7 @@ void IConfigureOptions.Configure(ApplicationOptions options) if (options.Genesis == string.Empty && options.GenesisPath == string.Empty) { var genesis = repository.CreateGenesis( - genesisKey: new PrivateKey(), DateTimeOffset.UtcNow); + genesisKey: privateKey, DateTimeOffset.UtcNow); options.Genesis = ByteUtil.Hex(genesis); } diff --git a/src/console/LibplanetConsole.Console.Executable/Repository.cs b/src/console/LibplanetConsole.Console.Executable/Repository.cs index 25311309..eb5912e4 100644 --- a/src/console/LibplanetConsole.Console.Executable/Repository.cs +++ b/src/console/LibplanetConsole.Console.Executable/Repository.cs @@ -29,6 +29,8 @@ public Repository(PortGroup ports, NodeOptions[] nodes, ClientOptions[] clients) Clients = clients; } + public required PrivateKey PrivateKey { get; init; } + public int Port { get; set; } public NodeOptions[] Nodes { get; } = []; @@ -187,6 +189,7 @@ public async Task SaveAsync( var consensusPort = ConsensusPort is not 0 ? ConsensusPort : _ports[5]; var applicationOptions = new ApplicationOptions { + PrivateKey = PrivateKeyUtility.ToString(PrivateKey), GenesisPath = PathUtility.GetRelativePath(settingsPath, genesisPath), AppProtocolVersionPath = PathUtility.GetRelativePath( settingsPath, appProtocolVersionPath), diff --git a/src/console/LibplanetConsole.Console/ApplicationOptions.cs b/src/console/LibplanetConsole.Console/ApplicationOptions.cs index 5c7ce121..5f7925b0 100644 --- a/src/console/LibplanetConsole.Console/ApplicationOptions.cs +++ b/src/console/LibplanetConsole.Console/ApplicationOptions.cs @@ -1,5 +1,7 @@ using System.Text.Json.Serialization; using Libplanet.Net; +using LibplanetConsole.Common; +using LibplanetConsole.Common.DataAnnotations; using LibplanetConsole.Options; namespace LibplanetConsole.Console; @@ -11,9 +13,15 @@ public sealed class ApplicationOptions : OptionsBase, IAppli public const int SeedBlocksyncPortIncrement = 6; public const int SeedConsensusPortIncrement = 7; + private PrivateKey? _privateKey; private Block? _genesisBlock; private AppProtocolVersion? _appProtocolVersion; + [PrivateKey] + public string PrivateKey { get; set; } = string.Empty; + + PrivateKey IApplicationOptions.PrivateKey => ActualPrivateKey; + [JsonIgnore] public NodeOptions[] Nodes { get; set; } = []; @@ -54,6 +62,9 @@ AppProtocolVersion IApplicationOptions.AppProtocolVersion ProcessOptions? IApplicationOptions.ProcessOptions => NoProcess ? null : new ProcessOptions { Detach = Detach, NewWindow = NewWindow, }; + private PrivateKey ActualPrivateKey + => _privateKey ??= PrivateKeyUtility.ParseOrRandom(PrivateKey); + private Block GetGenesisBlock() { if (GenesisPath != string.Empty) diff --git a/src/console/LibplanetConsole.Console/BlockChain.cs b/src/console/LibplanetConsole.Console/BlockChain.cs index 80b5d08a..05af28b5 100644 --- a/src/console/LibplanetConsole.Console/BlockChain.cs +++ b/src/console/LibplanetConsole.Console/BlockChain.cs @@ -5,16 +5,21 @@ namespace LibplanetConsole.Console; -internal sealed class BlockChain : IBlockChain, IDisposable +internal sealed class BlockChain : IBlockChain, IDisposable, IConsole { - private readonly INodeCollection _nodes; + private readonly NodeCollection _nodes; + private readonly PrivateKey _privateKey; + private readonly BlockHash _genesisHash; private readonly ILogger _logger; private IBlockChain? _blockChain; + private Node? _node; private bool _isDisposed; - public BlockChain(NodeCollection nodes, ILogger logger) + public BlockChain(NodeCollection nodes, IApplicationOptions options, ILogger logger) { _nodes = nodes; + _privateKey = options.PrivateKey; + _genesisHash = options.GenesisBlock.Hash; _logger = logger; UpdateCurrent(_nodes.Current); _nodes.CurrentChanged += Nodes_CurrentChanged; @@ -41,15 +46,26 @@ void IDisposable.Dispose() } } - Task IBlockChain.SendTransactionAsync( + public async Task SendTransactionAsync( IAction[] actions, CancellationToken cancellationToken) { - if (IsRunning is false || _blockChain is null) + if (IsRunning is false || _blockChain is null || _node is null) { throw new InvalidOperationException("BlockChain is not running."); } - return _blockChain.SendTransactionAsync(actions, cancellationToken); + var privateKey = _privateKey; + var genesisHash = _genesisHash; + var nonce = await _blockChain.GetNextNonceAsync(privateKey.Address, cancellationToken); + var values = actions.Select(item => item.PlainValue).ToArray(); + var transaction = Transaction.Create( + nonce: nonce, + privateKey: privateKey, + genesisHash: genesisHash, + actions: new TxActionList(values)); + + await _node.SendTransactionAsync(transaction, cancellationToken); + return transaction.Id; } Task IBlockChain.GetNextNonceAsync( @@ -128,7 +144,7 @@ Task IBlockChain.GetActionAsync( return _blockChain.GetActionAsync(txId, actionIndex, cancellationToken); } - private void UpdateCurrent(INode? node) + private void UpdateCurrent(Node? node) { if (_blockChain is not null) { @@ -144,6 +160,7 @@ private void UpdateCurrent(INode? node) } } + _node = node; _blockChain = node?.GetKeyedService(INode.Key); if (_blockChain is not null) diff --git a/src/console/LibplanetConsole.Console/Commands/ClientCommand.cs b/src/console/LibplanetConsole.Console/Commands/ClientCommand.cs index 97eb13a6..cf5d47fd 100644 --- a/src/console/LibplanetConsole.Console/Commands/ClientCommand.cs +++ b/src/console/LibplanetConsole.Console/Commands/ClientCommand.cs @@ -122,9 +122,8 @@ public async Task TxAsync( { var address = Address; var client = clients.GetClientOrCurrent(address); - var blockChain = client.GetRequiredService(); var action = new StringAction { Value = text }; - await blockChain.SendTransactionAsync([action], cancellationToken); + await client.SendTransactionAsync([action], cancellationToken); await Out.WriteLineAsync($"{client.Address.ToShortString()}: {text}"); } diff --git a/src/console/LibplanetConsole.Console/Commands/NodeCommand.cs b/src/console/LibplanetConsole.Console/Commands/NodeCommand.cs index 3c24a614..79e9f519 100644 --- a/src/console/LibplanetConsole.Console/Commands/NodeCommand.cs +++ b/src/console/LibplanetConsole.Console/Commands/NodeCommand.cs @@ -116,9 +116,8 @@ public async Task TxAsync( { var address = Address; var node = nodes.GetNodeOrCurrent(address); - var blockChain = node.GetRequiredService(); var action = new StringAction { Value = text }; - await blockChain.SendTransactionAsync([action], cancellationToken); + await node.SendTransactionAsync([action], cancellationToken); await Out.WriteLineAsync($"{node.Address.ToShortString()}: {text}"); } diff --git a/src/console/LibplanetConsole.Console/Commands/TxCommand.cs b/src/console/LibplanetConsole.Console/Commands/TxCommand.cs index 48cda8e9..d0b785f9 100644 --- a/src/console/LibplanetConsole.Console/Commands/TxCommand.cs +++ b/src/console/LibplanetConsole.Console/Commands/TxCommand.cs @@ -22,16 +22,14 @@ protected override async Task OnExecuteAsync(CancellationToken cancellationToken var text = Text; if (addressable is INode node) { - var blockChain = node.GetRequiredService(); var action = new StringAction { Value = text }; - await blockChain.SendTransactionAsync([action], cancellationToken); + await node.SendTransactionAsync([action], cancellationToken); await Out.WriteLineAsync($"{node.Address.ToShortString()}: {text}"); } else if (addressable is IClient client) { - var blockChain = client.GetRequiredService(); var action = new StringAction { Value = text }; - await blockChain.SendTransactionAsync([action], cancellationToken); + await client.SendTransactionAsync([action], cancellationToken); await Out.WriteLineAsync($"{client.Address.ToShortString()}: {text}"); } else diff --git a/src/console/LibplanetConsole.Console/IApplicationOptions.cs b/src/console/LibplanetConsole.Console/IApplicationOptions.cs index b49e3342..01b26916 100644 --- a/src/console/LibplanetConsole.Console/IApplicationOptions.cs +++ b/src/console/LibplanetConsole.Console/IApplicationOptions.cs @@ -4,6 +4,8 @@ namespace LibplanetConsole.Console; public interface IApplicationOptions { + PrivateKey PrivateKey { get; } + NodeOptions[] Nodes { get; } ClientOptions[] Clients { get; } diff --git a/src/console/LibplanetConsole.Console/IClient.cs b/src/console/LibplanetConsole.Console/IClient.cs index b3ea4a77..14c3aced 100644 --- a/src/console/LibplanetConsole.Console/IClient.cs +++ b/src/console/LibplanetConsole.Console/IClient.cs @@ -42,4 +42,6 @@ public interface IClient : IAddressable, IAsyncDisposable, IKeyedServiceProvider Task StopAsync(CancellationToken cancellationToken); string GetCommandLine(); + + Task SendTransactionAsync(IAction[] actions, CancellationToken cancellationToken); } diff --git a/src/console/LibplanetConsole.Console/IConsole.cs b/src/console/LibplanetConsole.Console/IConsole.cs new file mode 100644 index 00000000..9417a624 --- /dev/null +++ b/src/console/LibplanetConsole.Console/IConsole.cs @@ -0,0 +1,9 @@ +using LibplanetConsole.Common; +using Microsoft.Extensions.DependencyInjection; + +namespace LibplanetConsole.Console; + +public interface IConsole +{ + Task SendTransactionAsync(IAction[] actions, CancellationToken cancellationToken); +} diff --git a/src/console/LibplanetConsole.Console/INode.cs b/src/console/LibplanetConsole.Console/INode.cs index eb348366..16432bf9 100644 --- a/src/console/LibplanetConsole.Console/INode.cs +++ b/src/console/LibplanetConsole.Console/INode.cs @@ -42,4 +42,6 @@ public interface INode : IAddressable, IAsyncDisposable, IKeyedServiceProvider, Task StopAsync(CancellationToken cancellationToken); string GetCommandLine(); + + Task SendTransactionAsync(IAction[] actions, CancellationToken cancellationToken); } diff --git a/src/console/LibplanetConsole.Console/Node.BlockChain.cs b/src/console/LibplanetConsole.Console/Node.BlockChain.cs index 84dbd459..6714bb48 100644 --- a/src/console/LibplanetConsole.Console/Node.BlockChain.cs +++ b/src/console/LibplanetConsole.Console/Node.BlockChain.cs @@ -55,6 +55,22 @@ public async Task SendTransactionAsync( return ToTxId(response.TxId); } + public async Task SendTransactionAsync( + Transaction transaction, CancellationToken cancellationToken) + { + if (_blockChainService is null) + { + throw new InvalidOperationException("BlockChainService is not initialized."); + } + + var request = new SendTransactionRequest + { + TransactionData = ToGrpc(transaction.Serialize()), + }; + var callOptions = new CallOptions(cancellationToken: cancellationToken); + await _blockChainService.SendTransactionAsync(request, callOptions); + } + public async Task GetTipHashAsync(CancellationToken cancellationToken) { if (_blockChainService is null) diff --git a/src/console/LibplanetConsole.Console/ServiceCollectionExtensions.cs b/src/console/LibplanetConsole.Console/ServiceCollectionExtensions.cs index c0f7d710..043faeb1 100644 --- a/src/console/LibplanetConsole.Console/ServiceCollectionExtensions.cs +++ b/src/console/LibplanetConsole.Console/ServiceCollectionExtensions.cs @@ -32,7 +32,8 @@ public static IServiceCollection AddConsole( @this.AddSingleton() .AddSingleton(s => s.GetRequiredService()); @this.AddSingleton() - .AddSingleton(s => s.GetRequiredService()); + .AddSingleton(s => s.GetRequiredService()) + .AddSingleton(s => s.GetRequiredService()); @this.AddSingleton() .AddSingleton(s => s.GetRequiredService()); diff --git a/src/node/LibplanetConsole.Node/ActionProvider.cs b/src/node/LibplanetConsole.Node/ActionProvider.cs index 76555237..c6fb3ec8 100644 --- a/src/node/LibplanetConsole.Node/ActionProvider.cs +++ b/src/node/LibplanetConsole.Node/ActionProvider.cs @@ -18,7 +18,7 @@ internal sealed class ActionProvider : IActionProvider public ImmutableArray EndTxActions { get; } = []; - public IAction[] GetGenesisActions(PrivateKey genesisKey, PublicKey[] validatorKeys) + public IAction[] GetGenesisActions(Address genesisAddress, PublicKey[] validatorKeys) { var validators = validatorKeys .Select(item => new Validator(item, BigInteger.One)) diff --git a/src/node/LibplanetConsole.Node/BlockUtility.cs b/src/node/LibplanetConsole.Node/BlockUtility.cs index ab0bf778..3ab49f0d 100644 --- a/src/node/LibplanetConsole.Node/BlockUtility.cs +++ b/src/node/LibplanetConsole.Node/BlockUtility.cs @@ -9,12 +9,13 @@ public static Block CreateGenesisBlock( GenesisOptions genesisOptions) { var genesisKey = genesisOptions.GenesisKey; + var genesisAddress = genesisKey.Address; var validators = genesisOptions.Validators; var timestamp = genesisOptions.Timestamp; var actionLoaderProvider = ModuleLoader.LoadActionLoader( genesisOptions.ActionProviderModulePath, genesisOptions.ActionProviderType); - var actions = actionLoaderProvider.GetGenesisActions(genesisKey, validators); + var actions = actionLoaderProvider.GetGenesisActions(genesisAddress, validators); var genesisBlock = CreateGenesisBlock(genesisKey, timestamp, actions); return genesisBlock; diff --git a/src/node/LibplanetConsole.Node/Commands/TxCommand.cs b/src/node/LibplanetConsole.Node/Commands/TxCommand.cs index c7e4132c..d85680da 100644 --- a/src/node/LibplanetConsole.Node/Commands/TxCommand.cs +++ b/src/node/LibplanetConsole.Node/Commands/TxCommand.cs @@ -5,7 +5,7 @@ namespace LibplanetConsole.Node.Commands; [CommandSummary("Sends a transaction using a string")] -internal sealed class TxCommand(INode node, IBlockChain blockChain) : CommandAsyncBase +internal sealed class TxCommand(INode node) : CommandAsyncBase { [CommandPropertyRequired] [CommandSummary("Specifies the text to send")] @@ -17,7 +17,7 @@ protected override async Task OnExecuteAsync(CancellationToken cancellationToken { Value = Text, }; - await blockChain.SendTransactionAsync([action], cancellationToken); + await node.SendTransactionAsync([action], cancellationToken); await Out.WriteLineAsync($"{node.Address.ToShortString()}: {Text}"); } } diff --git a/src/node/LibplanetConsole.Node/IActionProvider.cs b/src/node/LibplanetConsole.Node/IActionProvider.cs index 72d2342c..36753ca5 100644 --- a/src/node/LibplanetConsole.Node/IActionProvider.cs +++ b/src/node/LibplanetConsole.Node/IActionProvider.cs @@ -12,7 +12,7 @@ public interface IActionProvider ImmutableArray EndTxActions { get; } - IAction[] GetGenesisActions(PrivateKey genesisKey, PublicKey[] validatorKeys); + IAction[] GetGenesisActions(Address genesisAddress, PublicKey[] validatorKeys); IActionLoader GetActionLoader(); } diff --git a/src/node/LibplanetConsole.Node/Services/NodeGrpcServiceV1.cs b/src/node/LibplanetConsole.Node/Services/NodeGrpcServiceV1.cs index c6a9d7a1..704b7f3e 100644 --- a/src/node/LibplanetConsole.Node/Services/NodeGrpcServiceV1.cs +++ b/src/node/LibplanetConsole.Node/Services/NodeGrpcServiceV1.cs @@ -15,9 +15,7 @@ internal sealed class NodeGrpcServiceV1 : NodeGrpcService.NodeGrpcServiceBase, I private bool _isDisposed; public NodeGrpcServiceV1( - IHostApplicationLifetime applicationLifetime, - Node node, - ILogger logger) + IHostApplicationLifetime applicationLifetime, Node node, ILogger logger) { _applicationLifetime = applicationLifetime; _node = node; diff --git a/src/shared/LibplanetConsole.Blockchain/IBlockChain.cs b/src/shared/LibplanetConsole.Blockchain/IBlockChain.cs index 4ff765a1..17116be1 100644 --- a/src/shared/LibplanetConsole.Blockchain/IBlockChain.cs +++ b/src/shared/LibplanetConsole.Blockchain/IBlockChain.cs @@ -22,8 +22,6 @@ public interface IBlockChain BlockInfo Tip { get; } - Task SendTransactionAsync(IAction[] actions, CancellationToken cancellationToken); - Task GetNextNonceAsync(Address address, CancellationToken cancellationToken); Task GetStateAsync(