From 7ad79e47adee7a9dbc05d1e368faca47fe920081 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 5 Sep 2025 18:23:30 +0300 Subject: [PATCH 1/2] *: introduce StateRootInHeader Close #1526. Signed-off-by: Anna Shaleva --- src/Neo/NeoSystem.cs | 12 +++++ src/Neo/Network/P2P/Payloads/Block.cs | 5 ++ src/Neo/Network/P2P/Payloads/BlockVersion.cs | 28 ++++++++++ src/Neo/Network/P2P/Payloads/Header.cs | 29 +++++++++- src/Neo/SmartContract/Native/TrimmedBlock.cs | 15 ++++-- .../Consensus/ConsensusContext.MakePayload.cs | 4 +- .../DBFTPlugin/Consensus/ConsensusContext.cs | 9 +++- .../Consensus/ConsensusService.OnMessage.cs | 2 +- src/Plugins/DBFTPlugin/DBFTPlugin.csproj | 1 + src/Plugins/DBFTPlugin/DBFTPlugin.json | 5 +- .../DBFTPlugin/Messages/PrepareRequest.cs | 7 +++ src/Plugins/StateService/StatePlugin.cs | 16 +++++- .../StateService/Storage/StateStore.cs | 3 +- .../Network/P2P/Payloads/UT_Header.cs | 53 ++++++++++++++++++- 14 files changed, 176 insertions(+), 13 deletions(-) create mode 100644 src/Neo/Network/P2P/Payloads/BlockVersion.cs diff --git a/src/Neo/NeoSystem.cs b/src/Neo/NeoSystem.cs index 83782fcad8..bfb70cc5f1 100644 --- a/src/Neo/NeoSystem.cs +++ b/src/Neo/NeoSystem.cs @@ -164,6 +164,7 @@ public NeoSystem(ProtocolSettings settings, IStoreProvider storageProvider, stri { Header = new Header { + Version = settings.IsHardforkEnabled(Hardfork.HF_Faun, 0) ? (uint)BlockVersion.V1 : (uint)BlockVersion.V0, PrevHash = UInt256.Zero, MerkleRoot = UInt256.Zero, Timestamp = (new DateTime(2016, 7, 15, 15, 8, 21, DateTimeKind.Utc)).ToTimestampMS(), @@ -331,5 +332,16 @@ public bool ContainsConflictHash(UInt256 hash, IEnumerable signers) { return NativeContract.Ledger.ContainsConflictHash(StoreView, hash, signers, this.GetMaxTraceableBlocks()); } + + /// + /// Returns index of the latest block persisted to native Ledger contract. + /// + /// + /// Index of the latest persisted block. + /// + public uint CurrentIndex() + { + return NativeContract.Ledger.CurrentIndex(StoreView); + } } } diff --git a/src/Neo/Network/P2P/Payloads/Block.cs b/src/Neo/Network/P2P/Payloads/Block.cs index 07f1de57ae..4c58525983 100644 --- a/src/Neo/Network/P2P/Payloads/Block.cs +++ b/src/Neo/Network/P2P/Payloads/Block.cs @@ -80,6 +80,11 @@ public sealed class Block : IEquatable, IInventory /// public UInt160 NextConsensus => Header.NextConsensus; + /// + /// MPT root hash calculated after the previous block is persisted. + /// + public UInt256 PrevStateRoot => Header.PrevStateRoot; + /// /// The witness of the block. /// diff --git a/src/Neo/Network/P2P/Payloads/BlockVersion.cs b/src/Neo/Network/P2P/Payloads/BlockVersion.cs new file mode 100644 index 0000000000..bc129c5175 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/BlockVersion.cs @@ -0,0 +1,28 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// BlockVersion.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.Network.P2P.Payloads +{ + /// + /// Represents the block version. + /// + public enum BlockVersion : byte + { + /// + /// Initial block version. + /// + V0, + /// + /// Block version 1 which adds PrevStateRoot field to the header. + /// + V1, + } +} diff --git a/src/Neo/Network/P2P/Payloads/Header.cs b/src/Neo/Network/P2P/Payloads/Header.cs index a716638484..16a7e9480a 100644 --- a/src/Neo/Network/P2P/Payloads/Header.cs +++ b/src/Neo/Network/P2P/Payloads/Header.cs @@ -35,6 +35,7 @@ public sealed class Header : IEquatable
, IVerifiable private uint index; private byte primaryIndex; private UInt160 nextConsensus; + private UInt256 prevStateRoot; /// /// The witness of the block. @@ -113,6 +114,15 @@ public UInt160 NextConsensus set { nextConsensus = value; _hash = null; } } + /// + /// MPT root hash got after the previous block processing. + /// + public UInt256 PrevStateRoot + { + get => prevStateRoot; + set { prevStateRoot = value; _hash = null; } + } + private UInt256 _hash = null; /// @@ -137,6 +147,7 @@ public UInt256 Hash sizeof(uint) + // Index sizeof(byte) + // PrimaryIndex UInt160.Length + // NextConsensus + (version == (uint)BlockVersion.V0 ? 0 : UInt256.Length) + // PrevStateRoot (Witness is null ? 1 : 1 + Witness.Size); // Witness, cannot be null for valid header Witness[] IVerifiable.Witnesses @@ -166,7 +177,7 @@ void IVerifiable.DeserializeUnsigned(ref MemoryReader reader) { _hash = null; version = reader.ReadUInt32(); - if (version > 0) throw new FormatException($"`version`({version}) in Header must be 0"); + if (version > (uint)BlockVersion.V1) throw new FormatException($"`version`({version}) in Header must be 0 or 1"); prevHash = reader.ReadSerializable(); merkleRoot = reader.ReadSerializable(); timestamp = reader.ReadUInt64(); @@ -174,6 +185,8 @@ void IVerifiable.DeserializeUnsigned(ref MemoryReader reader) index = reader.ReadUInt32(); primaryIndex = reader.ReadByte(); nextConsensus = reader.ReadSerializable(); + if (version == (uint)BlockVersion.V1) + prevStateRoot = reader.ReadSerializable(); } public bool Equals(Header other) @@ -217,6 +230,8 @@ void IVerifiable.SerializeUnsigned(BinaryWriter writer) writer.Write(index); writer.Write(primaryIndex); writer.Write(nextConsensus); + if (version == (uint)BlockVersion.V1) + writer.Write(prevStateRoot); } /// @@ -238,6 +253,8 @@ public JObject ToJson(ProtocolSettings settings) json["primary"] = primaryIndex; json["nextconsensus"] = nextConsensus.ToAddress(settings.AddressVersion); json["witnesses"] = new JArray(Witness.ToJson()); + if (version == (uint)BlockVersion.V1) + json["previousstateroot"] = prevStateRoot.ToString(); return json; } @@ -245,6 +262,8 @@ internal bool Verify(ProtocolSettings settings, DataCache snapshot) { if (primaryIndex >= settings.ValidatorsCount) return false; + if (version != (uint)GetExpectedVersion(settings)) + return false; TrimmedBlock prev = NativeContract.Ledger.GetTrimmedBlock(snapshot, prevHash); if (prev is null) return false; if (prev.Index + 1 != index) return false; @@ -258,6 +277,8 @@ internal bool Verify(ProtocolSettings settings, DataCache snapshot, HeaderCache { Header prev = headerCache.Last; if (prev is null) return Verify(settings, snapshot); + if (version != (uint)GetExpectedVersion(settings)) + return false; if (primaryIndex >= settings.ValidatorsCount) return false; if (prev.Hash != prevHash) return false; @@ -266,6 +287,11 @@ internal bool Verify(ProtocolSettings settings, DataCache snapshot, HeaderCache return this.VerifyWitness(settings, snapshot, prev.nextConsensus, Witness, 3_00000000L, out _); } + private BlockVersion GetExpectedVersion(ProtocolSettings settings) + { + return settings.IsHardforkEnabled(Hardfork.HF_Faun, Index) ? BlockVersion.V1 : BlockVersion.V0; + } + public Header Clone() { return new Header() @@ -278,6 +304,7 @@ public Header Clone() Index = index, PrimaryIndex = primaryIndex, NextConsensus = nextConsensus, + PrevStateRoot = prevStateRoot, Witness = Witness?.Clone(), _hash = _hash }; diff --git a/src/Neo/SmartContract/Native/TrimmedBlock.cs b/src/Neo/SmartContract/Native/TrimmedBlock.cs index a7b4ffc847..b47d4dcd0c 100644 --- a/src/Neo/SmartContract/Native/TrimmedBlock.cs +++ b/src/Neo/SmartContract/Native/TrimmedBlock.cs @@ -15,6 +15,7 @@ using Neo.VM; using Neo.VM.Types; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using Array = Neo.VM.Types.Array; @@ -110,8 +111,7 @@ void IInteroperable.FromStackItem(StackItem stackItem) StackItem IInteroperable.ToStackItem(IReferenceCounter referenceCounter) { - return new Array(referenceCounter, - [ + var block = new List() { // Computed properties Header.Hash.ToArray(), @@ -125,9 +125,14 @@ StackItem IInteroperable.ToStackItem(IReferenceCounter referenceCounter) Header.PrimaryIndex, Header.NextConsensus.ToArray(), - // Block properties - Hashes.Length - ]); + }; + if (Header.Version == (uint)BlockVersion.V1) + block.Add(Header.PrevStateRoot.ToArray()); + + // Block properties + block.Add(Hashes.Length); + + return new Array(referenceCounter, block); } } } diff --git a/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.MakePayload.cs b/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.MakePayload.cs index 9849082a87..9762a5cab1 100644 --- a/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.MakePayload.cs +++ b/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.MakePayload.cs @@ -115,7 +115,8 @@ public ExtensiblePayload MakePrepareRequest() PrevHash = Block.PrevHash, Timestamp = Block.Timestamp, Nonce = Block.Nonce, - TransactionHashes = TransactionHashes + TransactionHashes = TransactionHashes, + PrevStateRoot = Block.PrevStateRoot, }); } @@ -141,6 +142,7 @@ public ExtensiblePayload MakeRecoveryMessage() Nonce = Block.Nonce, BlockIndex = Block.Index, ValidatorIndex = Block.PrimaryIndex, + PrevStateRoot = Block.PrevStateRoot, TransactionHashes = TransactionHashes }; } diff --git a/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.cs b/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.cs index d2019f01c8..dd8ca0b39f 100644 --- a/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.cs +++ b/src/Plugins/DBFTPlugin/Consensus/ConsensusContext.cs @@ -199,16 +199,19 @@ public void Reset(byte viewNumber) Snapshot?.Dispose(); Snapshot = neoSystem.GetSnapshotCache(); uint height = NativeContract.Ledger.CurrentIndex(Snapshot); + var isFaun = neoSystem.Settings.IsHardforkEnabled(Hardfork.HF_Faun, height + 1); Block = new Block { Header = new Header { + Version = isFaun ? (uint)BlockVersion.V1 : (uint)BlockVersion.V0, PrevHash = NativeContract.Ledger.CurrentHash(Snapshot), Index = height + 1, NextConsensus = Contract.GetBFTAddress( NeoToken.ShouldRefreshCommittee(height + 1, neoSystem.Settings.CommitteeMembersCount) ? NativeContract.NEO.ComputeNextBlockValidators(Snapshot, neoSystem.Settings) : - NativeContract.NEO.GetNextBlockValidators(Snapshot, neoSystem.Settings.ValidatorsCount)) + NativeContract.NEO.GetNextBlockValidators(Snapshot, neoSystem.Settings.ValidatorsCount)), + PrevStateRoot = isFaun ? StateService.StatePlugin.GetStateRootHash(height) : null } }; TimePerBlock = neoSystem.GetTimePerBlock(); @@ -294,6 +297,8 @@ public void Deserialize(ref MemoryReader reader) Block.Header.NextConsensus = reader.ReadSerializable(); if (Block.NextConsensus.Equals(UInt160.Zero)) Block.Header.NextConsensus = null; + if (Block.Version == (uint)BlockVersion.V1) + Block.Header.PrevStateRoot = reader.ReadSerializable(); ViewNumber = reader.ReadByte(); TransactionHashes = reader.ReadSerializableArray(ushort.MaxValue); Transaction[] transactions = reader.ReadSerializableArray(ushort.MaxValue); @@ -320,6 +325,8 @@ public void Serialize(BinaryWriter writer) writer.Write(Block.Nonce); writer.Write(Block.PrimaryIndex); writer.Write(Block.NextConsensus ?? UInt160.Zero); + if (Block.Version == (uint)BlockVersion.V1) + writer.Write(Block.PrevStateRoot); writer.Write(ViewNumber); writer.Write(TransactionHashes ?? Array.Empty()); writer.Write(Transactions?.Values.ToArray() ?? Array.Empty()); diff --git a/src/Plugins/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs b/src/Plugins/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs index 1db1c56383..fcf4a7a94d 100644 --- a/src/Plugins/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs +++ b/src/Plugins/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs @@ -79,7 +79,7 @@ private void OnPrepareRequestReceived(ExtensiblePayload payload, PrepareRequest { if (context.RequestSentOrReceived || context.NotAcceptingPayloadsDueToViewChanging) return; if (message.ValidatorIndex != context.Block.PrimaryIndex || message.ViewNumber != context.ViewNumber) return; - if (message.Version != context.Block.Version || message.PrevHash != context.Block.PrevHash) return; + if (message.Version != context.Block.Version || message.PrevHash != context.Block.PrevHash || message.PrevStateRoot != context.Block.PrevStateRoot) return; if (message.TransactionHashes.Length > neoSystem.Settings.MaxTransactionsPerBlock) return; Log($"{nameof(OnPrepareRequestReceived)}: height={message.BlockIndex} view={message.ViewNumber} index={message.ValidatorIndex} tx={message.TransactionHashes.Length}"); if (message.Timestamp <= context.PrevHeader.Timestamp || message.Timestamp > TimeProvider.Current.UtcNow.AddMilliseconds(8 * context.TimePerBlock.TotalMilliseconds).ToTimestampMS()) diff --git a/src/Plugins/DBFTPlugin/DBFTPlugin.csproj b/src/Plugins/DBFTPlugin/DBFTPlugin.csproj index 27dc2d2d70..bb4820d628 100644 --- a/src/Plugins/DBFTPlugin/DBFTPlugin.csproj +++ b/src/Plugins/DBFTPlugin/DBFTPlugin.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Plugins/DBFTPlugin/DBFTPlugin.json b/src/Plugins/DBFTPlugin/DBFTPlugin.json index 705b2b77cb..a27dc2962c 100644 --- a/src/Plugins/DBFTPlugin/DBFTPlugin.json +++ b/src/Plugins/DBFTPlugin/DBFTPlugin.json @@ -7,5 +7,8 @@ "MaxBlockSize": 2097152, "MaxBlockSystemFee": 150000000000, "UnhandledExceptionPolicy": "StopNode" - } + }, + "Dependency": [ + "StateService" + ] } diff --git a/src/Plugins/DBFTPlugin/Messages/PrepareRequest.cs b/src/Plugins/DBFTPlugin/Messages/PrepareRequest.cs index a3bb77cb76..6f24a55a8d 100644 --- a/src/Plugins/DBFTPlugin/Messages/PrepareRequest.cs +++ b/src/Plugins/DBFTPlugin/Messages/PrepareRequest.cs @@ -11,6 +11,7 @@ using Neo.Extensions; using Neo.IO; +using Neo.Network.P2P.Payloads; using Neo.Plugins.DBFTPlugin.Types; using System; using System.IO; @@ -24,6 +25,7 @@ public class PrepareRequest : ConsensusMessage public UInt256 PrevHash; public ulong Timestamp; public ulong Nonce; + public UInt256 PrevStateRoot; public UInt256[] TransactionHashes; public override int Size => base.Size @@ -31,6 +33,7 @@ public class PrepareRequest : ConsensusMessage + UInt256.Length //PrevHash + sizeof(ulong) //Timestamp + sizeof(ulong) // Nonce + + (Version == (uint)BlockVersion.V0 ? 0 : UInt256.Length) // PrevStateRoot + TransactionHashes.GetVarSize(); //TransactionHashes public PrepareRequest() : base(ConsensusMessageType.PrepareRequest) { } @@ -42,6 +45,8 @@ public override void Deserialize(ref MemoryReader reader) PrevHash = reader.ReadSerializable(); Timestamp = reader.ReadUInt64(); Nonce = reader.ReadUInt64(); + if (Version == (uint)BlockVersion.V1) + PrevStateRoot = reader.ReadSerializable(); TransactionHashes = reader.ReadSerializableArray(ushort.MaxValue); if (TransactionHashes.Distinct().Count() != TransactionHashes.Length) throw new FormatException(); @@ -60,6 +65,8 @@ public override void Serialize(BinaryWriter writer) writer.Write(PrevHash); writer.Write(Timestamp); writer.Write(Nonce); + if (Version == (uint)BlockVersion.V1) + writer.Write(PrevStateRoot); writer.Write(TransactionHashes); } } diff --git a/src/Plugins/StateService/StatePlugin.cs b/src/Plugins/StateService/StatePlugin.cs index 695f4afa30..3b06539ef4 100644 --- a/src/Plugins/StateService/StatePlugin.cs +++ b/src/Plugins/StateService/StatePlugin.cs @@ -102,10 +102,16 @@ void ICommittingHandler.Blockchain_Committing_Handler(NeoSystem system, Block bl IReadOnlyList applicationExecutedList) { if (system.Settings.Network != StateServiceSettings.Default.Network) return; - StateStore.Singleton.UpdateLocalStateRootSnapshot(block.Index, + var root = StateStore.Singleton.UpdateLocalStateRootSnapshot(block.Index, snapshot.GetChangeSet() .Where(p => p.Value.State != TrackState.None && p.Key.Id != NativeContract.Ledger.Id) .ToList()); + if (system.Settings.IsHardforkEnabled(Hardfork.HF_Faun, block.Index + 1)) + { + var nextH = system.HeaderCache[block.Index + 1]; + if (nextH is not null && root != nextH.PrevStateRoot) + throw new InvalidOperationException($"Stateroot mismatch at {block.Index}: expected {nextH.PrevStateRoot}, got {root}"); + } } void ICommittedHandler.Blockchain_Committed_Handler(NeoSystem system, Block block) @@ -114,6 +120,14 @@ void ICommittedHandler.Blockchain_Committed_Handler(NeoSystem system, Block bloc StateStore.Singleton.UpdateLocalStateRoot(block.Index); } + public static UInt256 GetStateRootHash(uint index) + { + using var snapshot = StateStore.Singleton.GetSnapshot(); + var root = snapshot.GetStateRoot(index); + if (root is null) return null; + return root.RootHash; + } + private void CheckNetwork() { var network = StateServiceSettings.Default.Network; diff --git a/src/Plugins/StateService/Storage/StateStore.cs b/src/Plugins/StateService/Storage/StateStore.cs index 80968468b6..0e5ad471c2 100644 --- a/src/Plugins/StateService/Storage/StateStore.cs +++ b/src/Plugins/StateService/Storage/StateStore.cs @@ -129,7 +129,7 @@ private bool OnNewStateRoot(StateRoot stateRoot) return true; } - public void UpdateLocalStateRootSnapshot(uint height, IEnumerable> changeSet) + public UInt256 UpdateLocalStateRootSnapshot(uint height, IEnumerable> changeSet) { _stateSnapshot?.Dispose(); _stateSnapshot = Singleton.GetSnapshot(); @@ -158,6 +158,7 @@ public void UpdateLocalStateRootSnapshot(uint height, IEnumerable Date: Fri, 5 Sep 2025 19:01:08 +0300 Subject: [PATCH 2/2] StateService: allow to enable StateService plugin from arbitraty height Do not require StateService plugin to be enabled from genesis block. Node can build MPT based on the current contract storage state from scratch at the arbitrary height. This feature is required to avoid chain resync from genesis for those nodes who don't have StateService plugin enabled. Signed-off-by: Anna Shaleva --- src/Plugins/StateService/StatePlugin.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Plugins/StateService/StatePlugin.cs b/src/Plugins/StateService/StatePlugin.cs index 3b06539ef4..0684a9ba13 100644 --- a/src/Plugins/StateService/StatePlugin.cs +++ b/src/Plugins/StateService/StatePlugin.cs @@ -102,6 +102,25 @@ void ICommittingHandler.Blockchain_Committing_Handler(NeoSystem system, Block bl IReadOnlyList applicationExecutedList) { if (system.Settings.Network != StateServiceSettings.Default.Network) return; + + // Rebuild MPT from scratch on node bootstrap if StateService plugin is not up-to-date wrt the current block height. + if (block.Index > 0) + { + using var stateSnapshot = StateStore.Singleton.GetSnapshot(); + var persistedHeight = block.Index - 1; + var stateRoot = stateSnapshot.GetStateRoot(persistedHeight); + if (stateRoot is null) + { + using var coreSnapshot = system.GetSnapshotCache(); + StateStore.Singleton.UpdateLocalStateRootSnapshot(persistedHeight, + coreSnapshot.Find(SeekDirection.Forward) + .Where(si => si.Key.Id != NativeContract.Ledger.Id) + .Select(si => new KeyValuePair(si.Key, new DataCache.Trackable(si.Value, TrackState.Added))) + .ToList()); + StateStore.Singleton.UpdateLocalStateRoot(persistedHeight); + } + } + var root = StateStore.Singleton.UpdateLocalStateRootSnapshot(block.Index, snapshot.GetChangeSet() .Where(p => p.Value.State != TrackState.None && p.Key.Id != NativeContract.Ledger.Id)