diff --git a/src/Neo/Cryptography/ProofOfWork.cs b/src/Neo/Cryptography/ProofOfWork.cs
new file mode 100644
index 0000000000..df1ff4fa94
--- /dev/null
+++ b/src/Neo/Cryptography/ProofOfWork.cs
@@ -0,0 +1,68 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// ProofOfWork.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.
+
+using Neo.Extensions;
+using Neo.Extensions.Factories;
+using System;
+using System.Buffers.Binary;
+
+namespace Neo.Cryptography
+{
+ public class ProofOfWork
+ {
+ ///
+ /// Verify if the proof of work match with the difficulty.
+ ///
+ /// Proof of Work
+ /// Difficulty
+ ///
+ public static bool VerifyDifficulty(UInt256 proofOfWork, uint difficulty)
+ {
+ // Take the first 4 bytes in order to check the proof of work difficulty
+
+ var bytes = proofOfWork.ToArray();
+ var value = BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(0, 4));
+ return value < difficulty;
+ }
+
+ ///
+ /// Compute proof of work
+ ///
+ /// BlockHash
+ /// Nonce
+ /// Proof of Work
+ public static UInt256 Compute(UInt256 blockHash, long nonce)
+ {
+ var salt = new byte[16];
+ BinaryPrimitives.WriteInt64BigEndian(salt, nonce);
+
+ return (UInt256)Helper.Blake2b_256(blockHash.ToArray(), salt);
+ }
+
+ ///
+ /// Compute proof of work with difficulty
+ ///
+ /// Block hash
+ /// Difficulty
+ /// Nonce
+ public static long ComputeNonce(UInt256 blockHash, uint difficulty)
+ {
+ while (true)
+ {
+ var nonce = RandomNumberFactory.NextInt64();
+ var pow = Compute(blockHash, nonce);
+
+ if (VerifyDifficulty(pow, difficulty))
+ return nonce;
+ }
+ }
+ }
+}
diff --git a/src/Neo/SmartContract/Native/NeoToken.cs b/src/Neo/SmartContract/Native/NeoToken.cs
index b0ef6359e3..0d2cd5218f 100644
--- a/src/Neo/SmartContract/Native/NeoToken.cs
+++ b/src/Neo/SmartContract/Native/NeoToken.cs
@@ -11,8 +11,10 @@
#pragma warning disable IDE0051
+using Neo.Cryptography;
using Neo.Cryptography.ECC;
using Neo.Extensions;
+using Neo.Network.P2P.Payloads;
using Neo.Persistence;
using Neo.SmartContract.Iterators;
using Neo.SmartContract.Manifest;
@@ -286,6 +288,13 @@ internal override async ContractTask PostPersistAsync(ApplicationEngine engine)
}
}
}
+
+ // Set primary as alive
+
+ var key = CreateStorageKey(Prefix_Candidate, pubkey);
+ var item = engine.SnapshotCache.GetAndChange(key, () => new StorageItem(new CandidateState()));
+ var state = item.GetInteroperable();
+ state.LastProofOfLife = engine.PersistingBlock.Index;
}
///
@@ -411,6 +420,39 @@ private bool RegisterCandidate(ApplicationEngine engine, ECPoint pubkey)
return RegisterInternal(engine, pubkey);
}
+ ///
+ /// Proof of life of a candidate.
+ ///
+ /// The engine used to check witness and read data.
+ /// The public key of the candidate.
+ /// Proof of Work
+ /// Block Index
+ [ContractMethod(Hardfork.HF_Faun, RequiredCallFlags = CallFlags.States)]
+ private void ProofOfLife(ApplicationEngine engine, ECPoint pubkey, UInt256 proofOfWork, uint blockIndex)
+ {
+ if (engine.PersistingBlock == null || blockIndex >= engine.PersistingBlock.Index)
+ throw new Exception("Invalid proof of work");
+
+ if (!engine.CheckWitnessInternal(Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash()))
+ throw new Exception("Invalid witness");
+
+ var key = CreateStorageKey(Prefix_Candidate, pubkey);
+ var item = engine.SnapshotCache.GetAndChange(key, () => new StorageItem(new CandidateState()));
+ var state = item.GetInteroperable();
+ if (!state.Registered)
+ throw new Exception("Only registered candidates are availables");
+
+ if (!ProofOfWork.VerifyDifficulty(proofOfWork, Policy.GetProofOfLifeDifficulty(engine.SnapshotCache)))
+ throw new Exception("Proof of work is too easy");
+
+ var blockHash = Ledger.GetBlockHash(engine.SnapshotCache, blockIndex);
+
+ if (blockHash == null || ProofOfWork.Compute(blockHash, (engine.ScriptContainer as Transaction)?.Nonce ?? 0) != proofOfWork)
+ throw new Exception("Invalid proof of work");
+
+ state.LastProofOfLife = blockIndex;
+ }
+
private bool RegisterInternal(ApplicationEngine engine, ECPoint pubkey)
{
if (!engine.CheckWitnessInternal(Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash()))
@@ -521,12 +563,12 @@ internal async ContractTask VoteInternal(ApplicationEngine engine, UInt160
///
/// Gets the first 256 registered candidates.
///
- /// The snapshot used to read data.
+ /// The ApplicationEngine used.
/// All the registered candidates.
[ContractMethod(CpuFee = 1 << 22, RequiredCallFlags = CallFlags.ReadStates)]
- internal (ECPoint PublicKey, BigInteger Votes)[] GetCandidates(DataCache snapshot)
+ internal (ECPoint PublicKey, BigInteger Votes)[] GetCandidates(ApplicationEngine engine)
{
- return GetCandidatesInternal(snapshot)
+ return GetCandidatesInternal(engine.SnapshotCache, engine.IsHardforkEnabled(Hardfork.HF_Faun))
.Select(p => (p.PublicKey, p.State.Votes))
.Take(256)
.ToArray();
@@ -535,24 +577,26 @@ internal async ContractTask VoteInternal(ApplicationEngine engine, UInt160
///
/// Gets the registered candidates iterator.
///
- /// The snapshot used to read data.
+ /// The ApplicationEngine used.
/// All the registered candidates.
[ContractMethod(CpuFee = 1 << 22, RequiredCallFlags = CallFlags.ReadStates)]
- private IIterator GetAllCandidates(IReadOnlyStore snapshot)
+ private IIterator GetAllCandidates(ApplicationEngine engine)
{
const FindOptions options = FindOptions.RemovePrefix | FindOptions.DeserializeValues | FindOptions.PickField1;
- var enumerator = GetCandidatesInternal(snapshot)
+ var enumerator = GetCandidatesInternal(engine.SnapshotCache, engine.IsHardforkEnabled(Hardfork.HF_Faun))
.Select(p => (p.Key, p.Value))
.GetEnumerator();
return new StorageIterator(enumerator, 1, options);
}
- internal IEnumerable<(StorageKey Key, StorageItem Value, ECPoint PublicKey, CandidateState State)> GetCandidatesInternal(IReadOnlyStore snapshot)
+ internal IEnumerable<(StorageKey Key, StorageItem Value, ECPoint PublicKey, CandidateState State)> GetCandidatesInternal(IReadOnlyStore snapshot, bool withProofOfLife)
{
var prefixKey = CreateStorageKey(Prefix_Candidate);
+ var requiredProofOfLife = Ledger.CurrentIndex(snapshot) - Policy.GetMaxProofOfNodeHeight(snapshot);
+
return snapshot.Find(prefixKey)
.Select(p => (p.Key, p.Value, PublicKey: p.Key.Key[1..].AsSerializable(), State: p.Value.GetInteroperable()))
- .Where(p => p.State.Registered)
+ .Where(p => p.State.Registered && (!withProofOfLife || (p.State.LastProofOfLife != null && p.State.LastProofOfLife >= requiredProofOfLife)))
.Where(p => !Policy.IsBlocked(snapshot, Contract.CreateSignatureRedeemScript(p.PublicKey).ToScriptHash()));
}
@@ -624,11 +668,20 @@ public ECPoint[] ComputeNextBlockValidators(DataCache snapshot, ProtocolSettings
private IEnumerable<(ECPoint PublicKey, BigInteger Votes)> ComputeCommitteeMembers(DataCache snapshot, ProtocolSettings settings)
{
- decimal votersCount = (decimal)(BigInteger)snapshot[_votersCount];
- decimal voterTurnout = votersCount / (decimal)TotalAmount;
- var candidates = GetCandidatesInternal(snapshot)
+ var votersCount = (decimal)(BigInteger)snapshot[_votersCount];
+ var voterTurnout = votersCount / (decimal)TotalAmount;
+ var candidates = GetCandidatesInternal(snapshot, settings.IsHardforkEnabled(Hardfork.HF_Faun, Ledger.CurrentIndex(snapshot)))
.Select(p => (p.PublicKey, p.State.Votes))
.ToArray();
+
+ if (candidates.Length < settings.CommitteeMembersCount)
+ {
+ // If there are not enough candidates, include those without proof of life
+ candidates = GetCandidatesInternal(snapshot, false)
+ .Select(p => (p.PublicKey, p.State.Votes))
+ .ToArray();
+ }
+
if (voterTurnout < EffectiveVoterTurnout || candidates.Length < settings.CommitteeMembersCount)
return settings.StandbyCommittee.Select(p => (p, candidates.FirstOrDefault(k => k.PublicKey.Equals(p)).Votes));
return candidates
@@ -703,16 +756,27 @@ internal class CandidateState : IInteroperable
{
public bool Registered;
public BigInteger Votes;
+ public uint? LastProofOfLife;
public void FromStackItem(StackItem stackItem)
{
- Struct @struct = (Struct)stackItem;
+ var @struct = (Struct)stackItem;
Registered = @struct[0].GetBoolean();
Votes = @struct[1].GetInteger();
+
+ if (@struct.Count > 2)
+ {
+ LastProofOfLife = (uint)@struct[2].GetInteger();
+ }
}
public StackItem ToStackItem(IReferenceCounter? referenceCounter)
{
+ if (LastProofOfLife.HasValue)
+ {
+ return new Struct(referenceCounter) { Registered, Votes, LastProofOfLife.Value };
+ }
+
return new Struct(referenceCounter) { Registered, Votes };
}
}
diff --git a/src/Neo/SmartContract/Native/PolicyContract.cs b/src/Neo/SmartContract/Native/PolicyContract.cs
index 6fd802367d..c1fd9ed0fc 100644
--- a/src/Neo/SmartContract/Native/PolicyContract.cs
+++ b/src/Neo/SmartContract/Native/PolicyContract.cs
@@ -53,6 +53,16 @@ public sealed class PolicyContract : NativeContract
///
public const uint DefaultNotaryAssistedAttributeFee = 1000_0000;
+ ///
+ /// The default max proof of node height.
+ ///
+ public const uint DefaultMaxProofOfNodeHeight = 10_000;
+
+ ///
+ /// The default proof of node difficulty.
+ ///
+ public const uint DefaultProofOfNodeDifficulty = 0x00FFFFFF;
+
///
/// The maximum execution fee factor that the committee can set.
///
@@ -94,6 +104,8 @@ public sealed class PolicyContract : NativeContract
private const byte Prefix_MillisecondsPerBlock = 21;
private const byte Prefix_MaxValidUntilBlockIncrement = 22;
private const byte Prefix_MaxTraceableBlocks = 23;
+ private const byte Prefix_MaxProofOfNodeHeight = 24;
+ private const byte Prefix_ProofOfNodeDifficulty = 25;
private readonly StorageKey _feePerByte;
private readonly StorageKey _execFeeFactor;
@@ -101,6 +113,8 @@ public sealed class PolicyContract : NativeContract
private readonly StorageKey _millisecondsPerBlock;
private readonly StorageKey _maxValidUntilBlockIncrement;
private readonly StorageKey _maxTraceableBlocks;
+ private readonly StorageKey _maxProofOfNodeHeight;
+ private readonly StorageKey _proofOfNodeDifficulty;
///
/// The event name for the block generation time changed.
@@ -127,6 +141,8 @@ internal PolicyContract() : base()
_millisecondsPerBlock = CreateStorageKey(Prefix_MillisecondsPerBlock);
_maxValidUntilBlockIncrement = CreateStorageKey(Prefix_MaxValidUntilBlockIncrement);
_maxTraceableBlocks = CreateStorageKey(Prefix_MaxTraceableBlocks);
+ _maxProofOfNodeHeight = CreateStorageKey(Prefix_MaxProofOfNodeHeight);
+ _proofOfNodeDifficulty = CreateStorageKey(Prefix_ProofOfNodeDifficulty);
}
internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardfork)
@@ -144,11 +160,11 @@ internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfor
engine.SnapshotCache.Add(_maxValidUntilBlockIncrement, new StorageItem(engine.ProtocolSettings.MaxValidUntilBlockIncrement));
engine.SnapshotCache.Add(_maxTraceableBlocks, new StorageItem(engine.ProtocolSettings.MaxTraceableBlocks));
}
-
- // After Faun Hardfork the unit it's pico-gas, before it was datoshi
-
if (hardfork == Hardfork.HF_Faun)
{
+ engine.SnapshotCache.Add(_maxProofOfNodeHeight, new StorageItem(DefaultMaxProofOfNodeHeight));
+ engine.SnapshotCache.Add(_proofOfNodeDifficulty, new StorageItem(DefaultProofOfNodeDifficulty));
+
// Add decimals to exec fee factor
var item = engine.SnapshotCache.TryGet(_execFeeFactor) ??
throw new InvalidOperationException("Policy was not initialized");
@@ -270,6 +286,28 @@ public uint GetAttributeFeeV1(IReadOnlyStore snapshot, byte attributeType)
return GetAttributeFee(snapshot, attributeType, true);
}
+ ///
+ /// Gets the max proof of node height.
+ ///
+ /// The snapshot used to read data.
+ /// The proof of node height.
+ [ContractMethod(Hardfork.HF_Echidna, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)]
+ public uint GetMaxProofOfNodeHeight(IReadOnlyStore snapshot)
+ {
+ return (uint)(BigInteger)snapshot[_maxProofOfNodeHeight];
+ }
+
+ ///
+ /// Gets the max proof of node difficulty.
+ ///
+ /// The snapshot used to read data.
+ /// The proof of node height.
+ [ContractMethod(Hardfork.HF_Echidna, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)]
+ public uint GetProofOfLifeDifficulty(IReadOnlyStore snapshot)
+ {
+ return (uint)(BigInteger)snapshot[_proofOfNodeDifficulty];
+ }
+
///
/// Generic handler for GetAttributeFeeV0 and GetAttributeFee that
/// gets the fee for attribute.
@@ -481,6 +519,26 @@ private void SetAttributeFee(ApplicationEngine engine, byte attributeType, uint
engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_AttributeFee, attributeType), () => new StorageItem(DefaultAttributeFee)).Set(value);
}
+ [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)]
+ private void SetMaxProofOfNodeHeight(ApplicationEngine engine, uint value)
+ {
+ var maxValue = GetMaxTraceableBlocks(engine.SnapshotCache);
+
+ if (value < 100 || value > maxValue)
+ throw new ArgumentOutOfRangeException(nameof(value), $"MaxProofOfNodeHeight must be between [100, {maxValue}], got {value}");
+ AssertCommittee(engine);
+
+ engine.SnapshotCache.GetAndChange(_maxProofOfNodeHeight)!.Set(value);
+ }
+
+ [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)]
+ private void SetProofOfNodeDifficulty(ApplicationEngine engine, uint value)
+ {
+ AssertCommittee(engine);
+
+ engine.SnapshotCache.GetAndChange(_proofOfNodeDifficulty)!.Set(value);
+ }
+
[ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)]
private void SetFeePerByte(ApplicationEngine engine, long value)
{
diff --git a/tests/Neo.UnitTests/Cryptography/UT_ProofOfWork.cs b/tests/Neo.UnitTests/Cryptography/UT_ProofOfWork.cs
new file mode 100644
index 0000000000..fa0dbd8d2d
--- /dev/null
+++ b/tests/Neo.UnitTests/Cryptography/UT_ProofOfWork.cs
@@ -0,0 +1,59 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// UT_ProofOfWork.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.
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Neo.Cryptography;
+using Neo.Extensions;
+using System.Buffers.Binary;
+
+namespace Neo.UnitTests.Cryptography
+{
+ [TestClass]
+ public class UT_ProofOfWork
+ {
+ [TestMethod]
+ public void VerifyDifficulty_ShouldReturnTrue_WhenProofIsBelowDifficulty()
+ {
+ var proofBytes = new byte[32];
+ BinaryPrimitives.WriteInt64BigEndian(proofBytes, 10);
+
+ var proof = new UInt256(proofBytes);
+ var result = ProofOfWork.VerifyDifficulty(proof, 100);
+
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ public void VerifyDifficulty_ShouldReturnFalse_WhenProofIsAboveDifficulty()
+ {
+ var proofBytes = new byte[32];
+ BinaryPrimitives.WriteInt64BigEndian(proofBytes, 200);
+
+ var proof = new UInt256(proofBytes);
+ var result = ProofOfWork.VerifyDifficulty(proof, 100);
+
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ public void ComputeNonce_ShouldReturnNonceMeetingDifficulty()
+ {
+ var blockHash = new UInt256(new byte[32]);
+ uint difficulty = 0x00FFFFFF; // very low difficulty for quick test
+
+ var nonce = ProofOfWork.ComputeNonce(blockHash, difficulty);
+ var pow = ProofOfWork.Compute(blockHash, nonce);
+
+ Assert.IsTrue(ProofOfWork.VerifyDifficulty(pow, difficulty));
+ Assert.AreEqual(0, pow.ToArray()[0]);
+ }
+ }
+}
diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NeoToken.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NeoToken.cs
index 5174f9a4ab..ef8b0a635d 100644
--- a/tests/Neo.UnitTests/SmartContract/Native/UT_NeoToken.cs
+++ b/tests/Neo.UnitTests/SmartContract/Native/UT_NeoToken.cs
@@ -366,7 +366,7 @@ public void Check_RegisterValidator()
// Check GetRegisteredValidators
- var members = NativeContract.NEO.GetCandidatesInternal(clonedCache);
+ var members = NativeContract.NEO.GetCandidatesInternal(clonedCache, false);
Assert.AreEqual(2, members.Count());
}
@@ -396,7 +396,7 @@ public void Check_RegisterValidatorViaNEP27()
Assert.IsTrue(ret.Result);
// Check GetRegisteredValidators
- var members = NativeContract.NEO.GetCandidatesInternal(clonedCache);
+ var members = NativeContract.NEO.GetCandidatesInternal(clonedCache, false);
Assert.AreEqual(1, members.Count());
Assert.AreEqual(point, members.First().PublicKey);
@@ -426,7 +426,7 @@ public void Check_UnregisterCandidate()
Assert.IsTrue(ret.State);
Assert.IsTrue(ret.Result);
- var members = NativeContract.NEO.GetCandidatesInternal(clonedCache);
+ var members = NativeContract.NEO.GetCandidatesInternal(clonedCache, false);
Assert.AreEqual(1, members.Count());
Assert.AreEqual(keyCount + 1, clonedCache.GetChangeSet().Count());
StorageKey key = CreateStorageKey(33, point);
@@ -438,7 +438,7 @@ public void Check_UnregisterCandidate()
Assert.AreEqual(keyCount, clonedCache.GetChangeSet().Count());
- members = NativeContract.NEO.GetCandidatesInternal(clonedCache);
+ members = NativeContract.NEO.GetCandidatesInternal(clonedCache, false);
Assert.AreEqual(0, members.Count());
Assert.IsNull(clonedCache.TryGet(key));
@@ -775,12 +775,12 @@ public void TestGetCandidates1()
public void TestGetCandidates2()
{
var clonedCache = _snapshotCache.CloneCache();
- var result = NativeContract.NEO.GetCandidatesInternal(clonedCache);
+ var result = NativeContract.NEO.GetCandidatesInternal(clonedCache, false);
Assert.AreEqual(0, result.Count());
StorageKey key = NativeContract.NEO.CreateStorageKey(33, ECCurve.Secp256r1.G);
clonedCache.Add(key, new StorageItem(new CandidateState() { Registered = true }));
- Assert.AreEqual(1, NativeContract.NEO.GetCandidatesInternal(clonedCache).Count());
+ Assert.AreEqual(1, NativeContract.NEO.GetCandidatesInternal(clonedCache, false).Count());
}
[TestMethod]
@@ -869,6 +869,7 @@ public void TestGetValidators()
{
var clonedCache = _snapshotCache.CloneCache();
var result = NativeContract.NEO.ComputeNextBlockValidators(clonedCache, TestProtocolSettings.Default);
+
Assert.AreEqual("02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70", result[0].ToArray().ToHexString());
Assert.AreEqual("024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d", result[1].ToArray().ToHexString());
Assert.AreEqual("02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e", result[2].ToArray().ToHexString());