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..0684a9ba13 100644 --- a/src/Plugins/StateService/StatePlugin.cs +++ b/src/Plugins/StateService/StatePlugin.cs @@ -102,10 +102,35 @@ void ICommittingHandler.Blockchain_Committing_Handler(NeoSystem system, Block bl IReadOnlyList applicationExecutedList) { if (system.Settings.Network != StateServiceSettings.Default.Network) return; - StateStore.Singleton.UpdateLocalStateRootSnapshot(block.Index, + + // 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) .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 +139,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