diff --git a/CHANGES.md b/CHANGES.md index cc1ee9cce7..0ac54436f6 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 3b64ad1989..2cc1c4115b 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 0000000000..ae42577fad --- /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 c880279cb2..e26987ac70 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 856403dd71..4745e0033b 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 79487a95b6..e8f9559205 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 0000000000..68594882fb --- /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 fdcc14c771..2ac47a0c86 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