Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/Neo/NeoSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -331,5 +332,16 @@ public bool ContainsConflictHash(UInt256 hash, IEnumerable<UInt160> signers)
{
return NativeContract.Ledger.ContainsConflictHash(StoreView, hash, signers, this.GetMaxTraceableBlocks());
}

/// <summary>
/// Returns index of the latest block persisted to native Ledger contract.
/// </summary>
/// <returns>
/// Index of the latest persisted block.
/// </returns>
public uint CurrentIndex()
{
return NativeContract.Ledger.CurrentIndex(StoreView);
}
}
}
5 changes: 5 additions & 0 deletions src/Neo/Network/P2P/Payloads/Block.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ public sealed class Block : IEquatable<Block>, IInventory
/// </summary>
public UInt160 NextConsensus => Header.NextConsensus;

/// <summary>
/// MPT root hash calculated after the previous block is persisted.
/// </summary>
public UInt256 PrevStateRoot => Header.PrevStateRoot;

/// <summary>
/// The witness of the block.
/// </summary>
Expand Down
28 changes: 28 additions & 0 deletions src/Neo/Network/P2P/Payloads/BlockVersion.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Represents the block version.
/// </summary>
public enum BlockVersion : byte
{
/// <summary>
/// Initial block version.
/// </summary>
V0,
/// <summary>
/// Block version 1 which adds PrevStateRoot field to the header.
/// </summary>
V1,
}
}
29 changes: 28 additions & 1 deletion src/Neo/Network/P2P/Payloads/Header.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public sealed class Header : IEquatable<Header>, IVerifiable
private uint index;
private byte primaryIndex;
private UInt160 nextConsensus;
private UInt256 prevStateRoot;

/// <summary>
/// The witness of the block.
Expand Down Expand Up @@ -113,6 +114,15 @@ public UInt160 NextConsensus
set { nextConsensus = value; _hash = null; }
}

/// <summary>
/// MPT root hash got after the previous block processing.
/// </summary>
public UInt256 PrevStateRoot
{
get => prevStateRoot;
set { prevStateRoot = value; _hash = null; }
}

private UInt256 _hash = null;

/// <inheritdoc/>
Expand All @@ -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
Expand Down Expand Up @@ -166,14 +177,16 @@ 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<UInt256>();
merkleRoot = reader.ReadSerializable<UInt256>();
timestamp = reader.ReadUInt64();
nonce = reader.ReadUInt64();
index = reader.ReadUInt32();
primaryIndex = reader.ReadByte();
nextConsensus = reader.ReadSerializable<UInt160>();
if (version == (uint)BlockVersion.V1)
prevStateRoot = reader.ReadSerializable<UInt256>();
}

public bool Equals(Header other)
Expand Down Expand Up @@ -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);
}

/// <summary>
Expand All @@ -238,13 +253,17 @@ 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;
}

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;
Expand All @@ -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;
Expand All @@ -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()
Expand All @@ -278,6 +304,7 @@ public Header Clone()
Index = index,
PrimaryIndex = primaryIndex,
NextConsensus = nextConsensus,
PrevStateRoot = prevStateRoot,
Witness = Witness?.Clone(),
_hash = _hash
};
Expand Down
15 changes: 10 additions & 5 deletions src/Neo/SmartContract/Native/TrimmedBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -110,8 +111,7 @@ void IInteroperable.FromStackItem(StackItem stackItem)

StackItem IInteroperable.ToStackItem(IReferenceCounter referenceCounter)
{
return new Array(referenceCounter,
[
var block = new List<StackItem>() {
// Computed properties
Header.Hash.ToArray(),

Expand All @@ -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);
Comment on lines +129 to +133
Copy link
Member

@shargon shargon Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still thinking that if we append at the end, we don't affect SC that checks the Hashes.Length without check the version


return new Array(referenceCounter, block);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ public ExtensiblePayload MakePrepareRequest()
PrevHash = Block.PrevHash,
Timestamp = Block.Timestamp,
Nonce = Block.Nonce,
TransactionHashes = TransactionHashes
TransactionHashes = TransactionHashes,
PrevStateRoot = Block.PrevStateRoot,
});
}

