diff --git a/src/Neo/SmartContract/Native/NativeContract.cs b/src/Neo/SmartContract/Native/NativeContract.cs
index 85a58033a9..33fe6a3425 100644
--- a/src/Neo/SmartContract/Native/NativeContract.cs
+++ b/src/Neo/SmartContract/Native/NativeContract.cs
@@ -104,6 +104,11 @@ public CacheEntry GetAllowedMethods(NativeContract native, ApplicationEngine eng
///
public static OracleContract Oracle { get; } = new();
+ ///
+ /// Gets the instance of the class.
+ ///
+ public static Notary Notary { get; } = new();
+
#endregion
///
diff --git a/src/Neo/SmartContract/Native/Notary.cs b/src/Neo/SmartContract/Native/Notary.cs
new file mode 100644
index 0000000000..7c3c89dc2c
--- /dev/null
+++ b/src/Neo/SmartContract/Native/Notary.cs
@@ -0,0 +1,343 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// Notary.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.
+
+#nullable enable
+#pragma warning disable IDE0051
+
+using Neo.Cryptography;
+using Neo.Cryptography.ECC;
+using Neo.Extensions;
+using Neo.Network.P2P;
+using Neo.Network.P2P.Payloads;
+using Neo.Persistence;
+using Neo.SmartContract.Manifest;
+using Neo.VM;
+using Neo.VM.Types;
+using System;
+using System.Linq;
+using System.Numerics;
+using Array = Neo.VM.Types.Array;
+
+namespace Neo.SmartContract.Native
+{
+ ///
+ /// The Notary native contract used for multisignature transactions forming assistance.
+ ///
+ public sealed class Notary : NativeContract
+ {
+ ///
+ /// A default value for maximum allowed NotValidBeforeDelta. It is set to be
+ /// 20 rounds for 7 validators, a little more than half an hour for 15-seconds blocks.
+ ///
+ private const int DefaultMaxNotValidBeforeDelta = 140;
+ ///
+ /// A default value for deposit lock period.
+ ///
+ private const int DefaultDepositDeltaTill = 5760;
+ private const byte Prefix_Deposit = 1;
+ private const byte Prefix_MaxNotValidBeforeDelta = 10;
+
+ internal Notary() : base() { }
+
+ public override Hardfork? ActiveIn => Hardfork.HF_Echidna;
+
+ internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardfork)
+ {
+ if (hardfork == ActiveIn)
+ {
+ engine.SnapshotCache.Add(CreateStorageKey(Prefix_MaxNotValidBeforeDelta), new StorageItem(DefaultMaxNotValidBeforeDelta));
+ }
+ return ContractTask.CompletedTask;
+ }
+
+ internal override async ContractTask OnPersistAsync(ApplicationEngine engine)
+ {
+ long nFees = 0;
+ ECPoint[]? notaries = null;
+ foreach (var tx in engine.PersistingBlock.Transactions)
+ {
+ var attr = tx.GetAttribute();
+ if (attr is not null)
+ {
+ notaries ??= GetNotaryNodes(engine.SnapshotCache);
+ var nKeys = attr.NKeys;
+ nFees += (long)nKeys + 1;
+ if (tx.Sender == Hash)
+ {
+ var payer = tx.Signers[1];
+ // Don't need to seal because Deposit is a fixed-sized interoperable, hence always can be serialized.
+ var balance = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Deposit, payer.Account))?.GetInteroperable();
+ if (balance != null)
+ {
+ balance.Amount -= tx.SystemFee + tx.NetworkFee;
+ if (balance.Amount.Sign == 0) RemoveDepositFor(engine.SnapshotCache, payer.Account);
+ }
+ }
+ }
+ }
+ if (nFees == 0) return;
+ if (notaries == null) return;
+ var singleReward = CalculateNotaryReward(engine.SnapshotCache, nFees, notaries.Length);
+ foreach (var notary in notaries) await GAS.Mint(engine, Contract.CreateSignatureRedeemScript(notary).ToScriptHash(), singleReward, false);
+ }
+
+ protected override void OnManifestCompose(IsHardforkEnabledDelegate hfChecker, uint blockHeight, ContractManifest manifest)
+ {
+ manifest.SupportedStandards = ["NEP-27"];
+ }
+
+ ///
+ /// Verify checks whether the transaction is signed by one of the notaries and
+ /// ensures whether deposited amount of GAS is enough to pay the actual sender's fee.
+ ///
+ /// ApplicationEngine
+ /// Signature
+ /// Whether transaction is valid.
+ [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)]
+ private bool Verify(ApplicationEngine engine, byte[] signature)
+ {
+ if (signature is null || signature.Length != 64) return false;
+ var tx = engine.ScriptContainer as Transaction;
+ if (tx?.GetAttribute() is null) return false;
+ foreach (var signer in tx.Signers)
+ {
+ if (signer.Account == Hash)
+ {
+ if (signer.Scopes != WitnessScope.None) return false;
+ break;
+ }
+ }
+ if (tx.Sender == Hash)
+ {
+ if (tx.Signers.Length != 2) return false;
+ var payer = tx.Signers[1].Account;
+ var balance = GetDepositFor(engine.SnapshotCache, payer);
+ if (balance is null || balance.Amount.CompareTo(tx.NetworkFee + tx.SystemFee) < 0) return false;
+ }
+ var notaries = GetNotaryNodes(engine.SnapshotCache);
+ var hash = tx.GetSignData(engine.GetNetwork());
+ return notaries.Any(n => Crypto.VerifySignature(hash, signature, n));
+ }
+
+ ///
+ /// OnNEP17Payment is a callback that accepts GAS transfer as Notary deposit.
+ /// It also sets the deposit's lock height after which deposit can be withdrawn.
+ ///
+ /// ApplicationEngine
+ /// GAS sender
+ /// The amount of GAS sent
+ /// Deposit-related data: optional To value (treated as deposit owner if set) and Till height after which deposit can be withdrawn
+ [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)]
+ private void OnNEP17Payment(ApplicationEngine engine, UInt160 from, BigInteger amount, StackItem data)
+ {
+ if (engine.CallingScriptHash != GAS.Hash) throw new InvalidOperationException(string.Format("only GAS can be accepted for deposit, got {0}", engine.CallingScriptHash.ToString()));
+ if (data is not Array additionalParams || additionalParams.Count != 2) throw new FormatException("`data` parameter should be an array of 2 elements");
+ var to = from;
+ if (!additionalParams[0].Equals(StackItem.Null)) to = additionalParams[0].GetSpan().ToArray().AsSerializable();
+ var till = (uint)additionalParams[1].GetInteger();
+ var tx = (Transaction)engine.ScriptContainer;
+ var allowedChangeTill = tx.Sender == to;
+ var currentHeight = Ledger.CurrentIndex(engine.SnapshotCache);
+ if (till < currentHeight + 2) throw new ArgumentOutOfRangeException(string.Format("`till` shouldn't be less than the chain's height {0} + 1", currentHeight + 2));
+ // Don't need to seal because Deposit is a fixed-sized interoperable, hence always can be serialized.
+ var deposit = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Deposit, to))?.GetInteroperable();
+ if (deposit != null && till < deposit.Till) throw new ArgumentOutOfRangeException(string.Format("`till` shouldn't be less than the previous value {0}", deposit.Till));
+ if (deposit is null)
+ {
+ var feePerKey = Policy.GetAttributeFeeV1(engine.SnapshotCache, (byte)TransactionAttributeType.NotaryAssisted);
+ if ((long)amount < 2 * feePerKey) throw new ArgumentOutOfRangeException(string.Format("first deposit can not be less than {0}, got {1}", 2 * feePerKey, amount));
+ deposit = new Deposit() { Amount = 0, Till = 0 };
+ if (!allowedChangeTill) till = currentHeight + DefaultDepositDeltaTill;
+ }
+ else if (!allowedChangeTill) till = deposit.Till;
+
+ deposit.Amount += amount;
+ deposit.Till = till;
+ PutDepositFor(engine, to, deposit);
+ }
+
+ ///
+ /// Lock asset until the specified height is unlocked.
+ ///
+ /// ApplicationEngine
+ /// Account
+ /// specified height
+ /// Whether deposit lock height was successfully updated.
+ [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)]
+ public bool LockDepositUntil(ApplicationEngine engine, UInt160 account, uint till)
+ {
+ if (!engine.CheckWitnessInternal(account)) return false;
+ if (till < Ledger.CurrentIndex(engine.SnapshotCache) + 2) return false; // deposit must be valid at least until the next block after persisting block.
+ var deposit = GetDepositFor(engine.SnapshotCache, account);
+ if (deposit is null || till < deposit.Till) return false;
+ deposit.Till = till;
+
+ PutDepositFor(engine, account, deposit);
+ return true;
+ }
+
+ ///
+ /// ExpirationOf returns deposit lock height for specified address.
+ ///
+ /// DataCache
+ /// Account
+ /// Deposit lock height of the specified address.
+ [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)]
+ public uint ExpirationOf(DataCache snapshot, UInt160 account)
+ {
+ var deposit = GetDepositFor(snapshot, account);
+ if (deposit is null) return 0;
+ return deposit.Till;
+ }
+
+ ///
+ /// BalanceOf returns deposited GAS amount for specified address.
+ ///
+ /// DataCache
+ /// Account
+ /// Deposit balance of the specified account.
+ [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)]
+ public BigInteger BalanceOf(DataCache snapshot, UInt160 account)
+ {
+ var deposit = GetDepositFor(snapshot, account);
+ if (deposit is null) return 0;
+ return deposit.Amount;
+ }
+
+ ///
+ /// Withdraw sends all deposited GAS for "from" address to "to" address. If "to"
+ /// address is not specified, then "from" will be used as a sender.
+ ///
+ /// ApplicationEngine
+ /// From Account
+ /// To Account
+ /// Whether withdrawal was successfull.
+ [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.All)]
+ private async ContractTask Withdraw(ApplicationEngine engine, UInt160 from, UInt160 to)
+ {
+ if (!engine.CheckWitnessInternal(from)) return false;
+ var receive = to is null ? from : to;
+ var deposit = GetDepositFor(engine.SnapshotCache, from);
+ if (deposit is null) return false;
+ if (Ledger.CurrentIndex(engine.SnapshotCache) < deposit.Till) return false;
+ RemoveDepositFor(engine.SnapshotCache, from);
+ if (!await engine.CallFromNativeContractAsync(Hash, GAS.Hash, "transfer", Hash.ToArray(), receive.ToArray(), deposit.Amount, StackItem.Null))
+ {
+ throw new InvalidOperationException(string.Format("Transfer to {0} has failed", receive.ToString()));
+ }
+ return true;
+ }
+
+ ///
+ /// GetMaxNotValidBeforeDelta is Notary contract method and returns the maximum NotValidBefore delta.
+ ///
+ /// DataCache
+ /// NotValidBefore
+ [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)]
+ public uint GetMaxNotValidBeforeDelta(IReadOnlyStore snapshot)
+ {
+ return (uint)(BigInteger)snapshot[CreateStorageKey(Prefix_MaxNotValidBeforeDelta)];
+ }
+
+ ///
+ /// SetMaxNotValidBeforeDelta is Notary contract method and sets the maximum NotValidBefore delta.
+ ///
+ /// ApplicationEngine
+ /// Value
+ [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)]
+ private void SetMaxNotValidBeforeDelta(ApplicationEngine engine, uint value)
+ {
+ var maxVUBIncrement = engine.SnapshotCache.GetMaxValidUntilBlockIncrement(engine.ProtocolSettings);
+ if (value > maxVUBIncrement / 2 || value < ProtocolSettings.Default.ValidatorsCount)
+ throw new FormatException(string.Format("MaxNotValidBeforeDelta cannot be more than {0} or less than {1}",
+ maxVUBIncrement / 2, ProtocolSettings.Default.ValidatorsCount));
+ if (!CheckCommittee(engine)) throw new InvalidOperationException();
+ engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_MaxNotValidBeforeDelta))!.Set(value);
+ }
+
+ ///
+ /// GetNotaryNodes returns public keys of notary nodes.
+ ///
+ /// DataCache
+ /// Public keys of notary nodes.
+ private static ECPoint[] GetNotaryNodes(DataCache snapshot)
+ {
+ return RoleManagement.GetDesignatedByRole(snapshot, Role.P2PNotary, Ledger.CurrentIndex(snapshot) + 1);
+ }
+
+ ///
+ /// GetDepositFor returns Deposit for the specified account or nil in case if deposit
+ /// is not found in storage.
+ ///
+ ///
+ ///
+ /// Deposit for the specified account.
+ private Deposit? GetDepositFor(DataCache snapshot, UInt160 acc)
+ {
+ return snapshot.TryGet(CreateStorageKey(Prefix_Deposit, acc))?.GetInteroperable();
+ }
+
+ ///
+ /// PutDepositFor puts deposit on the balance of the specified account in the storage.
+ ///
+ /// ApplicationEngine
+ /// Account
+ /// deposit
+ private void PutDepositFor(ApplicationEngine engine, UInt160 acc, Deposit deposit)
+ {
+ // Don't need to seal because Deposit is a fixed-sized interoperable, hence always can be serialized.
+ var indeposit = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Deposit, acc), () => new StorageItem(deposit));
+ indeposit!.Value = new StorageItem(deposit).Value;
+ }
+
+ ///
+ /// RemoveDepositFor removes deposit from the storage.
+ ///
+ /// DataCache
+ /// Account
+ private void RemoveDepositFor(DataCache snapshot, UInt160 acc)
+ {
+ snapshot.Delete(CreateStorageKey(Prefix_Deposit, acc));
+ }
+
+ ///
+ /// CalculateNotaryReward calculates the reward for a single notary node based on FEE's count and Notary nodes count.
+ ///
+ /// DataCache
+ ///
+ ///
+ /// result
+ private static long CalculateNotaryReward(IReadOnlyStore snapshot, long nFees, int notariesCount)
+ {
+ return nFees * Policy.GetAttributeFeeV1(snapshot, (byte)TransactionAttributeType.NotaryAssisted) / notariesCount;
+ }
+
+ public class Deposit : IInteroperable
+ {
+ public BigInteger Amount { get; set; }
+ public uint Till { get; set; }
+
+ public void FromStackItem(StackItem stackItem)
+ {
+ var @struct = (Struct)stackItem;
+ Amount = @struct[0].GetInteger();
+ Till = (uint)@struct[1].GetInteger();
+ }
+
+ public StackItem ToStackItem(IReferenceCounter referenceCounter)
+ {
+ return new Struct(referenceCounter) { Amount, Till };
+ }
+ }
+ }
+}
+
+#nullable disable
diff --git a/tests/Neo.UnitTests/Extensions/NativeContractExtensions.cs b/tests/Neo.UnitTests/Extensions/NativeContractExtensions.cs
index acfb835944..2dad5dc458 100644
--- a/tests/Neo.UnitTests/Extensions/NativeContractExtensions.cs
+++ b/tests/Neo.UnitTests/Extensions/NativeContractExtensions.cs
@@ -132,6 +132,11 @@ public static StackItem Call(this NativeContract contract, DataCache snapshot, s
public static StackItem Call(this NativeContract contract, DataCache snapshot, IVerifiable container, Block persistingBlock, string method, params ContractParameter[] args)
{
using var engine = ApplicationEngine.Create(TriggerType.Application, container, snapshot, persistingBlock, settings: TestProtocolSettings.Default);
+ return Call(contract, engine, method, args);
+ }
+
+ public static StackItem Call(this NativeContract contract, ApplicationEngine engine, string method, params ContractParameter[] args)
+ {
using var script = new ScriptBuilder();
script.EmitDynamicCall(contract.Hash, method, args);
engine.LoadScript(script.ToArray());
diff --git a/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs b/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs
index fab7d9a544..e2b9b89f9f 100644
--- a/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs
+++ b/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs
@@ -52,12 +52,38 @@ public void SerializeUnsigned(BinaryWriter writer) { }
}
public static bool Transfer(this NativeContract contract, DataCache snapshot, byte[] from, byte[] to, BigInteger amount, bool signFrom, Block persistingBlock)
+ {
+ return Transfer(contract, snapshot, from, to, amount, signFrom, persistingBlock, null);
+ }
+
+ public static bool Transfer(this NativeContract contract, DataCache snapshot, byte[] from, byte[] to, BigInteger amount, bool signFrom, Block persistingBlock, object data)
{
using var engine = ApplicationEngine.Create(TriggerType.Application,
new ManualWitness(signFrom ? new UInt160(from) : null), snapshot, persistingBlock, settings: TestProtocolSettings.Default);
using var script = new ScriptBuilder();
- script.EmitDynamicCall(contract.Hash, "transfer", from, to, amount, null);
+ script.EmitDynamicCall(contract.Hash, "transfer", from, to, amount, data);
+ engine.LoadScript(script.ToArray());
+
+ if (engine.Execute() == VMState.FAULT)
+ {
+ throw engine.FaultException;
+ }
+
+ var result = engine.ResultStack.Pop();
+ Assert.IsInstanceOfType(result, typeof(Boolean));
+
+ return result.GetBoolean();
+ }
+
+ public static bool TransferWithTransaction(this NativeContract contract, DataCache snapshot, byte[] from, byte[] to, BigInteger amount, bool signFrom, Block persistingBlock, object data)
+ {
+ using var engine = ApplicationEngine.Create(TriggerType.Application,
+ new Transaction() { Signers = [new() { Account = signFrom ? new(from) : null, Scopes = WitnessScope.Global }], Attributes = [] },
+ snapshot, persistingBlock, settings: TestProtocolSettings.Default);
+
+ using var script = new ScriptBuilder();
+ script.EmitDynamicCall(contract.Hash, "transfer", from, to, amount, data);
engine.LoadScript(script.ToArray());
if (engine.Execute() == VMState.FAULT)
diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs
index 0a7bec5b20..d395022d37 100644
--- a/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs
+++ b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs
@@ -20,10 +20,10 @@
using Neo.VM;
using Neo.Wallets;
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
-using VMTypes = Neo.VM.Types;
namespace Neo.UnitTests.SmartContract.Native
{
@@ -154,61 +154,5 @@ internal static StorageKey CreateStorageKey(byte prefix, byte[] key = null)
Key = buffer
};
}
-
- [TestMethod]
- public void Check_OnPersist_NotaryAssisted()
- {
- // Hardcode test values.
- const uint defaultNotaryssestedFeePerKey = 1000_0000;
- const byte NKeys1 = 4;
- const byte NKeys2 = 6;
-
- // Generate two transactions with NotaryAssisted attributes with hardcoded NKeys values.
- var from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators);
- var tx1 = TestUtils.GetTransaction(from);
- tx1.Attributes = new TransactionAttribute[] { new NotaryAssisted() { NKeys = NKeys1 } };
- var netFee1 = 1_0000_0000;
- tx1.NetworkFee = netFee1;
- var tx2 = TestUtils.GetTransaction(from);
- tx2.Attributes = new TransactionAttribute[] { new NotaryAssisted() { NKeys = NKeys2 } };
- var netFee2 = 2_0000_0000;
- tx2.NetworkFee = netFee2;
-
- // Calculate expected Notary nodes reward.
- var expectedNotaryReward = (NKeys1 + 1) * defaultNotaryssestedFeePerKey + (NKeys2 + 1) * defaultNotaryssestedFeePerKey;
-
- // Build block to check transaction fee distribution during Gas OnPersist.
- var persistingBlock = new Block
- {
- Header = new Header
- {
- Index = (uint)TestProtocolSettings.Default.CommitteeMembersCount,
- MerkleRoot = UInt256.Zero,
- NextConsensus = UInt160.Zero,
- PrevHash = UInt256.Zero,
- Witness = Witness.Empty
- },
- Transactions = [tx1, tx2],
- };
- var snapshot = _snapshotCache.CloneCache();
- var script = new ScriptBuilder();
- script.EmitSysCall(ApplicationEngine.System_Contract_NativeOnPersist);
- var engine = ApplicationEngine.Create(TriggerType.OnPersist, null, snapshot, persistingBlock, settings: TestProtocolSettings.Default);
-
- // Check that block's Primary balance is 0.
- ECPoint[] validators = NativeContract.NEO.GetNextBlockValidators(engine.SnapshotCache, engine.ProtocolSettings.ValidatorsCount);
- var primary = Contract.CreateSignatureRedeemScript(validators[engine.PersistingBlock.PrimaryIndex]).ToScriptHash();
- Assert.AreEqual(0, NativeContract.GAS.BalanceOf(engine.SnapshotCache, primary));
-
- // Execute OnPersist script.
- engine.LoadScript(script.ToArray());
- Assert.AreEqual(VMState.HALT, engine.Execute());
-
- // Check that proper amount of GAS was minted to block's Primary and the rest
- // will be minted to Notary nodes as a reward once Notary contract is implemented.
- Assert.AreEqual(2 + 1, engine.Notifications.Count()); // burn tx1 and tx2 network fee + mint primary reward
- Assert.AreEqual(netFee1 + netFee2 - expectedNotaryReward, engine.Notifications[2].State[2]);
- Assert.AreEqual(netFee1 + netFee2 - expectedNotaryReward, NativeContract.GAS.BalanceOf(engine.SnapshotCache, primary));
- }
}
}
diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs
index 5baf6575f3..e70dccf622 100644
--- a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs
+++ b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs
@@ -49,6 +49,7 @@ public void TestSetup()
{"PolicyContract", """{"id":-7,"updatecounter":0,"hash":"0xcc5e4edd9f5f8dba8bb65734541df7a1c081c67b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":588003825},"manifest":{"name":"PolicyContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"blockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":0,"safe":false},{"name":"getAttributeFee","parameters":[{"name":"attributeType","type":"Integer"}],"returntype":"Integer","offset":7,"safe":true},{"name":"getExecFeeFactor","parameters":[],"returntype":"Integer","offset":14,"safe":true},{"name":"getFeePerByte","parameters":[],"returntype":"Integer","offset":21,"safe":true},{"name":"getMaxTraceableBlocks","parameters":[],"returntype":"Integer","offset":28,"safe":true},{"name":"getMaxValidUntilBlockIncrement","parameters":[],"returntype":"Integer","offset":35,"safe":true},{"name":"getMillisecondsPerBlock","parameters":[],"returntype":"Integer","offset":42,"safe":true},{"name":"getStoragePrice","parameters":[],"returntype":"Integer","offset":49,"safe":true},{"name":"isBlocked","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":56,"safe":true},{"name":"setAttributeFee","parameters":[{"name":"attributeType","type":"Integer"},{"name":"value","type":"Integer"}],"returntype":"Void","offset":63,"safe":false},{"name":"setExecFeeFactor","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":70,"safe":false},{"name":"setFeePerByte","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":77,"safe":false},{"name":"setMaxTraceableBlocks","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":84,"safe":false},{"name":"setMaxValidUntilBlockIncrement","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"setMillisecondsPerBlock","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":98,"safe":false},{"name":"setStoragePrice","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":105,"safe":false},{"name":"unblockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":112,"safe":false}],"events":[{"name":"MillisecondsPerBlockChanged","parameters":[{"name":"old","type":"Integer"},{"name":"new","type":"Integer"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""},
{"RoleManagement", """{"id":-8,"updatecounter":0,"hash":"0x49cf4e5378ffcd4dec034fd98a174c5491e395e2","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0A=","checksum":983638438},"manifest":{"name":"RoleManagement","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"designateAsRole","parameters":[{"name":"role","type":"Integer"},{"name":"nodes","type":"Array"}],"returntype":"Void","offset":0,"safe":false},{"name":"getDesignatedByRole","parameters":[{"name":"role","type":"Integer"},{"name":"index","type":"Integer"}],"returntype":"Array","offset":7,"safe":true}],"events":[{"name":"Designation","parameters":[{"name":"Role","type":"Integer"},{"name":"BlockIndex","type":"Integer"},{"name":"Old","type":"Array"},{"name":"New","type":"Array"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""},
{"OracleContract", """{"id":-9,"updatecounter":0,"hash":"0xfe924b7cfe89ddd271abaf7210a80a7e11178758","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2663858513},"manifest":{"name":"OracleContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"finish","parameters":[],"returntype":"Void","offset":0,"safe":false},{"name":"getPrice","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"request","parameters":[{"name":"url","type":"String"},{"name":"filter","type":"String"},{"name":"callback","type":"String"},{"name":"userData","type":"Any"},{"name":"gasForResponse","type":"Integer"}],"returntype":"Void","offset":14,"safe":false},{"name":"setPrice","parameters":[{"name":"price","type":"Integer"}],"returntype":"Void","offset":21,"safe":false},{"name":"verify","parameters":[],"returntype":"Boolean","offset":28,"safe":true}],"events":[{"name":"OracleRequest","parameters":[{"name":"Id","type":"Integer"},{"name":"RequestContract","type":"Hash160"},{"name":"Url","type":"String"},{"name":"Filter","type":"String"}]},{"name":"OracleResponse","parameters":[{"name":"Id","type":"Integer"},{"name":"OriginalTx","type":"Hash256"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""},
+ {"Notary", """{"id":-10,"updatecounter":0,"hash":"0xc1e14f19c3e60d0b9244d06dd7ba9b113135ec3b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1110259869},"manifest":{"name":"Notary","groups":[],"features":{},"supportedstandards":["NEP-27"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"expirationOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":7,"safe":true},{"name":"getMaxNotValidBeforeDelta","parameters":[],"returntype":"Integer","offset":14,"safe":true},{"name":"lockDepositUntil","parameters":[{"name":"account","type":"Hash160"},{"name":"till","type":"Integer"}],"returntype":"Boolean","offset":21,"safe":false},{"name":"onNEP17Payment","parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","offset":28,"safe":false},{"name":"setMaxNotValidBeforeDelta","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":35,"safe":false},{"name":"verify","parameters":[{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":42,"safe":true},{"name":"withdraw","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"}],"returntype":"Boolean","offset":49,"safe":false}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}
};
}
diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs
new file mode 100644
index 0000000000..6474360af4
--- /dev/null
+++ b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs
@@ -0,0 +1,623 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// UT_Notary.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 FluentAssertions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Neo.Cryptography.ECC;
+using Neo.Extensions;
+using Neo.Network.P2P.Payloads;
+using Neo.Persistence;
+using Neo.SmartContract;
+using Neo.SmartContract.Native;
+using Neo.UnitTests.Extensions;
+using Neo.VM;
+using Neo.Wallets;
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Numerics;
+
+namespace Neo.UnitTests.SmartContract.Native
+{
+ [TestClass]
+ public class UT_Notary
+ {
+ private DataCache _snapshot;
+ private Block _persistingBlock;
+
+ [TestInitialize]
+ public void TestSetup()
+ {
+ _snapshot = TestBlockchain.GetTestSnapshotCache();
+ _persistingBlock = new Block { Header = new Header() };
+ }
+
+ [TestMethod]
+ public void Check_Name() => NativeContract.Notary.Name.Should().Be(nameof(Notary));
+
+ [TestMethod]
+ public void Check_OnNEP17Payment()
+ {
+ var snapshot = _snapshot.CloneCache();
+ var persistingBlock = new Block { Header = new Header { Index = 1000 } };
+ byte[] from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators).ToArray();
+ byte[] to = NativeContract.Notary.Hash.ToArray();
+
+ // Set proper current index for deposit's Till parameter check.
+ var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12);
+ snapshot.Add(storageKey, new StorageItem(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 }));
+
+ // Non-GAS transfer should fail.
+ Assert.ThrowsExactly(() => NativeContract.NEO.Transfer(snapshot, from, to, BigInteger.Zero, true, persistingBlock));
+
+ // GAS transfer with invalid data format should fail.
+ Assert.ThrowsExactly(() => NativeContract.GAS.Transfer(snapshot, from, to, BigInteger.Zero, true, persistingBlock, 5));
+
+ // GAS transfer with wrong number of data elements should fail.
+ var data = new ContractParameter { Type = ContractParameterType.Array, Value = new List() { new ContractParameter { Type = ContractParameterType.Boolean, Value = true } } };
+ Assert.ThrowsExactly(() => NativeContract.GAS.Transfer(snapshot, from, to, BigInteger.Zero, true, persistingBlock, data));
+
+ // Gas transfer with invalid Till parameter should fail.
+ data = new ContractParameter { Type = ContractParameterType.Array, Value = new List() { new ContractParameter { Type = ContractParameterType.Any }, new ContractParameter { Type = ContractParameterType.Integer, Value = persistingBlock.Index } } };
+ Assert.ThrowsExactly(() => NativeContract.GAS.TransferWithTransaction(snapshot, from, to, BigInteger.Zero, true, persistingBlock, data));
+
+ // Insufficient first deposit.
+ data = new ContractParameter { Type = ContractParameterType.Array, Value = new List() { new ContractParameter { Type = ContractParameterType.Any }, new ContractParameter { Type = ContractParameterType.Integer, Value = persistingBlock.Index + 100 } } };
+ Assert.ThrowsExactly(() => NativeContract.GAS.TransferWithTransaction(snapshot, from, to, 2 * 1000_0000 - 1, true, persistingBlock, data));
+
+ // Good deposit.
+ data = new ContractParameter { Type = ContractParameterType.Array, Value = new List() { new ContractParameter { Type = ContractParameterType.Any }, new ContractParameter { Type = ContractParameterType.Integer, Value = persistingBlock.Index + 100 } } };
+ Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, to, 2 * 1000_0000 + 1, true, persistingBlock, data));
+ }
+
+ [TestMethod]
+ public void Check_ExpirationOf()
+ {
+ var snapshot = _snapshot.CloneCache();
+ var persistingBlock = new Block { Header = new Header { Index = 1000 } };
+ byte[] from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators).ToArray();
+ byte[] ntr = NativeContract.Notary.Hash.ToArray();
+
+ // Set proper current index for deposit's Till parameter check.
+ var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12);
+ snapshot.Add(storageKey, new StorageItem(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 }));
+
+ // Check that 'till' of an empty deposit is 0 by default.
+ Call_ExpirationOf(snapshot, from, persistingBlock).Should().Be(0);
+
+ // Make initial deposit.
+ var till = persistingBlock.Index + 123;
+ var data = new ContractParameter { Type = ContractParameterType.Array, Value = new List() { new ContractParameter { Type = ContractParameterType.Any }, new ContractParameter { Type = ContractParameterType.Integer, Value = till } } };
+ Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, ntr, 2 * 1000_0000 + 1, true, persistingBlock, data));
+
+ // Ensure deposit's 'till' value is properly set.
+ Call_ExpirationOf(snapshot, from, persistingBlock).Should().Be(till);
+
+ // Make one more deposit with updated 'till' parameter.
+ till += 5;
+ data = new ContractParameter { Type = ContractParameterType.Array, Value = new List() { new ContractParameter { Type = ContractParameterType.Any }, new ContractParameter { Type = ContractParameterType.Integer, Value = till } } };
+ Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, ntr, 5, true, persistingBlock, data));
+
+ // Ensure deposit's 'till' value is properly updated.
+ Call_ExpirationOf(snapshot, from, persistingBlock).Should().Be(till);
+
+ // Make deposit to some side account with custom 'till' value.
+ UInt160 to = UInt160.Parse("01ff00ff00ff00ff00ff00ff00ff00ff00ff00a4");
+ data = new ContractParameter { Type = ContractParameterType.Array, Value = new List() { new ContractParameter { Type = ContractParameterType.Hash160, Value = to }, new ContractParameter { Type = ContractParameterType.Integer, Value = till } } };
+ Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, ntr, 2 * 1000_0000 + 1, true, persistingBlock, data));
+
+ // Default 'till' value should be set for to's deposit.
+ var defaultDeltaTill = 5760;
+ Call_ExpirationOf(snapshot, to.ToArray(), persistingBlock).Should().Be(persistingBlock.Index - 1 + defaultDeltaTill);
+
+ // Withdraw own deposit.
+ persistingBlock.Header.Index = till + 1;
+ var currentBlock = snapshot.GetAndChange(storageKey, () => new StorageItem(new HashIndexState()));
+ currentBlock.GetInteroperable().Index = till + 1;
+ Call_Withdraw(snapshot, from, from, persistingBlock);
+
+ // Check that 'till' value is properly updated.
+ Call_ExpirationOf(snapshot, from, persistingBlock).Should().Be(0);
+ }
+
+ [TestMethod]
+ public void Check_LockDepositUntil()
+ {
+ var snapshot = _snapshot.CloneCache();
+ var persistingBlock = new Block { Header = new Header { Index = 1000 } };
+ byte[] from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators).ToArray();
+
+ // Set proper current index for deposit's Till parameter check.
+ var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12);
+ snapshot.Add(storageKey, new StorageItem(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 }));
+
+ // Check that 'till' of an empty deposit is 0 by default.
+ Call_ExpirationOf(snapshot, from, persistingBlock).Should().Be(0);
+
+ // Update `till` value of an empty deposit should fail.
+ Call_LockDepositUntil(snapshot, from, 123, persistingBlock).Should().Be(false);
+ Call_ExpirationOf(snapshot, from, persistingBlock).Should().Be(0);
+
+ // Make initial deposit.
+ var till = persistingBlock.Index + 123;
+ var data = new ContractParameter { Type = ContractParameterType.Array, Value = new List() { new ContractParameter { Type = ContractParameterType.Any }, new ContractParameter { Type = ContractParameterType.Integer, Value = till } } };
+ Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, NativeContract.Notary.Hash.ToArray(), 2 * 1000_0000 + 1, true, persistingBlock, data));
+
+ // Ensure deposit's 'till' value is properly set.
+ Call_ExpirationOf(snapshot, from, persistingBlock).Should().Be(till);
+
+ // Update deposit's `till` value for side account should fail.
+ UInt160 other = UInt160.Parse("01ff00ff00ff00ff00ff00ff00ff00ff00ff00a4");
+ Call_LockDepositUntil(snapshot, other.ToArray(), till + 10, persistingBlock).Should().Be(false);
+ Call_ExpirationOf(snapshot, from, persistingBlock).Should().Be(till);
+
+ // Decrease deposit's `till` value should fail.
+ Call_LockDepositUntil(snapshot, from, till - 1, persistingBlock).Should().Be(false);
+ Call_ExpirationOf(snapshot, from, persistingBlock).Should().Be(till);
+
+ // Good.
+ till += 10;
+ Call_LockDepositUntil(snapshot, from, till, persistingBlock).Should().Be(true);
+ Call_ExpirationOf(snapshot, from, persistingBlock).Should().Be(till);
+ }
+
+ [TestMethod]
+ public void Check_BalanceOf()
+ {
+ var snapshot = _snapshot.CloneCache();
+ var persistingBlock = new Block { Header = new Header { Index = 1000 } };
+ UInt160 fromAddr = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators);
+ byte[] from = fromAddr.ToArray();
+ byte[] ntr = NativeContract.Notary.Hash.ToArray();
+
+ // Set proper current index for deposit expiration.
+ var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12);
+ snapshot.Add(storageKey, new StorageItem(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 }));
+
+ // Ensure that default deposit is 0.
+ Call_BalanceOf(snapshot, from, persistingBlock).Should().Be(0);
+
+ // Make initial deposit.
+ var till = persistingBlock.Index + 123;
+ var deposit1 = 2 * 1_0000_0000;
+ var data = new ContractParameter { Type = ContractParameterType.Array, Value = new List() { new ContractParameter { Type = ContractParameterType.Any }, new ContractParameter { Type = ContractParameterType.Integer, Value = till } } };
+ Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, ntr, deposit1, true, persistingBlock, data));
+
+ // Ensure value is deposited.
+ Call_BalanceOf(snapshot, from, persistingBlock).Should().Be(deposit1);
+
+ // Make one more deposit with updated 'till' parameter.
+ var deposit2 = 5;
+ data = new ContractParameter { Type = ContractParameterType.Array, Value = new List() { new ContractParameter { Type = ContractParameterType.Any }, new ContractParameter { Type = ContractParameterType.Integer, Value = till } } };
+ Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, ntr, deposit2, true, persistingBlock, data));
+
+ // Ensure deposit's 'till' value is properly updated.
+ Call_BalanceOf(snapshot, from, persistingBlock).Should().Be(deposit1 + deposit2);
+
+ // Make deposit to some side account.
+ UInt160 to = UInt160.Parse("01ff00ff00ff00ff00ff00ff00ff00ff00ff00a4");
+ data = new ContractParameter { Type = ContractParameterType.Array, Value = new List() { new ContractParameter { Type = ContractParameterType.Hash160, Value = to }, new ContractParameter { Type = ContractParameterType.Integer, Value = till } } };
+ Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, ntr, deposit1, true, persistingBlock, data));
+
+ Call_BalanceOf(snapshot, to.ToArray(), persistingBlock).Should().Be(deposit1);
+
+ // Process some Notary transaction and check that some deposited funds have been withdrawn.
+ var tx1 = TestUtils.GetTransaction(NativeContract.Notary.Hash, fromAddr);
+ tx1.Attributes = new TransactionAttribute[] { new NotaryAssisted() { NKeys = 4 } };
+ tx1.NetworkFee = 1_0000_0000;
+
+ // Build block to check transaction fee distribution during Gas OnPersist.
+ persistingBlock = new Block
+ {
+ Header = new Header
+ {
+ Index = (uint)TestProtocolSettings.Default.CommitteeMembersCount,
+ MerkleRoot = UInt256.Zero,
+ NextConsensus = UInt160.Zero,
+ PrevHash = UInt256.Zero,
+ Witness = new Witness() { InvocationScript = Array.Empty(), VerificationScript = Array.Empty() }
+ },
+ Transactions = new Transaction[] { tx1 }
+ };
+ // Designate Notary node.
+ byte[] privateKey1 = new byte[32];
+ var rng = System.Security.Cryptography.RandomNumberGenerator.Create();
+ rng.GetBytes(privateKey1);
+ KeyPair key1 = new KeyPair(privateKey1);
+ UInt160 committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot);
+ var ret = NativeContract.RoleManagement.Call(
+ snapshot,
+ new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr),
+ new Block { Header = new Header() },
+ "designateAsRole",
+ new ContractParameter(ContractParameterType.Integer) { Value = new BigInteger((int)Role.P2PNotary) },
+ new ContractParameter(ContractParameterType.Array)
+ {
+ Value = new List(){
+ new ContractParameter(ContractParameterType.ByteArray){Value = key1.PublicKey.ToArray()},
+ }
+ }
+ );
+ snapshot.Commit();
+
+ // Execute OnPersist script.
+ var script = new ScriptBuilder();
+ script.EmitSysCall(ApplicationEngine.System_Contract_NativeOnPersist);
+ var engine = ApplicationEngine.Create(TriggerType.OnPersist, null, snapshot, persistingBlock, settings: TestProtocolSettings.Default);
+ engine.LoadScript(script.ToArray());
+ Assert.IsTrue(engine.Execute() == VMState.HALT);
+ snapshot.Commit();
+
+ // Check that transaction's fees were paid by from's deposit.
+ Call_BalanceOf(snapshot, from, persistingBlock).Should().Be(deposit1 + deposit2 - tx1.NetworkFee - tx1.SystemFee);
+
+ // Withdraw own deposit.
+ persistingBlock.Header.Index = till + 1;
+ var currentBlock = snapshot.GetAndChange(storageKey, () => new StorageItem(new HashIndexState()));
+ currentBlock.GetInteroperable().Index = till + 1;
+ Call_Withdraw(snapshot, from, from, persistingBlock);
+
+ // Check that no deposit is left.
+ Call_BalanceOf(snapshot, from, persistingBlock).Should().Be(0);
+ }
+
+ [TestMethod]
+ public void Check_Withdraw()
+ {
+ var snapshot = _snapshot.CloneCache();
+ var persistingBlock = new Block { Header = new Header { Index = 1000 } };
+ UInt160 fromAddr = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators);
+ byte[] from = fromAddr.ToArray();
+
+ // Set proper current index to get proper deposit expiration height.
+ var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12);
+ snapshot.Add(storageKey, new StorageItem(new HashIndexState { Hash = UInt256.Zero, Index = persistingBlock.Index - 1 }));
+
+ // Ensure that default deposit is 0.
+ Call_BalanceOf(snapshot, from, persistingBlock).Should().Be(0);
+
+ // Make initial deposit.
+ var till = persistingBlock.Index + 123;
+ var deposit1 = 2 * 1_0000_0000;
+ var data = new ContractParameter { Type = ContractParameterType.Array, Value = new List() { new ContractParameter { Type = ContractParameterType.Any }, new ContractParameter { Type = ContractParameterType.Integer, Value = till } } };
+ Assert.IsTrue(NativeContract.GAS.TransferWithTransaction(snapshot, from, NativeContract.Notary.Hash.ToArray(), deposit1, true, persistingBlock, data));
+
+ // Ensure value is deposited.
+ Call_BalanceOf(snapshot, from, persistingBlock).Should().Be(deposit1);
+
+ // Unwitnessed withdraw should fail.
+ UInt160 sideAccount = UInt160.Parse("01ff00ff00ff00ff00ff00ff00ff00ff00ff00a4");
+ Call_Withdraw(snapshot, from, sideAccount.ToArray(), persistingBlock, false).Should().Be(false);
+
+ // Withdraw missing (zero) deposit should fail.
+ Call_Withdraw(snapshot, sideAccount.ToArray(), sideAccount.ToArray(), persistingBlock).Should().Be(false);
+
+ // Withdraw before deposit expiration should fail.
+ Call_Withdraw(snapshot, from, from, persistingBlock).Should().Be(false);
+
+ // Good.
+ persistingBlock.Header.Index = till + 1;
+ var currentBlock = snapshot.GetAndChange(storageKey, () => new StorageItem(new HashIndexState()));
+ currentBlock.GetInteroperable().Index = till + 1;
+ Call_Withdraw(snapshot, from, from, persistingBlock).Should().Be(true);
+
+ // Check that no deposit is left.
+ Call_BalanceOf(snapshot, from, persistingBlock).Should().Be(0);
+ }
+
+ internal static BigInteger Call_BalanceOf(DataCache snapshot, byte[] address, Block persistingBlock)
+ {
+ using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, persistingBlock, settings: TestProtocolSettings.Default);
+
+ using var script = new ScriptBuilder();
+ script.EmitDynamicCall(NativeContract.Notary.Hash, "balanceOf", address);
+ engine.LoadScript(script.ToArray());
+
+ engine.Execute().Should().Be(VMState.HALT);
+
+ var result = engine.ResultStack.Pop();
+ result.Should().BeOfType(typeof(VM.Types.Integer));
+
+ return result.GetInteger();
+ }
+
+ internal static BigInteger Call_ExpirationOf(DataCache snapshot, byte[] address, Block persistingBlock)
+ {
+ using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, persistingBlock, settings: TestProtocolSettings.Default);
+
+ using var script = new ScriptBuilder();
+ script.EmitDynamicCall(NativeContract.Notary.Hash, "expirationOf", address);
+ engine.LoadScript(script.ToArray());
+
+ engine.Execute().Should().Be(VMState.HALT);
+
+ var result = engine.ResultStack.Pop();
+ result.Should().BeOfType(typeof(VM.Types.Integer));
+
+ return result.GetInteger();
+ }
+
+ internal static bool Call_LockDepositUntil(DataCache snapshot, byte[] address, uint till, Block persistingBlock)
+ {
+ using var engine = ApplicationEngine.Create(TriggerType.Application, new Transaction() { Signers = new Signer[] { new Signer() { Account = new UInt160(address), Scopes = WitnessScope.Global } }, Attributes = System.Array.Empty() }, snapshot, persistingBlock, settings: TestProtocolSettings.Default);
+
+ using var script = new ScriptBuilder();
+ script.EmitDynamicCall(NativeContract.Notary.Hash, "lockDepositUntil", address, till);
+ engine.LoadScript(script.ToArray());
+
+ engine.Execute().Should().Be(VMState.HALT);
+
+ var result = engine.ResultStack.Pop();
+ result.Should().BeOfType(typeof(VM.Types.Boolean));
+
+ return result.GetBoolean();
+ }
+
+ internal static bool Call_Withdraw(DataCache snapshot, byte[] from, byte[] to, Block persistingBlock, bool witnessedByFrom = true)
+ {
+ var accFrom = UInt160.Zero;
+ if (witnessedByFrom)
+ {
+ accFrom = new UInt160(from);
+ }
+ using var engine = ApplicationEngine.Create(TriggerType.Application, new Transaction() { Signers = new Signer[] { new Signer() { Account = accFrom, Scopes = WitnessScope.Global } }, Attributes = System.Array.Empty() }, snapshot, persistingBlock, settings: TestProtocolSettings.Default);
+
+ using var script = new ScriptBuilder();
+ script.EmitDynamicCall(NativeContract.Notary.Hash, "withdraw", from, to);
+ engine.LoadScript(script.ToArray());
+
+ if (engine.Execute() != VMState.HALT)
+ {
+ throw engine.FaultException;
+ }
+
+ var result = engine.ResultStack.Pop();
+ result.Should().BeOfType(typeof(VM.Types.Boolean));
+
+ return result.GetBoolean();
+ }
+
+ [TestMethod]
+ public void Check_GetMaxNotValidBeforeDelta()
+ {
+ const int defaultMaxNotValidBeforeDelta = 140;
+ NativeContract.Notary.GetMaxNotValidBeforeDelta(_snapshot).Should().Be(defaultMaxNotValidBeforeDelta);
+ }
+
+ [TestMethod]
+ public void Check_SetMaxNotValidBeforeDelta()
+ {
+ var snapshot = _snapshot.CloneCache();
+ var persistingBlock = new Block { Header = new Header { Index = 1000 } };
+ UInt160 committeeAddress = NativeContract.NEO.GetCommitteeAddress(snapshot);
+
+ using var engine = ApplicationEngine.Create(TriggerType.Application, new Nep17NativeContractExtensions.ManualWitness(committeeAddress), snapshot, persistingBlock, settings: TestProtocolSettings.Default);
+ using var script = new ScriptBuilder();
+ script.EmitDynamicCall(NativeContract.Notary.Hash, "setMaxNotValidBeforeDelta", 100);
+ engine.LoadScript(script.ToArray());
+ VMState vMState = engine.Execute();
+ vMState.Should().Be(VMState.HALT);
+ NativeContract.Notary.GetMaxNotValidBeforeDelta(snapshot).Should().Be(100);
+ }
+
+ [TestMethod]
+ public void Check_OnPersist_FeePerKeyUpdate()
+ {
+ // Hardcode test values.
+ const uint defaultNotaryAssistedFeePerKey = 1000_0000;
+ const uint newNotaryAssistedFeePerKey = 5000_0000;
+ const byte NKeys = 4;
+
+ // Generate one transaction with NotaryAssisted attribute with hardcoded NKeys values.
+ var from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators);
+ var tx2 = TestUtils.GetTransaction(from);
+ tx2.Attributes = new TransactionAttribute[] { new NotaryAssisted() { NKeys = NKeys } };
+ var netFee = 1_0000_0000; // enough to cover defaultNotaryAssistedFeePerKey, but not enough to cover newNotaryAssistedFeePerKey.
+ tx2.NetworkFee = netFee;
+ tx2.SystemFee = 1000_0000;
+
+ // Calculate overall expected Notary nodes reward.
+ var expectedNotaryReward = (NKeys + 1) * defaultNotaryAssistedFeePerKey;
+
+ // Build block to check transaction fee distribution during Gas OnPersist.
+ var persistingBlock = new Block
+ {
+ Header = new Header
+ {
+ Index = 10,
+ MerkleRoot = UInt256.Zero,
+ NextConsensus = UInt160.Zero,
+ PrevHash = UInt256.Zero,
+ Witness = new Witness() { InvocationScript = Array.Empty(), VerificationScript = Array.Empty() }
+ },
+ Transactions = new Transaction[] { tx2 }
+ };
+ var snapshot = _snapshot.CloneCache();
+
+ // Designate Notary node.
+ byte[] privateKey1 = new byte[32];
+ var rng = System.Security.Cryptography.RandomNumberGenerator.Create();
+ rng.GetBytes(privateKey1);
+ KeyPair key1 = new KeyPair(privateKey1);
+ UInt160 committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot);
+ var ret = NativeContract.RoleManagement.Call(
+ snapshot,
+ new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr),
+ new Block { Header = new Header() },
+ "designateAsRole",
+ new ContractParameter(ContractParameterType.Integer) { Value = new BigInteger((int)Role.P2PNotary) },
+ new ContractParameter(ContractParameterType.Array)
+ {
+ Value = new List(){
+ new ContractParameter(ContractParameterType.ByteArray){Value = key1.PublicKey.ToArray()}
+ }
+ }
+ );
+ snapshot.Commit();
+
+ // Create engine with custom settings (HF_Echidna should be enabled to properly interact with NotaryAssisted attribute).
+ var settings = ProtocolSettings.Default with
+ {
+ Network = 0x334F454Eu,
+ StandbyCommittee =
+ [
+ ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1),
+ ECPoint.Parse("02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093", ECCurve.Secp256r1),
+ ECPoint.Parse("03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a", ECCurve.Secp256r1),
+ ECPoint.Parse("02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554", ECCurve.Secp256r1),
+ ECPoint.Parse("024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d", ECCurve.Secp256r1),
+ ECPoint.Parse("02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e", ECCurve.Secp256r1),
+ ECPoint.Parse("02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70", ECCurve.Secp256r1)
+ ],
+ ValidatorsCount = 7,
+ Hardforks = new Dictionary{
+ {Hardfork.HF_Aspidochelone, 1},
+ {Hardfork.HF_Basilisk, 2},
+ {Hardfork.HF_Cockatrice, 3},
+ {Hardfork.HF_Domovoi, 4},
+ {Hardfork.HF_Echidna, 5}
+ }.ToImmutableDictionary()
+ };
+
+ // Imitate Blockchain's Persist behaviour: OnPersist + transactions processing.
+ // Execute OnPersist firstly:
+ var script = new ScriptBuilder();
+ script.EmitSysCall(ApplicationEngine.System_Contract_NativeOnPersist);
+ var engine = ApplicationEngine.Create(TriggerType.OnPersist, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), snapshot, persistingBlock, settings: settings);
+ engine.LoadScript(script.ToArray());
+ Assert.IsTrue(engine.Execute() == VMState.HALT, engine.FaultException?.ToString());
+ snapshot.Commit();
+
+ // Process transaction that changes NotaryServiceFeePerKey after OnPersist.
+ ret = NativeContract.Policy.Call(engine,
+ "setAttributeFee", new ContractParameter(ContractParameterType.Integer) { Value = (BigInteger)(byte)TransactionAttributeType.NotaryAssisted }, new ContractParameter(ContractParameterType.Integer) { Value = newNotaryAssistedFeePerKey });
+ Assert.IsNull(ret);
+ snapshot.Commit();
+
+ // Process tx2 with NotaryAssisted attribute.
+ engine = ApplicationEngine.Create(TriggerType.Application, tx2, snapshot, persistingBlock, settings: TestProtocolSettings.Default, tx2.SystemFee);
+ engine.LoadScript(tx2.Script);
+ Assert.IsTrue(engine.Execute() == VMState.HALT);
+ snapshot.Commit();
+
+ // Ensure that Notary reward is distributed based on the old value of NotaryAssisted price
+ // and no underflow happens during GAS distribution.
+ ECPoint[] validators = NativeContract.NEO.GetNextBlockValidators(engine.SnapshotCache, engine.ProtocolSettings.ValidatorsCount);
+ var primary = Contract.CreateSignatureRedeemScript(validators[engine.PersistingBlock.PrimaryIndex]).ToScriptHash();
+ NativeContract.GAS.BalanceOf(snapshot, primary).Should().Be(netFee - expectedNotaryReward);
+ NativeContract.GAS.BalanceOf(engine.SnapshotCache, Contract.CreateSignatureRedeemScript(key1.PublicKey).ToScriptHash()).Should().Be(expectedNotaryReward);
+ }
+
+ [TestMethod]
+ public void Check_OnPersist_NotaryRewards()
+ {
+ // Hardcode test values.
+ const uint defaultNotaryssestedFeePerKey = 1000_0000;
+ const byte NKeys1 = 4;
+ const byte NKeys2 = 6;
+
+ // Generate two transactions with NotaryAssisted attributes with hardcoded NKeys values.
+ var from = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators);
+ var tx1 = TestUtils.GetTransaction(from);
+ tx1.Attributes = new TransactionAttribute[] { new NotaryAssisted() { NKeys = NKeys1 } };
+ var netFee1 = 1_0000_0000;
+ tx1.NetworkFee = netFee1;
+ var tx2 = TestUtils.GetTransaction(from);
+ tx2.Attributes = new TransactionAttribute[] { new NotaryAssisted() { NKeys = NKeys2 } };
+ var netFee2 = 2_0000_0000;
+ tx2.NetworkFee = netFee2;
+
+ // Calculate overall expected Notary nodes reward.
+ var expectedNotaryReward = (NKeys1 + 1) * defaultNotaryssestedFeePerKey + (NKeys2 + 1) * defaultNotaryssestedFeePerKey;
+
+ // Build block to check transaction fee distribution during Gas OnPersist.
+ var persistingBlock = new Block
+ {
+ Header = new Header
+ {
+ Index = (uint)TestProtocolSettings.Default.CommitteeMembersCount,
+ MerkleRoot = UInt256.Zero,
+ NextConsensus = UInt160.Zero,
+ PrevHash = UInt256.Zero,
+ Witness = new Witness() { InvocationScript = Array.Empty(), VerificationScript = Array.Empty() }
+ },
+ Transactions = new Transaction[] { tx1, tx2 }
+ };
+ var snapshot = _snapshot.CloneCache();
+
+ // Designate several Notary nodes.
+ byte[] privateKey1 = new byte[32];
+ var rng = System.Security.Cryptography.RandomNumberGenerator.Create();
+ rng.GetBytes(privateKey1);
+ KeyPair key1 = new KeyPair(privateKey1);
+ byte[] privateKey2 = new byte[32];
+ rng.GetBytes(privateKey2);
+ KeyPair key2 = new KeyPair(privateKey2);
+ UInt160 committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot);
+ var ret = NativeContract.RoleManagement.Call(
+ snapshot,
+ new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr),
+ new Block { Header = new Header() },
+ "designateAsRole",
+ new ContractParameter(ContractParameterType.Integer) { Value = new BigInteger((int)Role.P2PNotary) },
+ new ContractParameter(ContractParameterType.Array)
+ {
+ Value = new List(){
+ new ContractParameter(ContractParameterType.ByteArray){Value = key1.PublicKey.ToArray()},
+ new ContractParameter(ContractParameterType.ByteArray){Value = key2.PublicKey.ToArray()},
+ }
+ }
+ );
+ snapshot.Commit();
+
+ var script = new ScriptBuilder();
+ script.EmitSysCall(ApplicationEngine.System_Contract_NativeOnPersist);
+ var engine = ApplicationEngine.Create(TriggerType.OnPersist, null, snapshot, persistingBlock, settings: TestProtocolSettings.Default);
+
+ // Check that block's Primary balance is 0.
+ ECPoint[] validators = NativeContract.NEO.GetNextBlockValidators(engine.SnapshotCache, engine.ProtocolSettings.ValidatorsCount);
+ var primary = Contract.CreateSignatureRedeemScript(validators[engine.PersistingBlock.PrimaryIndex]).ToScriptHash();
+ NativeContract.GAS.BalanceOf(engine.SnapshotCache, primary).Should().Be(0);
+
+ // Execute OnPersist script.
+ engine.LoadScript(script.ToArray());
+ Assert.IsTrue(engine.Execute() == VMState.HALT);
+
+ // Check that proper amount of GAS was minted to block's Primary and the rest
+ // is evenly devided between designated Notary nodes as a reward.
+ Assert.AreEqual(2 + 1 + 2, engine.Notifications.Count()); // burn tx1 and tx2 network fee + mint primary reward + transfer reward to Notary1 and Notary2
+ Assert.AreEqual(netFee1 + netFee2 - expectedNotaryReward, engine.Notifications[2].State[2]);
+ NativeContract.GAS.BalanceOf(engine.SnapshotCache, primary).Should().Be(netFee1 + netFee2 - expectedNotaryReward);
+ Assert.AreEqual(expectedNotaryReward / 2, engine.Notifications[3].State[2]);
+ NativeContract.GAS.BalanceOf(engine.SnapshotCache, Contract.CreateSignatureRedeemScript(key1.PublicKey).ToScriptHash()).Should().Be(expectedNotaryReward / 2);
+ Assert.AreEqual(expectedNotaryReward / 2, engine.Notifications[4].State[2]);
+ NativeContract.GAS.BalanceOf(engine.SnapshotCache, Contract.CreateSignatureRedeemScript(key2.PublicKey).ToScriptHash()).Should().Be(expectedNotaryReward / 2);
+ }
+
+ internal static StorageKey CreateStorageKey(byte prefix, uint key)
+ {
+ return CreateStorageKey(prefix, BitConverter.GetBytes(key));
+ }
+
+ internal static StorageKey CreateStorageKey(byte prefix, byte[] key = null)
+ {
+ byte[] buffer = GC.AllocateUninitializedArray(sizeof(byte) + (key?.Length ?? 0));
+ buffer[0] = prefix;
+ key?.CopyTo(buffer.AsSpan(1));
+ return new()
+ {
+ Id = NativeContract.GAS.Id,
+ Key = buffer
+ };
+ }
+ }
+}
diff --git a/tests/Neo.UnitTests/TestUtils.Transaction.cs b/tests/Neo.UnitTests/TestUtils.Transaction.cs
index 8ebdeb3a58..8b305a370f 100644
--- a/tests/Neo.UnitTests/TestUtils.Transaction.cs
+++ b/tests/Neo.UnitTests/TestUtils.Transaction.cs
@@ -115,6 +115,47 @@ public static Transaction GetTransaction(UInt160 sender)
};
}
+ public static Transaction GetTransaction(UInt160 sender, UInt160 signer)
+ {
+ return new Transaction
+ {
+ Script = new[] { (byte)OpCode.PUSH2 },
+ Attributes = [],
+ Signers =
+ [
+ new Signer
+ {
+ Account = sender,
+ Scopes = WitnessScope.CalledByEntry,
+ AllowedContracts = [],
+ AllowedGroups = [],
+ Rules = [],
+ },
+ new Signer
+ {
+ Account = signer,
+ Scopes = WitnessScope.CalledByEntry,
+ AllowedContracts = [],
+ AllowedGroups = [],
+ Rules = [],
+ }
+ ],
+ Witnesses =
+ [
+ new Witness
+ {
+ InvocationScript = Memory.Empty,
+ VerificationScript = Memory.Empty,
+ },
+ new Witness
+ {
+ InvocationScript = Memory.Empty,
+ VerificationScript = Memory.Empty,
+ }
+ ]
+ };
+ }
+
public static Transaction CreateInvalidTransaction(DataCache snapshot, NEP6Wallet wallet, WalletAccount account, InvalidTransactionType type, UInt256 conflict = null)
{
var rand = new Random();