Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add genesis config option for Node #4017

Merged
merged 5 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions sdk/node/Libplanet.Node.Tests/Services/BlockChainServiceTest.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
using Bencodex;
using Bencodex.Types;
using Libplanet.Common;
using Libplanet.Crypto;
using Libplanet.Node.Extensions;
using Libplanet.Node.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Libplanet.Node.Tests.Services;

Expand All @@ -16,4 +22,62 @@ public void Create_Test()

Assert.Equal(1, blockChain.Count);
}

[Fact]
public void Create_Using_Genesis_Configuration_Test()
{
var genesisKey = new PrivateKey();
string tempDirectory = Path.GetTempPath();
string tempFilePath = Path.Combine(tempDirectory, Guid.NewGuid().ToString() + ".json");
Address accountA = new("0000000000000000000000000000000000000000");
Address accountB = new("0000000000000000000000000000000000000001");
Address addressA = new("0000000000000000000000000000000000000000");
Address addressB = new("0000000000000000000000000000000000000001");
var codec = new Codec();

try
{
string jsonContent = $@"
{{
""{accountA}"": {{
""{addressA}"": ""{ByteUtil.Hex(codec.Encode((Text)"A"))}"",
""{addressB}"": ""{ByteUtil.Hex(codec.Encode((Integer)123))}""
}},
""{accountB}"": {{
""{addressA}"": ""{ByteUtil.Hex(codec.Encode((Text)"B"))}"",
""{addressB}"": ""{ByteUtil.Hex(codec.Encode((Integer)456))}""
}}
}}";
File.WriteAllText(tempFilePath, jsonContent);
var configDict = new Dictionary<string, string>
{
{ "Genesis:GenesisConfigurationPath", tempFilePath },
{ "Genesis:GenesisKey", ByteUtil.Hex(genesisKey.ToByteArray()) },
};

var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(configDict!)
.Build();

var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
services.AddLogging();
services.AddLibplanetNode(configuration);
var serviceProvider = services.BuildServiceProvider();
var blockChainService = serviceProvider.GetRequiredService<IBlockChainService>();
var blockChain = blockChainService.BlockChain;
var worldState = blockChain.GetNextWorldState()!;
Assert.Equal((Text)"A", worldState.GetAccountState(accountA).GetState(addressA));
Assert.Equal((Integer)123, worldState.GetAccountState(accountA).GetState(addressB));
Assert.Equal((Text)"B", worldState.GetAccountState(accountB).GetState(addressA));
Assert.Equal((Integer)456, worldState.GetAccountState(accountB).GetState(addressB));
}
finally
{
if (File.Exists(tempFilePath))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indent level can be reduced by one if the following code is used.
https://github.com/s2quake/libplanet-console/blob/main/src/common/LibplanetConsole.Common/IO/TempFile.cs
However, it's unfortunate that the code is not available in libplanet.node

{
File.Delete(tempFilePath);
}
}
}
}
1 change: 0 additions & 1 deletion sdk/node/Libplanet.Node/Libplanet.Node.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
<ProjectReference Include="..\..\..\src\Libplanet.Store\Libplanet.Store.csproj" />
<ProjectReference Include="..\..\..\src\Libplanet\Libplanet.csproj" />
<ProjectReference Include="..\..\..\src\Libplanet.Net\Libplanet.Net.csproj" />
<ProjectReference Include="..\..\..\..\..\suho\suho-lib9c\.Lib9c.Plugin.Shared\Lib9c.Plugin.Shared.csproj" />
</ItemGroup>

</Project>
11 changes: 9 additions & 2 deletions sdk/node/Libplanet.Node/Options/GenesisOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public sealed class GenesisOptions : OptionsBase<GenesisOptions>
[PrivateKey]
[Description(
$"The PrivateKey used to generate the genesis block. " +
$"This property cannot be used with {nameof(GenesisBlockPath)}.")]
$"This property cannot be used with {nameof(GenesisBlockPath)} and " +
$"{nameof(GenesisConfigurationPath)}.")]
public string GenesisKey { get; set; } = string.Empty;

[PublicKeyArray]
Expand All @@ -26,6 +27,12 @@ public sealed class GenesisOptions : OptionsBase<GenesisOptions>

