diff --git a/CHANGES.md b/CHANGES.md
index cc1ee9cce74..0ac54436f6e 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -39,6 +39,10 @@ To be released.
### Added APIs
+ - (Libplanet.Action) Added `InitialStateExtensions` static class with
+ extension methods for `Dictionary
>`s.
+ [[#3952]]
+
### Behavioral changes
- Changed `BlockChain.FindBranchPoint()` to only check for the first
@@ -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
@@ -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
diff --git a/src/Libplanet.Action/AssemblyInfo.cs b/src/Libplanet.Action/AssemblyInfo.cs
index 3b64ad1989d..2cc1c4115b8 100644
--- a/src/Libplanet.Action/AssemblyInfo.cs
+++ b/src/Libplanet.Action/AssemblyInfo.cs
@@ -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")]
diff --git a/src/Libplanet.Action/State/GroundStateExtensions.cs b/src/Libplanet.Action/State/GroundStateExtensions.cs
new file mode 100644
index 00000000000..ae42577fadc
--- /dev/null
+++ b/src/Libplanet.Action/State/GroundStateExtensions.cs
@@ -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
+{
+ ///
+ /// A set of useful extension methods for making an initial state to commit to
+ /// an .
+ ///
+ public static class GroundStateExtensions
+ {
+ public static ImmutableDictionary>
+ AddOrUpdateLegacyState(
+ this ImmutableDictionary>
+ groundState,
+ ImmutableDictionary legacyStates) =>
+ groundState.ContainsKey(ReservedAddresses.LegacyAccount)
+ ? groundState
+ .Remove(ReservedAddresses.LegacyAccount)
+ .Add(ReservedAddresses.LegacyAccount, legacyStates)
+ : groundState.Add(ReservedAddresses.LegacyAccount, legacyStates);
+
+ public static ImmutableDictionary>
+ AddOrUpdateValidatorSet(
+ this ImmutableDictionary>
+ groundState,
+ ValidatorSet validatorSet) =>
+ groundState.ContainsKey(ReservedAddresses.ValidatorSetAccount)
+ ? groundState
+ .Remove(ReservedAddresses.ValidatorSetAccount)
+ .Add(
+ ReservedAddresses.ValidatorSetAccount,
+ ImmutableDictionary.Empty
+ .Add(
+ ValidatorSetAccount.ValidatorSetAddress,
+ validatorSet.Bencoded))
+ : groundState
+ .Add(
+ ReservedAddresses.ValidatorSetAccount,
+ ImmutableDictionary.Empty
+ .Add(
+ ValidatorSetAccount.ValidatorSetAddress,
+ validatorSet.Bencoded));
+ }
+}
diff --git a/src/Libplanet.Action/State/IStateStoreExtensions.cs b/src/Libplanet.Action/State/IStateStoreExtensions.cs
index c880279cb22..e26987ac70c 100644
--- a/src/Libplanet.Action/State/IStateStoreExtensions.cs
+++ b/src/Libplanet.Action/State/IStateStoreExtensions.cs
@@ -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
{
+ ///
+ /// Commits representing a world state directly
+ /// to and returns its state root hash.
+ /// The world state created is set to .
+ ///
+ /// The to commit to.
+ /// The data representing a world state to commit.
+ /// The state root hash of the committed.
+ /// Thrown if given
+ /// is not in the right format.
+ ///
+ /// -
+ /// Every key in must be a of length
+ /// .
+ ///
+ /// -
+ /// Every value in must be a with
+ /// each key in the being a of length
+ /// .
+ ///
+ ///
+ ///
+ public static HashDigest 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);
+ }
+ }
+
+ ///
+ /// Commits representing a world state directly
+ /// to and returns its state root hash.
+ /// The world state created is set to .
+ ///
+ /// The to commit to.
+ /// The data representing a world state to commit.
+ /// The state root hash of the committed.
+ public static HashDigest CommitWorld(
+ this IStateStore stateStore,
+ ImmutableDictionary> 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;
+ }
+
///
/// Retrieves the associated with
/// given .
@@ -264,5 +336,20 @@ internal static IWorld MigrateWorld(
return world;
}
+
+ private static HashDigest CommitAccount(
+ this IStateStore stateStore,
+ ImmutableDictionary 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;
+ }
}
}
diff --git a/src/Libplanet/Blockchain/BlockChain.cs b/src/Libplanet/Blockchain/BlockChain.cs
index 856403dd71e..4745e0033bf 100644
--- a/src/Libplanet/Blockchain/BlockChain.cs
+++ b/src/Libplanet/Blockchain/BlockChain.cs
@@ -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;
@@ -394,8 +402,6 @@ public static BlockChain Create(
nameof(store));
}
- var id = Guid.NewGuid();
-
if (genesisBlock.ProtocolVersion < BlockMetadata.SlothProtocolVersion)
{
var preEval = new PreEvaluationBlock(
@@ -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);
diff --git a/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs b/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs
index 79487a95b6d..e8f9559205b 100644
--- a/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs
+++ b/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Numerics;
@@ -16,6 +17,7 @@
using Libplanet.Types.Tx;
using Libplanet.Store;
using Libplanet.Store.Trie;
+using Libplanet.Action.State;
namespace Libplanet.Explorer.Tests;
@@ -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.Empty.Add(
- new Validator(pk.PublicKey, 1)).ToList()),
- ImmutableDictionary.Create())
- }.ToPlainValues()))
- .ToImmutableList());
+ var initialWorld = ImmutableDictionary>.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(),
diff --git a/test/Libplanet.Tests/Action/IStateStoreExtensionsTest.cs b/test/Libplanet.Tests/Action/IStateStoreExtensionsTest.cs
new file mode 100644
index 00000000000..68594882fb5
--- /dev/null
+++ b/test/Libplanet.Tests/Action/IStateStoreExtensionsTest.cs
@@ -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(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(() => stateStore.CommitWorld(data));
+
+ data = Dictionary.Empty.Add(GetRandomBinary(), "Invalid format");
+ Assert.Throws(() => stateStore.CommitWorld(data));
+
+ data = Dictionary.Empty
+ .Add(
+ GetRandomBinary(8),
+ Dictionary.Empty
+ .Add(
+ GetRandomBinary(16),
+ "Invalid key length"));
+ Assert.Throws(() => 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>.Empty;
+ var legacyAddress = new PrivateKey().Address;
+ var legacyValue = new Text("Legacy value");
+ var legacyStates = ImmutableDictionary.Empty
+ .Add(legacyAddress, legacyValue);
+ var validatorKey = new PrivateKey();
+ var validatorPower = new Integer(123);
+ var validatorSet = new ValidatorSet(
+ new List() { 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());
+ }
+ }
+}
diff --git a/test/Libplanet.Tests/Blockchain/BlockChainTest.cs b/test/Libplanet.Tests/Blockchain/BlockChainTest.cs
index fdcc14c771f..2ac47a0c86d 100644
--- a/test/Libplanet.Tests/Blockchain/BlockChainTest.cs
+++ b/test/Libplanet.Tests/Blockchain/BlockChainTest.cs
@@ -195,22 +195,11 @@ public void ProcessActions()
policy.PolicyActionsRegistry,
stateStore,
actionLoader);
- var nonce = 0;
- var txs = TestUtils.ValidatorSet.Validators
- .Select(validator => Transaction.Create(
- nonce++,
- GenesisProposer,
- null,
- actions: new IAction[]
- {
- new Initialize(
- validatorSet: TestUtils.ValidatorSet,
- states: ImmutableDictionary.Create()),
- }.ToPlainValues(),
- timestamp: DateTimeOffset.UtcNow))
- .OrderBy(tx => tx.Id)
- .ToImmutableList();
- var genesis = BlockChain.ProposeGenesisBlock(transactions: txs);
+ var initialState =
+ ImmutableDictionary>.Empty
+ .AddOrUpdateValidatorSet(TestUtils.ValidatorSet);
+ var genesisStateRootHash = stateStore.CommitWorld(initialState);
+ var genesis = BlockChain.ProposeGenesisBlock(stateRootHash: genesisStateRootHash);
var chain = BlockChain.Create(
policy,
new VolatileStagePolicy(),
@@ -1152,6 +1141,44 @@ public void BlockActionWithMultipleAddress()
);
}
+ [Fact]
+ public void CannotCreateBlockChainWithoutTipStateRootHash()
+ {
+ var policy = new NullBlockPolicy();
+ var data = ImmutableDictionary>.Empty;
+ var fx = GetStoreFixture();
+
+ // NOTE: Even though data is empty, trie metadata gets recorded.
+ var tempStateStore = new TrieStateStore(new MemoryKeyValueStore());
+ var genesisStateRootHash = tempStateStore.CommitWorld(data);
+ Assert.NotEqual(genesisStateRootHash, MerkleTrie.EmptyRootHash);
+
+ Block genesis = BlockChain.ProposeGenesisBlock(stateRootHash: genesisStateRootHash);
+ var actionLoader = TypedActionLoader.Create(
+ typeof(BaseAction).Assembly, typeof(BaseAction));
+ var actionEvaluator = new ActionEvaluator(
+ policy.PolicyActionsRegistry,
+ fx.StateStore,
+ actionLoader);
+ Assert.Throws(() => BlockChain.Create(
+ policy,
+ new VolatileStagePolicy(),
+ fx.Store,
+ fx.StateStore,
+ genesis,
+ actionEvaluator));
+
+ // Works fine once committed to target store.
+ fx.StateStore.CommitWorld(data);
+ BlockChain.Create(
+ policy,
+ new VolatileStagePolicy(),
+ fx.Store,
+ fx.StateStore,
+ genesis,
+ actionEvaluator);
+ }
+
///
/// Builds a fixture that has incomplete states for blocks other
/// than the tip, to test GetState() method's