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 basic tools for direct initial world commit #3952

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
9 changes: 9 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ To be released.

### Added APIs

- (Libplanet.Action) Added `InitialStateExtensions` static class with
extension methods for `Dictionary<Address, Dictionary<Address, IValue>>`s.
[[#3952]]

### Behavioral changes

- Changed `BlockChain.FindBranchPoint()` to only check for the first
Expand All @@ -49,6 +53,10 @@ To be released.
- (Libplanet.Net) Changed to no longer report `BlockHashDownloadState`
and `BlockDownloadState` during preloading. It is strongly advised
not to rely on these to track the progress of preloading. [[#3943]]
- Changed `BlockChain()` to throw an `ArgumentException` if it cannot find
the state root for its `Tip` in the `IStateStore`. [[#3952]]
- Changed `BlockChain.Create()` to throw an `ArgumentException` if it
cannot find the state root for the genesis block provided. [[#3952]]

### Bug fixes

Expand All @@ -66,6 +74,7 @@ To be released.
[#3948]: https://github.com/planetarium/libplanet/pull/3948
[#3949]: https://github.com/planetarium/libplanet/pull/3949
[#3950]: https://github.com/planetarium/libplanet/pull/3950
[#3952]: https://github.com/planetarium/libplanet/pull/3952


Version 5.2.2
Expand Down
2 changes: 1 addition & 1 deletion src/Libplanet.Action/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Libplanet.Tests")]
[assembly: InternalsVisibleTo("Libplanet.Action.Tests")]
[assembly: InternalsVisibleTo("Libplanet.Explorer.Tests")]
[assembly: InternalsVisibleTo("Libplanet.Mocks")]
[assembly: InternalsVisibleTo("Libplanet.Tests")]
48 changes: 48 additions & 0 deletions src/Libplanet.Action/State/GroundStateExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Collections.Immutable;
using Bencodex.Types;
using Libplanet.Crypto;
using Libplanet.Store;
using Libplanet.Types.Consensus;

namespace Libplanet.Action.State
{
/// <summary>
/// A set of useful extension methods for making an initial state to commit to
/// an <see cref="IStateStore"/>.
/// </summary>
public static class GroundStateExtensions
{
public static ImmutableDictionary<Address, ImmutableDictionary<Address, IValue>>
AddOrUpdateLegacyState(
this ImmutableDictionary<Address, ImmutableDictionary<Address, IValue>>
groundState,
ImmutableDictionary<Address, IValue> legacyStates) =>
groundState.ContainsKey(ReservedAddresses.LegacyAccount)
? groundState
.Remove(ReservedAddresses.LegacyAccount)
.Add(ReservedAddresses.LegacyAccount, legacyStates)
: groundState.Add(ReservedAddresses.LegacyAccount, legacyStates);

public static ImmutableDictionary<Address, ImmutableDictionary<Address, IValue>>
AddOrUpdateValidatorSet(
this ImmutableDictionary<Address, ImmutableDictionary<Address, IValue>>
groundState,
ValidatorSet validatorSet) =>
groundState.ContainsKey(ReservedAddresses.ValidatorSetAccount)
? groundState
.Remove(ReservedAddresses.ValidatorSetAccount)
.Add(
ReservedAddresses.ValidatorSetAccount,
ImmutableDictionary<Address, IValue>.Empty
.Add(
ValidatorSetAccount.ValidatorSetAddress,
validatorSet.Bencoded))
: groundState
.Add(
ReservedAddresses.ValidatorSetAccount,
ImmutableDictionary<Address, IValue>.Empty
.Add(
ValidatorSetAccount.ValidatorSetAddress,
validatorSet.Bencoded));
}
}
89 changes: 88 additions & 1 deletion src/Libplanet.Action/State/IStateStoreExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,90 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Bencodex.Types;
using Libplanet.Common;
using Libplanet.Crypto;
using Libplanet.Store;
using Libplanet.Store.Trie;
using Libplanet.Types.Blocks;

namespace Libplanet.Action.State
{
internal static class IStateStoreExtensions
public static class IStateStoreExtensions
{
/// <summary>
/// Commits <paramref name="data"/> representing a world state directly
/// to <see cref="IStateStore"/> and returns its state root hash.
/// The world state created is set to <see cref="BlockMetadata.CurrentProtocolVersion"/>.
/// </summary>
/// <param name="stateStore">The <see cref="IStateStore"/> to commit to.</param>
/// <param name="data">The data representing a world state to commit.</param>
/// <returns>The state root hash of the <paramref name="data"/> committed.</returns>
/// <exception cref="ArgumentException">Thrown if given <paramref name="data"/>
/// is not in the right format.
/// <list type="bullet">
/// <item><description>
/// Every key in <paramref name="data"/> must be a <see cref="Binary"/> of length
/// <see cref="Address.Size"/>.
/// </description></item>
/// <item><description>
/// Every value in <paramref name="data"/> must be a <see cref="Dictionary"/> with
/// each key in the <see cref="Dictionary"/> being a <see cref="Binary"/> of length
/// <see cref="Address.Size"/>.
/// </description></item>
/// </list>
/// </exception>
public static HashDigest<SHA256> CommitWorld(
this IStateStore stateStore,
Dictionary data)
{
try
{
var dictionary = data.ToImmutableDictionary(
outerPair => new Address(((Binary)outerPair.Key).ByteArray),
outerPair => ((Dictionary)outerPair.Value).ToImmutableDictionary(
innerPair => new Address(((Binary)innerPair.Key).ByteArray),
innerPair => innerPair.Value));
return stateStore.CommitWorld(dictionary);
}
catch (Exception e)
{
throw new ArgumentException(
$"Could not convert {nameof(data)} to a proper format",
nameof(data),
e);
}
}

/// <summary>
/// Commits <paramref name="data"/> representing a world state directly
/// to <see cref="IStateStore"/> and returns its state root hash.
/// The world state created is set to <see cref="BlockMetadata.CurrentProtocolVersion"/>.
/// </summary>
/// <param name="stateStore">The <see cref="IStateStore"/> to commit to.</param>
/// <param name="data">The data representing a world state to commit.</param>
/// <returns>The state root hash of the <paramref name="data"/> committed.</returns>
public static HashDigest<SHA256> CommitWorld(
this IStateStore stateStore,
ImmutableDictionary<Address, ImmutableDictionary<Address, IValue>> data)
{
var stateRoot = stateStore.GetStateRoot(null);
stateRoot = stateRoot.SetMetadata(
new TrieMetadata(BlockMetadata.CurrentProtocolVersion));
stateRoot = stateStore.Commit(stateRoot);
foreach (var pair in data)
{
stateRoot = stateRoot.Set(
KeyConverters.ToStateKey(pair.Key),
new Binary(stateStore.CommitAccount(pair.Value).ByteArray));
}

return stateStore.Commit(stateRoot).Hash;
}

/// <summary>
/// Retrieves the <see cref="IWorld"/> associated with
/// given <paramref name="stateRootHash"/>.
Expand Down Expand Up @@ -264,5 +336,20 @@ internal static IWorld MigrateWorld(

return world;
}

private static HashDigest<SHA256> CommitAccount(
this IStateStore stateStore,
ImmutableDictionary<Address, IValue> data)
{
var stateRoot = stateStore.GetStateRoot(null);
foreach (var pair in data)
{
stateRoot = stateRoot.Set(
KeyConverters.ToStateKey(pair.Key),
pair.Value);
}

return stateStore.Commit(stateRoot).Hash;
}
}
}
22 changes: 20 additions & 2 deletions src/Libplanet/Blockchain/BlockChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,14 @@ private BlockChain(
);
}

if (!StateStore.GetStateRoot(Tip.StateRootHash).Recorded)
{
throw new ArgumentException(
$"Given {nameof(stateStore)} does not contain the latest state " +
$"corresponding to state root hash {Tip.StateRootHash}",
nameof(stateStore));
}

if (Tip.ProtocolVersion < BlockMetadata.SlothProtocolVersion)
{
_nextStateRootHash = Tip.StateRootHash;
Expand Down Expand Up @@ -394,8 +402,6 @@ public static BlockChain Create(
nameof(store));
}

var id = Guid.NewGuid();

if (genesisBlock.ProtocolVersion < BlockMetadata.SlothProtocolVersion)
{
var preEval = new PreEvaluationBlock(
Expand All @@ -412,7 +418,19 @@ public static BlockChain Create(
computedStateRootHash);
}
}
else
{
if (!stateStore.GetStateRoot(genesisBlock.StateRootHash).Recorded)
{
throw new ArgumentException(
$"Given {nameof(stateStore)} does not contain the state root " +
$"corresponding to the state root hash of {nameof(genesisBlock)} " +
$"{genesisBlock.StateRootHash}",
nameof(stateStore));
}
}

var id = Guid.NewGuid();
ValidateGenesis(genesisBlock);
var nonceDeltas = ValidateGenesisNonces(genesisBlock);

Expand Down
25 changes: 8 additions & 17 deletions test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Numerics;
Expand All @@ -16,6 +17,7 @@
using Libplanet.Types.Tx;
using Libplanet.Store;
using Libplanet.Store.Trie;
using Libplanet.Action.State;

namespace Libplanet.Explorer.Tests;

Expand Down Expand Up @@ -75,23 +77,12 @@ public GeneratedBlockChainFixture(
policy.PolicyActionsRegistry,
stateStore,
TypedActionLoader.Create(typeof(SimpleAction).Assembly, typeof(SimpleAction)));
Block genesisBlock = BlockChain.ProposeGenesisBlock(
transactions: PrivateKeys
.OrderBy(pk => pk.Address.ToHex())
.Select(
(pk, i) => Transaction.Create(
nonce: i,
privateKey: privateKey,
genesisHash: null,
actions: new IAction[]
{
new Initialize(
new ValidatorSet(
ImmutableList<Validator>.Empty.Add(
new Validator(pk.PublicKey, 1)).ToList()),
ImmutableDictionary.Create<Address, IValue>())
}.ToPlainValues()))
.ToImmutableList());
var initialWorld = ImmutableDictionary<Address, ImmutableDictionary<Address, IValue>>.Empty
.AddOrUpdateValidatorSet(
new ValidatorSet(
PrivateKeys.Select(pk => new Validator(pk.PublicKey, 1)).ToList()));
var initialStaterootHash = stateStore.CommitWorld(initialWorld);
Block genesisBlock = BlockChain.ProposeGenesisBlock(stateRootHash: initialStaterootHash);
Chain = BlockChain.Create(
policy,
new VolatileStagePolicy(),
Expand Down
95 changes: 95 additions & 0 deletions test/Libplanet.Tests/Action/IStateStoreExtensionsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Bencodex.Types;
using Libplanet.Action.State;
using Libplanet.Crypto;
using Libplanet.Store;
using Libplanet.Store.Trie;
using Libplanet.Types.Blocks;
using Libplanet.Types.Consensus;
using Xunit;

namespace Libplanet.Tests.Action
{
public class IStateStoreExtensionsTest
{
[Fact]
public void EmptyCommitHasSideEffect()
{
IStateStore stateStore = new TrieStateStore(new MemoryKeyValueStore());
Dictionary data = Dictionary.Empty;

var hash = stateStore.CommitWorld(data);
Assert.NotEqual(hash, MerkleTrie.EmptyRootHash);
var trie = stateStore.GetStateRoot(hash);
Assert.Equal(
BlockMetadata.CurrentProtocolVersion,
Assert.IsType<TrieMetadata>(trie.GetMetadata()).Version);
}

[Fact]
public void CannotCommitInvalidData()
{
Random random = new Random();
Binary GetRandomBinary(int size = Address.Size)
{
byte[] buffer = new byte[size];
random.NextBytes(buffer);
return new Binary(buffer);
}

IStateStore stateStore = new TrieStateStore(new MemoryKeyValueStore());
Dictionary data;

data = Dictionary.Empty.Add("Invalid", "key type");
Assert.Throws<ArgumentException>(() => stateStore.CommitWorld(data));

data = Dictionary.Empty.Add(GetRandomBinary(), "Invalid format");
Assert.Throws<ArgumentException>(() => stateStore.CommitWorld(data));

data = Dictionary.Empty
.Add(
GetRandomBinary(8),
Dictionary.Empty
.Add(
GetRandomBinary(16),
"Invalid key length"));
Assert.Throws<ArgumentException>(() => stateStore.CommitWorld(data));

data = Dictionary.Empty
.Add(
GetRandomBinary(),
Dictionary.Empty
.Add(
GetRandomBinary(),
"Valid"));
}

[Fact]
public void InitialStateHelper()
{
IStateStore stateStore = new TrieStateStore(new MemoryKeyValueStore());
var data = ImmutableDictionary<Address, ImmutableDictionary<Address, IValue>>.Empty;
var legacyAddress = new PrivateKey().Address;
var legacyValue = new Text("Legacy value");
var legacyStates = ImmutableDictionary<Address, IValue>.Empty
.Add(legacyAddress, legacyValue);
var validatorKey = new PrivateKey();
var validatorPower = new Integer(123);
var validatorSet = new ValidatorSet(
new List<Validator>() { new Validator(validatorKey.PublicKey, validatorPower) });
data = data.AddOrUpdateLegacyState(legacyStates);
data = data.AddOrUpdateValidatorSet(validatorSet);

var hash = stateStore.CommitWorld(data);
var world = stateStore.GetWorld(hash);
Assert.Equal(
legacyValue,
world.GetAccount(ReservedAddresses.LegacyAccount).GetState(legacyAddress));
Assert.Equal(
validatorSet,
world.GetValidatorSet());
}
}
}
Loading
Loading