[Description(
$"The path of the genesis block, which can be a file path or a URI." +
$"This property cannot be used with {nameof(GenesisKey)}.")]
$"This property cannot be used with {nameof(GenesisKey)} and " +
$"{nameof(GenesisConfigurationPath)}.")]
public string GenesisBlockPath { get; set; } = string.Empty;

[Description(
$"The path of the genesis configuration, which can be a file path or a URI." +
$"This property cannot be used with {nameof(GenesisKey)} and {nameof(GenesisBlockPath)}.")]
public string GenesisConfigurationPath { get; set; } = string.Empty;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are many ways to create Genesis blocks, it may be better to create a type variable rather than increase the properties.
If the properties increase, the complexity will increase because the exception handling code must be implemented.
For example,

enum GenesisBlockType
{
    Binary,
    BinaryUrl,
    Json,
    JsonUrl
}

I don't know what the best way is, but I think it's worth considering.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this suggestion should be dealt in one separated PR

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ internal sealed class GenesisOptionsConfigurator(
{
protected override void OnConfigure(GenesisOptions options)
{
if (options.GenesisBlockPath == string.Empty)
if (options.GenesisBlockPath == string.Empty &&
options.GenesisConfigurationPath == string.Empty)
{
if (options.GenesisKey == string.Empty)
{
Expand Down
44 changes: 44 additions & 0 deletions sdk/node/Libplanet.Node/Options/GenesisOptionsValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ protected override void OnValidate(string? name, GenesisOptions options)
failureMessages: [message]);
}

if (options.GenesisConfigurationPath != string.Empty)
{
var message = $"{nameof(options.GenesisConfigurationPath)} 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 " +
Expand All @@ -39,5 +49,39 @@ protected override void OnValidate(string? name, GenesisOptions options)
failureMessages: [message]);
}
}

if (options.GenesisConfigurationPath != string.Empty)
{
if (options.GenesisBlockPath != string.Empty)
{
var message = $"{nameof(options.GenesisBlockPath)} cannot be used with " +
$"{nameof(options.GenesisConfigurationPath)}.";
throw new OptionsValidationException(
optionsName: name ?? string.Empty,
optionsType: typeof(GenesisOptions),
failureMessages: [message]);
}

if (options.GenesisKey == string.Empty)
{
var message = $"{nameof(options.GenesisConfigurationPath)} must be used with " +
$"{nameof(options.GenesisKey)}.";
throw new OptionsValidationException(
optionsName: name ?? string.Empty,
optionsType: typeof(GenesisOptions),
failureMessages: [message]);
}

if (!Uri.TryCreate(options.GenesisConfigurationPath, UriKind.Absolute, out _)
&& !File.Exists(options.GenesisConfigurationPath))
{
var message = $"{nameof(options.GenesisConfigurationPath)} must be a Uri or a " +
$"existing file path.";
throw new OptionsValidationException(
optionsName: name ?? string.Empty,
optionsType: typeof(GenesisOptions),
failureMessages: [message]);
}
}
}
}
103 changes: 94 additions & 9 deletions sdk/node/Libplanet.Node/Services/BlockChainService.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Numerics;
using System.Text.Json;
using Bencodex;
using Bencodex.Types;
using Libplanet.Action;
using Libplanet.Action.Loader;
using Libplanet.Action.State;
using Libplanet.Action.Sys;
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.Store.Trie;
using Libplanet.Types.Blocks;
using Libplanet.Types.Consensus;
using Libplanet.Types.Tx;
Expand Down Expand Up @@ -51,7 +55,7 @@ private static BlockChain CreateBlockChain(
policyActionsRegistry: policyActionsRegistry,
stateStore,
actionLoader);
var genesisBlock = CreateGenesisBlock(genesisOptions);
var genesisBlock = CreateGenesisBlock(genesisOptions, stateStore);
var policy = new BlockPolicy(
policyActionsRegistry: policyActionsRegistry,
blockInterval: TimeSpan.FromSeconds(8),
Expand Down Expand Up @@ -88,15 +92,10 @@ private static BlockChain CreateBlockChain(
renderers: renderers);
}

private static Block CreateGenesisBlock(GenesisOptions genesisOptions)
private static Block CreateGenesisBlock(
GenesisOptions genesisOptions,
IStateStore stateStore)
{
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
Expand All @@ -108,6 +107,29 @@ private static Block CreateGenesisBlock(GenesisOptions genesisOptions)
};
}

