From 67390d93b5aef6e056519eff3cd8a17a3da35a27 Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Thu, 12 Sep 2024 17:57:50 +0900 Subject: [PATCH 1/8] Added direct commit of world to IStateStore --- src/Libplanet.Action/AssemblyInfo.cs | 3 +- src/Libplanet/Blockchain/BlockChain.cs | 16 +++ .../Blockchain/IStateStoreExtensions.cs | 105 ++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/Libplanet/Blockchain/IStateStoreExtensions.cs diff --git a/src/Libplanet.Action/AssemblyInfo.cs b/src/Libplanet.Action/AssemblyInfo.cs index 3b64ad1989..d32db2c362 100644 --- a/src/Libplanet.Action/AssemblyInfo.cs +++ b/src/Libplanet.Action/AssemblyInfo.cs @@ -1,6 +1,7 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Libplanet.Tests")] +[assembly: InternalsVisibleTo("Libplanet")] [assembly: InternalsVisibleTo("Libplanet.Action.Tests")] [assembly: InternalsVisibleTo("Libplanet.Explorer.Tests")] [assembly: InternalsVisibleTo("Libplanet.Mocks")] +[assembly: InternalsVisibleTo("Libplanet.Tests")] diff --git a/src/Libplanet/Blockchain/BlockChain.cs b/src/Libplanet/Blockchain/BlockChain.cs index 856403dd71..8b2eb81ab2 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; @@ -393,6 +401,14 @@ public static BlockChain Create( $"Given {nameof(store)} already has its canonical chain id set: {canonId}", nameof(store)); } + 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(); diff --git a/src/Libplanet/Blockchain/IStateStoreExtensions.cs b/src/Libplanet/Blockchain/IStateStoreExtensions.cs new file mode 100644 index 0000000000..f9692e811d --- /dev/null +++ b/src/Libplanet/Blockchain/IStateStoreExtensions.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Store; +using Libplanet.Store.Trie; +using Libplanet.Types.Blocks; + +namespace Libplanet.Blockchain +{ + /// + /// Convenient extension methods for . + /// + 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.ToDictionary( + outerPair => new Address(((Binary)outerPair.Key).ByteArray), + outerPair => ((Dictionary)outerPair.Value).ToDictionary( + 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, + Dictionary> data) + { + var stateRoot = stateStore.GetStateRoot(null); + stateRoot = stateRoot.SetMetadata( + new TrieMetadata(BlockMetadata.CurrentProtocolVersion)); + stateRoot = stateStore.Commit(stateRoot); + foreach ((var key, var value) in data) + { + stateRoot = stateRoot.Set( + KeyConverters.ToStateKey(key), + new Binary(stateStore.CommitAccount(value).ByteArray)); + } + + return stateStore.Commit(stateRoot).Hash; + } + + private static HashDigest CommitAccount( + this IStateStore stateStore, + Dictionary data) + { + var stateRoot = stateStore.GetStateRoot(null); + foreach ((var key, var value) in data) + { + stateRoot = stateRoot.Set( + KeyConverters.ToStateKey(key), + value); + } + + return stateStore.Commit(stateRoot).Hash; + } + } +} From 6fb2d7fa0b2d4b9bd6ca6e0aa74304b1cb403ab0 Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Thu, 12 Sep 2024 20:38:22 +0900 Subject: [PATCH 2/8] Removed Initialize in Fixture --- .../GeneratedBlockChainFixture.cs | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs b/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs index 79487a95b6..4ed1819d7c 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,15 @@ 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 = new Dictionary>(); + var validatorSet = new ValidatorSet( + PrivateKeys.Select(pk => new Validator(pk.PublicKey, 1)).ToList()); + initialWorld[ReservedAddresses.ValidatorSetAccount] = new Dictionary() + { + { ValidatorSetAccount.ValidatorSetAddress, validatorSet.Bencoded } + }; + var initialStaterootHash = stateStore.CommitWorld(initialWorld); + Block genesisBlock = BlockChain.ProposeGenesisBlock(stateRootHash: initialStaterootHash); Chain = BlockChain.Create( policy, new VolatileStagePolicy(), From 12da09bd7cac84ffa75a0670208a357d8fc25bbf Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Thu, 12 Sep 2024 21:25:41 +0900 Subject: [PATCH 3/8] Reorganized code; added helper methods --- src/Libplanet.Action/AssemblyInfo.cs | 1 - .../State/IStateStoreExtensions.cs | 89 ++++++++++++++- .../State/InitialStateExtensions.cs | 29 +++++ .../Blockchain/IStateStoreExtensions.cs | 105 ------------------ .../GeneratedBlockChainFixture.cs | 11 +- 5 files changed, 121 insertions(+), 114 deletions(-) create mode 100644 src/Libplanet.Action/State/InitialStateExtensions.cs delete mode 100644 src/Libplanet/Blockchain/IStateStoreExtensions.cs diff --git a/src/Libplanet.Action/AssemblyInfo.cs b/src/Libplanet.Action/AssemblyInfo.cs index d32db2c362..2cc1c4115b 100644 --- a/src/Libplanet.Action/AssemblyInfo.cs +++ b/src/Libplanet.Action/AssemblyInfo.cs @@ -1,6 +1,5 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Libplanet")] [assembly: InternalsVisibleTo("Libplanet.Action.Tests")] [assembly: InternalsVisibleTo("Libplanet.Explorer.Tests")] [assembly: InternalsVisibleTo("Libplanet.Mocks")] diff --git a/src/Libplanet.Action/State/IStateStoreExtensions.cs b/src/Libplanet.Action/State/IStateStoreExtensions.cs index c880279cb2..0934c88d7e 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 key, var value) in data) + { + stateRoot = stateRoot.Set( + KeyConverters.ToStateKey(key), + new Binary(stateStore.CommitAccount(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 key, var value) in data) + { + stateRoot = stateRoot.Set( + KeyConverters.ToStateKey(key), + value); + } + + return stateStore.Commit(stateRoot).Hash; + } } } diff --git a/src/Libplanet.Action/State/InitialStateExtensions.cs b/src/Libplanet.Action/State/InitialStateExtensions.cs new file mode 100644 index 0000000000..4cf8a6f02c --- /dev/null +++ b/src/Libplanet.Action/State/InitialStateExtensions.cs @@ -0,0 +1,29 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Types.Consensus; + +namespace Libplanet.Action.State +{ + public static class InitialStateExtensions + { + public static ImmutableDictionary> + AddLegacyStates( + this ImmutableDictionary> + initialState, + ImmutableDictionary legacyStates) => + initialState.Add( + ReservedAddresses.LegacyAccount, + legacyStates); + + public static ImmutableDictionary> + AddValidatorSet( + this ImmutableDictionary> + initialState, + ValidatorSet validatorSet) => + initialState.Add( + ReservedAddresses.ValidatorSetAccount, + ImmutableDictionary.Empty + .Add(ValidatorSetAccount.ValidatorSetAddress, validatorSet.Bencoded)); + } +} diff --git a/src/Libplanet/Blockchain/IStateStoreExtensions.cs b/src/Libplanet/Blockchain/IStateStoreExtensions.cs deleted file mode 100644 index f9692e811d..0000000000 --- a/src/Libplanet/Blockchain/IStateStoreExtensions.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using Bencodex.Types; -using Libplanet.Action.State; -using Libplanet.Common; -using Libplanet.Crypto; -using Libplanet.Store; -using Libplanet.Store.Trie; -using Libplanet.Types.Blocks; - -namespace Libplanet.Blockchain -{ - /// - /// Convenient extension methods for . - /// - 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.ToDictionary( - outerPair => new Address(((Binary)outerPair.Key).ByteArray), - outerPair => ((Dictionary)outerPair.Value).ToDictionary( - 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, - Dictionary> data) - { - var stateRoot = stateStore.GetStateRoot(null); - stateRoot = stateRoot.SetMetadata( - new TrieMetadata(BlockMetadata.CurrentProtocolVersion)); - stateRoot = stateStore.Commit(stateRoot); - foreach ((var key, var value) in data) - { - stateRoot = stateRoot.Set( - KeyConverters.ToStateKey(key), - new Binary(stateStore.CommitAccount(value).ByteArray)); - } - - return stateStore.Commit(stateRoot).Hash; - } - - private static HashDigest CommitAccount( - this IStateStore stateStore, - Dictionary data) - { - var stateRoot = stateStore.GetStateRoot(null); - foreach ((var key, var value) in data) - { - stateRoot = stateRoot.Set( - KeyConverters.ToStateKey(key), - value); - } - - return stateStore.Commit(stateRoot).Hash; - } - } -} diff --git a/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs b/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs index 4ed1819d7c..4539bf3a4a 100644 --- a/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs +++ b/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs @@ -77,13 +77,10 @@ public GeneratedBlockChainFixture( policy.PolicyActionsRegistry, stateStore, TypedActionLoader.Create(typeof(SimpleAction).Assembly, typeof(SimpleAction))); - var initialWorld = new Dictionary>(); - var validatorSet = new ValidatorSet( - PrivateKeys.Select(pk => new Validator(pk.PublicKey, 1)).ToList()); - initialWorld[ReservedAddresses.ValidatorSetAccount] = new Dictionary() - { - { ValidatorSetAccount.ValidatorSetAddress, validatorSet.Bencoded } - }; + var initialWorld = ImmutableDictionary>.Empty + .AddValidatorSet( + 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( From 8a49e3f127cbda602c6401a553221dc9f86f038b Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Thu, 12 Sep 2024 21:41:28 +0900 Subject: [PATCH 4/8] Partially removed Initialize in test --- .../Blockchain/BlockChainTest.cs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/test/Libplanet.Tests/Blockchain/BlockChainTest.cs b/test/Libplanet.Tests/Blockchain/BlockChainTest.cs index fdcc14c771..7170622abd 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 + .AddValidatorSet(TestUtils.ValidatorSet); + var genesisStateRootHash = stateStore.CommitWorld(initialState); + var genesis = BlockChain.ProposeGenesisBlock(stateRootHash: genesisStateRootHash); var chain = BlockChain.Create( policy, new VolatileStagePolicy(), From 1f16b9d912f93277c042103569132577eae2ed73 Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Fri, 13 Sep 2024 10:29:24 +0900 Subject: [PATCH 5/8] Added tests --- .../State/InitialStateExtensions.cs | 37 ++++++-- .../GeneratedBlockChainFixture.cs | 2 +- .../Action/IStateStoreExtensionsTest.cs | 95 +++++++++++++++++++ .../Blockchain/BlockChainTest.cs | 40 +++++++- 4 files changed, 163 insertions(+), 11 deletions(-) create mode 100644 test/Libplanet.Tests/Action/IStateStoreExtensionsTest.cs diff --git a/src/Libplanet.Action/State/InitialStateExtensions.cs b/src/Libplanet.Action/State/InitialStateExtensions.cs index 4cf8a6f02c..07fcb2c7ff 100644 --- a/src/Libplanet.Action/State/InitialStateExtensions.cs +++ b/src/Libplanet.Action/State/InitialStateExtensions.cs @@ -1,29 +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 InitialStateExtensions { public static ImmutableDictionary> - AddLegacyStates( + AddOrUpdateLegacyState( this ImmutableDictionary> initialState, ImmutableDictionary legacyStates) => - initialState.Add( - ReservedAddresses.LegacyAccount, - legacyStates); + initialState.ContainsKey(ReservedAddresses.LegacyAccount) + ? initialState + .Remove(ReservedAddresses.LegacyAccount) + .Add(ReservedAddresses.LegacyAccount, legacyStates) + : initialState.Add(ReservedAddresses.LegacyAccount, legacyStates); public static ImmutableDictionary> - AddValidatorSet( + AddOrUpdateValidatorSet( this ImmutableDictionary> initialState, ValidatorSet validatorSet) => - initialState.Add( - ReservedAddresses.ValidatorSetAccount, - ImmutableDictionary.Empty - .Add(ValidatorSetAccount.ValidatorSetAddress, validatorSet.Bencoded)); + initialState.ContainsKey(ReservedAddresses.ValidatorSetAccount) + ? initialState + .Remove(ReservedAddresses.ValidatorSetAccount) + .Add( + ReservedAddresses.ValidatorSetAccount, + ImmutableDictionary.Empty + .Add( + ValidatorSetAccount.ValidatorSetAddress, + validatorSet.Bencoded)) + : initialState + .Add( + ReservedAddresses.ValidatorSetAccount, + ImmutableDictionary.Empty + .Add( + ValidatorSetAccount.ValidatorSetAddress, + validatorSet.Bencoded)); } } diff --git a/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs b/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs index 4539bf3a4a..e8f9559205 100644 --- a/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs +++ b/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs @@ -78,7 +78,7 @@ public GeneratedBlockChainFixture( stateStore, TypedActionLoader.Create(typeof(SimpleAction).Assembly, typeof(SimpleAction))); var initialWorld = ImmutableDictionary>.Empty - .AddValidatorSet( + .AddOrUpdateValidatorSet( new ValidatorSet( PrivateKeys.Select(pk => new Validator(pk.PublicKey, 1)).ToList())); var initialStaterootHash = stateStore.CommitWorld(initialWorld); 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 7170622abd..2ac47a0c86 100644 --- a/test/Libplanet.Tests/Blockchain/BlockChainTest.cs +++ b/test/Libplanet.Tests/Blockchain/BlockChainTest.cs @@ -197,7 +197,7 @@ public void ProcessActions() actionLoader); var initialState = ImmutableDictionary>.Empty - .AddValidatorSet(TestUtils.ValidatorSet); + .AddOrUpdateValidatorSet(TestUtils.ValidatorSet); var genesisStateRootHash = stateStore.CommitWorld(initialState); var genesis = BlockChain.ProposeGenesisBlock(stateRootHash: genesisStateRootHash); var chain = BlockChain.Create( @@ -1141,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 From 9f17136ff15539252147b7c17e2bd2b61eccf49d Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Fri, 13 Sep 2024 11:19:58 +0900 Subject: [PATCH 6/8] Changelog --- CHANGES.md | 9 +++++++++ 1 file changed, 9 insertions(+) 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 From bed590f349c8905731d8fd8eb6519b36938239b9 Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Fri, 13 Sep 2024 16:18:31 +0900 Subject: [PATCH 7/8] Fixed Create() to allow compatibility for old blocks; Applied name change suggestion --- ...Extensions.cs => GroundStateExtensions.cs} | 18 +++++++-------- src/Libplanet/Blockchain/BlockChain.cs | 22 ++++++++++--------- 2 files changed, 21 insertions(+), 19 deletions(-) rename src/Libplanet.Action/State/{InitialStateExtensions.cs => GroundStateExtensions.cs} (79%) diff --git a/src/Libplanet.Action/State/InitialStateExtensions.cs b/src/Libplanet.Action/State/GroundStateExtensions.cs similarity index 79% rename from src/Libplanet.Action/State/InitialStateExtensions.cs rename to src/Libplanet.Action/State/GroundStateExtensions.cs index 07fcb2c7ff..ae42577fad 100644 --- a/src/Libplanet.Action/State/InitialStateExtensions.cs +++ b/src/Libplanet.Action/State/GroundStateExtensions.cs @@ -10,26 +10,26 @@ namespace Libplanet.Action.State /// A set of useful extension methods for making an initial state to commit to /// an . /// - public static class InitialStateExtensions + public static class GroundStateExtensions { public static ImmutableDictionary> AddOrUpdateLegacyState( this ImmutableDictionary> - initialState, + groundState, ImmutableDictionary legacyStates) => - initialState.ContainsKey(ReservedAddresses.LegacyAccount) - ? initialState + groundState.ContainsKey(ReservedAddresses.LegacyAccount) + ? groundState .Remove(ReservedAddresses.LegacyAccount) .Add(ReservedAddresses.LegacyAccount, legacyStates) - : initialState.Add(ReservedAddresses.LegacyAccount, legacyStates); + : groundState.Add(ReservedAddresses.LegacyAccount, legacyStates); public static ImmutableDictionary> AddOrUpdateValidatorSet( this ImmutableDictionary> - initialState, + groundState, ValidatorSet validatorSet) => - initialState.ContainsKey(ReservedAddresses.ValidatorSetAccount) - ? initialState + groundState.ContainsKey(ReservedAddresses.ValidatorSetAccount) + ? groundState .Remove(ReservedAddresses.ValidatorSetAccount) .Add( ReservedAddresses.ValidatorSetAccount, @@ -37,7 +37,7 @@ this ImmutableDictionary> .Add( ValidatorSetAccount.ValidatorSetAddress, validatorSet.Bencoded)) - : initialState + : groundState .Add( ReservedAddresses.ValidatorSetAccount, ImmutableDictionary.Empty diff --git a/src/Libplanet/Blockchain/BlockChain.cs b/src/Libplanet/Blockchain/BlockChain.cs index 8b2eb81ab2..4745e0033b 100644 --- a/src/Libplanet/Blockchain/BlockChain.cs +++ b/src/Libplanet/Blockchain/BlockChain.cs @@ -401,16 +401,6 @@ public static BlockChain Create( $"Given {nameof(store)} already has its canonical chain id set: {canonId}", nameof(store)); } - 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(); if (genesisBlock.ProtocolVersion < BlockMetadata.SlothProtocolVersion) { @@ -428,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); From 09ce0b00a5383e609410c159ff051e0d3e05ac60 Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Fri, 13 Sep 2024 16:24:46 +0900 Subject: [PATCH 8/8] Build fix --- src/Libplanet.Action/State/IStateStoreExtensions.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Libplanet.Action/State/IStateStoreExtensions.cs b/src/Libplanet.Action/State/IStateStoreExtensions.cs index 0934c88d7e..e26987ac70 100644 --- a/src/Libplanet.Action/State/IStateStoreExtensions.cs +++ b/src/Libplanet.Action/State/IStateStoreExtensions.cs @@ -75,11 +75,11 @@ public static HashDigest CommitWorld( stateRoot = stateRoot.SetMetadata( new TrieMetadata(BlockMetadata.CurrentProtocolVersion)); stateRoot = stateStore.Commit(stateRoot); - foreach ((var key, var value) in data) + foreach (var pair in data) { stateRoot = stateRoot.Set( - KeyConverters.ToStateKey(key), - new Binary(stateStore.CommitAccount(value).ByteArray)); + KeyConverters.ToStateKey(pair.Key), + new Binary(stateStore.CommitAccount(pair.Value).ByteArray)); } return stateStore.Commit(stateRoot).Hash; @@ -342,11 +342,11 @@ private static HashDigest CommitAccount( ImmutableDictionary data) { var stateRoot = stateStore.GetStateRoot(null); - foreach ((var key, var value) in data) + foreach (var pair in data) { stateRoot = stateRoot.Set( - KeyConverters.ToStateKey(key), - value); + KeyConverters.ToStateKey(pair.Key), + pair.Value); } return stateStore.Commit(stateRoot).Hash;