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());