if (genesisOptions.GenesisConfigurationPath != string.Empty)
{
var raw = genesisOptions.GenesisConfigurationPath switch
{
{ } path when Uri.TryCreate(path, UriKind.Absolute, out var uri)
=> LoadConfigurationFromUri(uri),
{ } path => LoadConfigurationFromFilePath(path),
_ => throw new NotSupportedException(),
};

return CreateGenesisBlockFromConfiguration(
PrivateKey.FromString(genesisOptions.GenesisKey),
raw,
stateStore);
}

if (genesisOptions.GenesisKey != string.Empty)
{
var genesisKey = PrivateKey.FromString(genesisOptions.GenesisKey);
var validatorKeys = genesisOptions.Validators.Select(PublicKey.FromHex).ToArray();
return CreateGenesisBlock(genesisKey, validatorKeys);
}

throw new UnreachableException("Genesis block path is not set.");
}

Expand Down Expand Up @@ -152,4 +174,67 @@ private static Block LoadGenesisBlockFromUrl(Uri genesisBlockUri)
var blockDict = (Dictionary)_codec.Decode(rawBlock);
return BlockMarshaler.UnmarshalBlock(blockDict);
}

private static byte[] LoadConfigurationFromFilePath(string configurationPath)
{
return File.ReadAllBytes(Path.GetFullPath(configurationPath));
}

private static byte[] LoadConfigurationFromUri(Uri configurationUri)
{
using var client = new HttpClient();
return configurationUri.IsFile
? LoadConfigurationFromFilePath(configurationUri.AbsolutePath)
: client.GetByteArrayAsync(configurationUri).Result;
}

private static Block CreateGenesisBlockFromConfiguration(
limebell marked this conversation as resolved.
Show resolved Hide resolved
PrivateKey genesisKey,
byte[] config,
IStateStore stateStore)
{
Dictionary<string, Dictionary<string, string>>? data =
JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(config);
if (data == null || data.Count == 0)
{
return BlockChain.ProposeGenesisBlock(
privateKey: genesisKey,
timestamp: DateTimeOffset.MinValue);
}

var nullTrie = stateStore.GetStateRoot(null);
nullTrie = nullTrie.SetMetadata(new TrieMetadata(BlockMetadata.WorldStateProtocolVersion));
IWorld world = new World(new WorldBaseState(nullTrie, stateStore));
var codec = new Codec();

foreach (var accountKv in data)
{
var key = new Address(accountKv.Key);
IAccount account = world.GetAccount(key);

foreach (var stateKv in accountKv.Value)
{
account = account.SetState(
new Address(stateKv.Key),
codec.Decode(ByteUtil.ParseHex(stateKv.Value)));
}

world = world.SetAccount(key, account);
}

var worldTrie = world.Trie;
foreach (var account in world.Delta.Accounts)
{
var accountTrie = stateStore.Commit(account.Value.Trie);
worldTrie = worldTrie.Set(
KeyConverters.ToStateKey(account.Key),
new Binary(accountTrie.Hash.ByteArray));
}

worldTrie = stateStore.Commit(worldTrie);
return BlockChain.ProposeGenesisBlock(
privateKey: genesisKey,
stateRootHash: worldTrie.Hash,
timestamp: DateTimeOffset.MinValue);
}
}
2 changes: 1 addition & 1 deletion src/Libplanet.Action/State/KeyConverters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Libplanet.Action.State
{
internal static class KeyConverters
public static class KeyConverters
{
// "___"
public static readonly KeyBytes ValidatorSetKey =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<ItemGroup>
<PackageReference Include="Cocona.Lite" Version="2.0.*" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="System.Text.Json" Version="7.0.0" />
<PackageReference Include="System.Text.Json" Version="9.0.*" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumping to the latest version is always welcome. At least for me. 😀

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vulnerability in 7.0 causes failure in build 😢

</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<ItemGroup>
<PackageReference Include="Cocona.Lite" Version="2.0.*" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Text.Json" Version="6.0.7" />
<PackageReference Include="System.Text.Json" Version="9.0.*" />
</ItemGroup>

<ItemGroup Condition="'$(SkipSonar)' != 'true'">
Expand Down
Loading