Expand All @@ -141,6 +142,7 @@ public ExtensiblePayload MakeRecoveryMessage()
Nonce = Block.Nonce,
BlockIndex = Block.Index,
ValidatorIndex = Block.PrimaryIndex,
PrevStateRoot = Block.PrevStateRoot,
TransactionHashes = TransactionHashes
};
}
Expand Down
9 changes: 8 additions & 1 deletion src/Plugins/DBFTPlugin/Consensus/ConsensusContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this null here cause any issue? is the codebase robust enough to tolerate a null prevstateroot?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's checked.

}
};
TimePerBlock = neoSystem.GetTimePerBlock();
Expand Down Expand Up @@ -294,6 +297,8 @@ public void Deserialize(ref MemoryReader reader)
Block.Header.NextConsensus = reader.ReadSerializable<UInt160>();
if (Block.NextConsensus.Equals(UInt160.Zero))
Block.Header.NextConsensus = null;
if (Block.Version == (uint)BlockVersion.V1)
Block.Header.PrevStateRoot = reader.ReadSerializable<UInt256>();
ViewNumber = reader.ReadByte();
TransactionHashes = reader.ReadSerializableArray<UInt256>(ushort.MaxValue);
Transaction[] transactions = reader.ReadSerializableArray<Transaction>(ushort.MaxValue);
Expand All @@ -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<UInt256>());
writer.Write(Transactions?.Values.ToArray() ?? Array.Empty<Transaction>());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
1 change: 1 addition & 0 deletions src/Plugins/DBFTPlugin/DBFTPlugin.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

<ItemGroup>
<ProjectReference Include="..\..\Neo.ConsoleService\Neo.ConsoleService.csproj" />
<ProjectReference Include="..\StateService\StateService.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
5 changes: 4 additions & 1 deletion src/Plugins/DBFTPlugin/DBFTPlugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
"MaxBlockSize": 2097152,
"MaxBlockSystemFee": 150000000000,
"UnhandledExceptionPolicy": "StopNode"
}
},
"Dependency": [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why add this extra dependency ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because dBFT should have an access to stateroot at every block, and stateroot is managed by StateService plugin.

"StateService"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@neo-project/core There's a pitfall that may be moved to a separate issue: dBFT needs StateService because dBFT should have an access to stateroots, and stateroots are managed by StateService plugin. However, StateService plugin requires RPCServer plugin to be installed because of RPC method extensions. So in other words, dBFT requires RPC server which is not a proper dependency for dBFT.

We may split StateService plugin functionality into two plugins: one part will manage stateroots, another part will expose additional RPC methods. Let me know if you agree or have other suggestions on how to solve this issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ref. #4190.

]
}
7 changes: 7 additions & 0 deletions src/Plugins/DBFTPlugin/Messages/PrepareRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,13 +25,15 @@ 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
+ sizeof(uint) //Version
+ 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) { }
Expand All @@ -42,6 +45,8 @@ public override void Deserialize(ref MemoryReader reader)
PrevHash = reader.ReadSerializable<UInt256>();
Timestamp = reader.ReadUInt64();
Nonce = reader.ReadUInt64();
if (Version == (uint)BlockVersion.V1)
PrevStateRoot = reader.ReadSerializable<UInt256>();
TransactionHashes = reader.ReadSerializableArray<UInt256>(ushort.MaxValue);
if (TransactionHashes.Distinct().Count() != TransactionHashes.Length)
throw new FormatException();
Expand All @@ -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);
}
}
Expand Down
35 changes: 34 additions & 1 deletion src/Plugins/StateService/StatePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,35 @@ void ICommittingHandler.Blockchain_Committing_Handler(NeoSystem system, Block bl
IReadOnlyList<ApplicationExecuted> 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is not clear to me, why rebuild here and why the plugin would be out of sync

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's to avoid node resynchronization for existing CNs. We can skip doing this (frankly, It'd be even easier for us), but then CNs would need to enable the plugin and restart from genesis.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the plugin to restart, not the whole node. Maybe we need this feature

Copy link
Member Author

@AnnaShaleva AnnaShaleva Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the plugin to restart,

This may be moved to a separate issue, but for now it's implemented as is. I'm open to suggestions on a place where we can move this functionality, because right now it's not a proper place for it (right now we rebuild MPT after block processing first time when the node is started with StateService plugin in fact).

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<StorageKey, DataCache.Trackable>(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)
Expand All @@ -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;
Expand Down
Loading
Loading