From 503fe37b77a47baae5a324b0d4483c7d0817c917 Mon Sep 17 00:00:00 2001 From: s2quake Date: Fri, 23 Aug 2024 14:24:59 +0900 Subject: [PATCH] feat: Add GenesisBlockPath option to GenesisOptions --- .../appsettings-schema.json | 4 + .../appsettings.Development.json | 3 +- .../LibplanetServicesExtensions.cs | 1 + .../Libplanet.Node/Options/GenesisOptions.cs | 13 ++- .../Options/GenesisOptionsConfigurator.cs | 29 +++--- .../Options/GenesisOptionsValidator.cs | 43 +++++++++ .../Services/BlockChainService.cs | 92 ++++++++++++++----- 7 files changed, 145 insertions(+), 40 deletions(-) create mode 100644 sdk/node/Libplanet.Node/Options/GenesisOptionsValidator.cs diff --git a/sdk/node/Libplanet.Node.Executable/appsettings-schema.json b/sdk/node/Libplanet.Node.Executable/appsettings-schema.json index f96e8011ae8..8f82fd5d5ff 100644 --- a/sdk/node/Libplanet.Node.Executable/appsettings-schema.json +++ b/sdk/node/Libplanet.Node.Executable/appsettings-schema.json @@ -1107,6 +1107,10 @@ "type": "string", "description": "The timestamp of the genesis block.", "format": "date-time" + }, + "GenesisBlockPath": { + "type": "string", + "description": "The path of the genesis block." } } }, diff --git a/sdk/node/Libplanet.Node.Executable/appsettings.Development.json b/sdk/node/Libplanet.Node.Executable/appsettings.Development.json index 9a2658c28bb..8b59c49d98e 100644 --- a/sdk/node/Libplanet.Node.Executable/appsettings.Development.json +++ b/sdk/node/Libplanet.Node.Executable/appsettings.Development.json @@ -12,8 +12,7 @@ } }, "Swarm": { - "IsEnabled": true, - "AppProtocolVersion": "200210/AB2da648b9154F2cCcAFBD85e0Bc3d51f97330Fc/MEUCIQCBr..8VdITFe9nMTobl4akFid.s8G2zy2pBidAyRXSeAIgER77qX+eywjgyth6QYi7rQw5nK3KXO6cQ6ngUh.CyfU=/ZHU5OnRpbWVzdGFtcHUxMDoyMDI0LTA3LTMwZQ==" + "IsEnabled": true }, "Validator": { "IsEnabled": true diff --git a/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs b/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs index e6097fd520e..21909f0f234 100644 --- a/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs +++ b/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs @@ -19,6 +19,7 @@ public static ILibplanetNodeBuilder AddLibplanetNode( services.AddOptions() .Bind(configuration.GetSection(GenesisOptions.Position)); services.AddSingleton, GenesisOptionsConfigurator>(); + services.AddSingleton, GenesisOptionsValidator>(); services.AddOptions() .Bind(configuration.GetSection(StoreOptions.Position)); diff --git a/sdk/node/Libplanet.Node/Options/GenesisOptions.cs b/sdk/node/Libplanet.Node/Options/GenesisOptions.cs index 7fc4f487c21..691aadeb54c 100644 --- a/sdk/node/Libplanet.Node/Options/GenesisOptions.cs +++ b/sdk/node/Libplanet.Node/Options/GenesisOptions.cs @@ -10,13 +10,22 @@ public sealed class GenesisOptions : OptionsBase public const string Position = "Genesis"; [PrivateKey] - [Description("The key of the genesis block.")] + [Description( + $"The PrivateKey used to generate the genesis block. " + + $"This property cannot be used with {nameof(GenesisBlockPath)}.")] public string GenesisKey { get; set; } = string.Empty; [PublicKeyArray] - [Description("Public keys of the validators.")] + [Description( + $"Public keys of the validators. This property cannot be used with " + + $"{nameof(GenesisBlockPath)}.")] public string[] Validators { get; set; } = []; [Description("The timestamp of the genesis block.")] public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.MinValue; + + [Description( + $"The path of the genesis block, which can be a file path or a URI." + + $"This property cannot be used with {nameof(GenesisKey)}.")] + public string GenesisBlockPath { get; set; } = string.Empty; } diff --git a/sdk/node/Libplanet.Node/Options/GenesisOptionsConfigurator.cs b/sdk/node/Libplanet.Node/Options/GenesisOptionsConfigurator.cs index afbfda63e09..3ad62560ae1 100644 --- a/sdk/node/Libplanet.Node/Options/GenesisOptionsConfigurator.cs +++ b/sdk/node/Libplanet.Node/Options/GenesisOptionsConfigurator.cs @@ -11,21 +11,24 @@ internal sealed class GenesisOptionsConfigurator( { protected override void OnConfigure(GenesisOptions options) { - if (options.GenesisKey == string.Empty) + if (options.GenesisBlockPath == string.Empty) { - var privateKey = new PrivateKey(); - options.GenesisKey = ByteUtil.Hex(privateKey.ByteArray); - logger.LogWarning( - "Genesis key is not set. A new private key is generated:{PrivateKey}", - options.GenesisKey); - } + if (options.GenesisKey == string.Empty) + { + var privateKey = new PrivateKey(); + options.GenesisKey = ByteUtil.Hex(privateKey.ByteArray); + logger.LogWarning( + "Genesis key is not set. A new private key is generated:{PrivateKey}", + options.GenesisKey); + } - if (options.Validators.Length == 0) - { - var privateKey = PrivateKey.FromString(nodeOptions.Value.PrivateKey); - options.Validators = [privateKey.PublicKey.ToHex(compress: false)]; - logger.LogWarning( - "Validators are not set. Use the node's private key as a validator."); + if (options.Validators.Length == 0) + { + var privateKey = PrivateKey.FromString(nodeOptions.Value.PrivateKey); + options.Validators = [privateKey.PublicKey.ToHex(compress: false)]; + logger.LogWarning( + "Validators are not set. Use the node's private key as a validator."); + } } } } diff --git a/sdk/node/Libplanet.Node/Options/GenesisOptionsValidator.cs b/sdk/node/Libplanet.Node/Options/GenesisOptionsValidator.cs new file mode 100644 index 00000000000..fa68871d53d --- /dev/null +++ b/sdk/node/Libplanet.Node/Options/GenesisOptionsValidator.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.Options; + +namespace Libplanet.Node.Options; + +internal sealed class GenesisOptionsValidator : OptionsValidatorBase +{ + protected override void OnValidate(string? name, GenesisOptions options) + { + if (options.GenesisBlockPath != string.Empty) + { + if (options.GenesisKey != string.Empty) + { + var message = $"{nameof(options.GenesisKey)} cannot be used with " + + $"{nameof(options.GenesisBlockPath)}."; + throw new OptionsValidationException( + optionsName: name ?? string.Empty, + optionsType: typeof(GenesisOptions), + failureMessages: [message]); + } + + if (options.Validators.Length > 0) + { + var message = $"{nameof(options.Validators)} cannot be used with " + + $"{nameof(options.GenesisBlockPath)}."; + throw new OptionsValidationException( + optionsName: name ?? string.Empty, + optionsType: typeof(GenesisOptions), + failureMessages: [message]); + } + + if (!Uri.TryCreate(options.GenesisBlockPath, UriKind.Absolute, out _) + && !File.Exists(options.GenesisBlockPath)) + { + var message = $"{nameof(options.GenesisBlockPath)} must be a Uri or a existing " + + $"file path."; + throw new OptionsValidationException( + optionsName: name ?? string.Empty, + optionsType: typeof(GenesisOptions), + failureMessages: [message]); + } + } + } +} diff --git a/sdk/node/Libplanet.Node/Services/BlockChainService.cs b/sdk/node/Libplanet.Node/Services/BlockChainService.cs index ef2f85c797a..2f528bce875 100644 --- a/sdk/node/Libplanet.Node/Services/BlockChainService.cs +++ b/sdk/node/Libplanet.Node/Services/BlockChainService.cs @@ -1,7 +1,9 @@ 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; using Libplanet.Action.Loader; @@ -25,6 +27,7 @@ namespace Libplanet.Node.Services; internal sealed class BlockChainService : IBlockChainService, IActionRenderer { + private static readonly Codec _codec = new(); private readonly SynchronizationContext _synchronizationContext; private readonly ILogger _logger; private readonly BlockChain _blockChain; @@ -93,36 +96,14 @@ private static BlockChain CreateBlockChain( IRenderer[] renderers, IActionLoader[] actionLoaders) { - var genesisKey = PrivateKey.FromString(genesisOptions.GenesisKey); var (store, stateStore) = CreateStore(storeOptions); var actionLoader = new AggregateTypedActionLoader(actionLoaders); var actionEvaluator = new ActionEvaluator( policyActionsRegistry: new(), stateStore, actionLoader); - var validators = genesisOptions.Validators.Select(PublicKey.FromHex) - .Select(item => new Validator(item, new BigInteger(1000))) - .ToArray(); - var validatorSet = new ValidatorSet(validators: [.. validators]); - var nonce = 0L; - IAction[] actions = - [ - new Initialize( - validatorSet: validatorSet, - states: ImmutableDictionary.Create()), - ]; - var transaction = Transaction.Create( - nonce: nonce, - privateKey: genesisKey, - genesisHash: null, - actions: [.. actions.Select(item => item.PlainValue)], - timestamp: DateTimeOffset.MinValue); - var transactions = ImmutableList.Create(transaction); - var genesisBlock = BlockChain.ProposeGenesisBlock( - privateKey: genesisKey, - transactions: transactions, - timestamp: DateTimeOffset.MinValue); + var genesisBlock = CreateGenesisBlock(genesisOptions); var policy = new BlockPolicy( blockInterval: TimeSpan.FromSeconds(10), getMaxTransactionsPerBlock: _ => int.MaxValue, @@ -176,4 +157,69 @@ private static (IStore, IStateStore) CreateStore(StoreOptions storeOptions) return (store, stateStore); } } + + private static Block CreateGenesisBlock(GenesisOptions genesisOptions) + { + if (genesisOptions.GenesisKey != string.Empty) + { + var genesisKey = PrivateKey.FromString(genesisOptions.GenesisKey); + var validatorKeys = genesisOptions.Validators.Select(PublicKey.FromHex).ToArray(); + return CreateGenesisBlock(genesisKey, validatorKeys); + } + + if (genesisOptions.GenesisBlockPath != string.Empty) + { + return genesisOptions.GenesisBlockPath switch + { + { } path when Uri.TryCreate(path, UriKind.Absolute, out var uri) + => LoadGenesisBlockFromUrl(uri), + { } path => LoadGenesisBlock(path), + _ => throw new NotSupportedException(), + }; + } + + throw new UnreachableException("Genesis block path is not set."); + } + + private static Block CreateGenesisBlock(PrivateKey genesisKey, PublicKey[] validatorKeys) + { + var validators = validatorKeys + .Select(item => new Validator(item, new BigInteger(1000))) + .ToArray(); + var validatorSet = new ValidatorSet(validators: [.. validators]); + var nonce = 0L; + IAction[] actions = + [ + new Initialize( + validatorSet: validatorSet, + states: ImmutableDictionary.Create()), + ]; + + var transaction = Transaction.Create( + nonce: nonce, + privateKey: genesisKey, + genesisHash: null, + actions: [.. actions.Select(item => item.PlainValue)], + timestamp: DateTimeOffset.MinValue); + var transactions = ImmutableList.Create(transaction); + return BlockChain.ProposeGenesisBlock( + privateKey: genesisKey, + transactions: transactions, + timestamp: DateTimeOffset.MinValue); + } + + private static Block LoadGenesisBlock(string genesisBlockPath) + { + var rawBlock = File.ReadAllBytes(Path.GetFullPath(genesisBlockPath)); + var blockDict = (Dictionary)new Codec().Decode(rawBlock); + return BlockMarshaler.UnmarshalBlock(blockDict); + } + + private static Block LoadGenesisBlockFromUrl(Uri genesisBlockUri) + { + using var client = new HttpClient(); + var rawBlock = client.GetByteArrayAsync(genesisBlockUri).Result; + var blockDict = (Dictionary)_codec.Decode(rawBlock); + return BlockMarshaler.UnmarshalBlock(blockDict); + } }