diff --git a/src/Neo.CLI/CLI/MainService.Blockchain.cs b/src/Neo.CLI/CLI/MainService.Blockchain.cs
index 4f896d63e3..9f5d40daa7 100644
--- a/src/Neo.CLI/CLI/MainService.Blockchain.cs
+++ b/src/Neo.CLI/CLI/MainService.Blockchain.cs
@@ -212,6 +212,10 @@ public void OnShowTransactionCommand(UInt256 hash)
ConsoleHelper.Info("", " Type: ", $"{n.Type}");
ConsoleHelper.Info("", " Height: ", $"{n.Height}");
break;
+ case NotaryAssisted n:
+ ConsoleHelper.Info("", " Type: ", $"{n.Type}");
+ ConsoleHelper.Info("", " NKeys: ", $"{n.NKeys}");
+ break;
default:
ConsoleHelper.Info("", " Type: ", $"{attribute.Type}");
ConsoleHelper.Info("", " Size: ", $"{attribute.Size} Byte(s)");
diff --git a/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs
new file mode 100644
index 0000000000..5c115a9f0e
--- /dev/null
+++ b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs
@@ -0,0 +1,71 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// NotaryAssisted.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.IO;
+using Neo.Json;
+using Neo.Persistence;
+using Neo.SmartContract.Native;
+using System.IO;
+using System.Linq;
+
+namespace Neo.Network.P2P.Payloads
+{
+ public class NotaryAssisted : TransactionAttribute
+ {
+ ///
+ /// Indicates the number of keys participating in the transaction (main or fallback) signing process.
+ ///
+ public byte NKeys;
+
+ public override TransactionAttributeType Type => TransactionAttributeType.NotaryAssisted;
+
+ public override bool AllowMultiple => false;
+
+ public override int Size => base.Size + sizeof(byte);
+
+ protected override void DeserializeWithoutType(ref MemoryReader reader)
+ {
+ NKeys = reader.ReadByte();
+ }
+
+ protected override void SerializeWithoutType(BinaryWriter writer)
+ {
+ writer.Write(NKeys);
+ }
+
+ public override JObject ToJson()
+ {
+ JObject json = base.ToJson();
+ json["nkeys"] = NKeys;
+ return json;
+ }
+
+ public override bool Verify(DataCache snapshot, Transaction tx)
+ {
+ return tx.Signers.Any(p => p.Account.Equals(NativeContract.Notary.Hash));
+ }
+
+ ///
+ /// Calculates the network fee needed to pay for NotaryAssisted attribute. According to the
+ /// https://github.com/neo-project/neo/issues/1573#issuecomment-704874472, network fee consists of
+ /// the base Notary service fee per key multiplied by the expected number of transactions that should
+ /// be collected by the service to complete Notary request increased by one (for Notary node witness
+ /// itself).
+ ///
+ /// The snapshot used to read data.
+ /// The transaction to calculate.
+ /// The network fee of the NotaryAssisted attribute.
+ public override long CalculateNetworkFee(DataCache snapshot, Transaction tx)
+ {
+ return (NKeys + 1) * base.CalculateNetworkFee(snapshot, tx);
+ }
+ }
+}
diff --git a/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs b/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs
index 116f136c07..7e7f4f4252 100644
--- a/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs
+++ b/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs
@@ -40,6 +40,12 @@ public enum TransactionAttributeType : byte
/// Indicates that the transaction conflicts with .
///
[ReflectionCache(typeof(Conflicts))]
- Conflicts = 0x21
+ Conflicts = 0x21,
+
+ ///
+ /// Indicates that the transaction uses notary request service with number of keys.
+ ///
+ [ReflectionCache(typeof(NotaryAssisted))]
+ NotaryAssisted = 0x22
}
}
diff --git a/src/Neo/SmartContract/Native/GasToken.cs b/src/Neo/SmartContract/Native/GasToken.cs
index b8a185b6f1..61c6180e9f 100644
--- a/src/Neo/SmartContract/Native/GasToken.cs
+++ b/src/Neo/SmartContract/Native/GasToken.cs
@@ -43,6 +43,14 @@ internal override async ContractTask OnPersistAsync(ApplicationEngine engine)
{
await Burn(engine, tx.Sender, tx.SystemFee + tx.NetworkFee);
totalNetworkFee += tx.NetworkFee;
+
+ // Reward for NotaryAssisted attribute will be minted to designated notary nodes
+ // by Notary contract.
+ var notaryAssisted = tx.GetAttribute();
+ if (notaryAssisted is not null)
+ {
+ totalNetworkFee -= (notaryAssisted.NKeys + 1) * Policy.GetAttributeFee(engine.Snapshot, (byte)notaryAssisted.Type);
+ }
}
ECPoint[] validators = NEO.GetNextBlockValidators(engine.Snapshot, engine.ProtocolSettings.ValidatorsCount);
UInt160 primary = Contract.CreateSignatureRedeemScript(validators[engine.PersistingBlock.PrimaryIndex]).ToScriptHash();
diff --git a/src/Neo/SmartContract/Native/NativeContract.cs b/src/Neo/SmartContract/Native/NativeContract.cs
index 03815b3fae..2b0c86d9d2 100644
--- a/src/Neo/SmartContract/Native/NativeContract.cs
+++ b/src/Neo/SmartContract/Native/NativeContract.cs
@@ -103,6 +103,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..949a19cd3c
--- /dev/null
+++ b/src/Neo/SmartContract/Native/Notary.cs
@@ -0,0 +1,322 @@
+// Copyright (C) 2015-2024 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.
+
+#pragma warning disable IDE0051
+
+using Neo.Cryptography;
+using Neo.Cryptography.ECC;
+using Neo.IO;
+using Neo.Network.P2P;
+using Neo.Network.P2P.Payloads;
+using Neo.Persistence;
+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() { }
+
+ internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardfork)
+ {
+ if (hardfork == ActiveIn)
+ {
+ engine.Snapshot.Add(CreateStorageKey(Prefix_MaxNotValidBeforeDelta), new StorageItem(DefaultMaxNotValidBeforeDelta));
+ }
+ return ContractTask.CompletedTask;
+ }
+
+ internal override async ContractTask OnPersistAsync(ApplicationEngine engine)
+ {
+ long nFees = 0;
+ ECPoint[] notaries = null;
+ foreach (Transaction tx in engine.PersistingBlock.Transactions)
+ {
+ var attr = tx.GetAttribute();
+ if (attr is not null)
+ {
+ notaries ??= GetNotaryNodes(engine.Snapshot);
+ var nKeys = attr.NKeys;
+ nFees += (long)nKeys + 1;
+ if (tx.Sender == Hash)
+ {
+ var payer = tx.Signers[1];
+ var balance = engine.Snapshot.GetAndChange(CreateStorageKey(Prefix_Deposit).Add(payer.Account.ToArray()))?.GetInteroperable();
+ balance.Amount -= tx.SystemFee + tx.NetworkFee;
+ if (balance.Amount.Sign == 0) RemoveDepositFor(engine.Snapshot, payer.Account);
+ }
+ }
+ }
+ if (nFees == 0) return;
+ var singleReward = CalculateNotaryReward(engine.Snapshot, nFees, notaries.Length);
+ foreach (var notary in notaries) await GAS.Mint(engine, notary.EncodePoint(true).ToScriptHash(), singleReward, false);
+ }
+
+ ///
+ /// 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[] sig)
+ {
+ Transaction tx = (Transaction)engine.ScriptContainer;
+ 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.Snapshot, payer);
+ if (balance is null || balance.Amount.CompareTo(tx.NetworkFee + tx.SystemFee) < 0) return false;
+ }
+ ECPoint[] notaries = GetNotaryNodes(engine.Snapshot);
+ var hash = tx.GetSignData(engine.GetNetwork());
+ return notaries.Any(n => Crypto.VerifySignature(hash, sig, 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.All)]
+ 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()));
+ var to = from;
+ var additionalParams = (Array)data;
+ if (additionalParams.Count() != 2) throw new FormatException("`data` parameter should be an array of 2 elements");
+ 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.Snapshot);
+ Deposit deposit = engine.Snapshot.GetAndChange(CreateStorageKey(Prefix_Deposit).Add(to.ToArray()))?.GetInteroperable();
+ if (till < currentHeight + 2) throw new ArgumentOutOfRangeException(string.Format("`till` shouldn't be less than the chain's height {0} + 1", currentHeight + 2));
+ 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.GetAttributeFee(engine.Snapshot, (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 addr, uint till)
+ {
+ if (!engine.CheckWitnessInternal(addr)) return false;
+ if (till < Ledger.CurrentIndex(engine.Snapshot)) return false;
+ Deposit deposit = GetDepositFor(engine.Snapshot, addr);
+ if (deposit is null) return false;
+ if (till < deposit.Till) return false;
+ deposit.Till = till;
+
+ PutDepositFor(engine, addr, 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 acc)
+ {
+ Deposit deposit = GetDepositFor(snapshot, acc);
+ 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 acc)
+ {
+ Deposit deposit = GetDepositFor(snapshot, acc);
+ 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.States)]
+ private async ContractTask Withdraw(ApplicationEngine engine, UInt160 from, UInt160 to)
+ {
+ if (!engine.CheckWitnessInternal(from)) throw new InvalidOperationException(string.Format("Failed to check witness for {0}", from.ToString()));
+ var receive = to is null ? from : to;
+ var deposit = GetDepositFor(engine.Snapshot, from) ?? throw new InvalidOperationException(string.Format("Deposit of {0} is null", from.ToString()));
+ if (Ledger.CurrentIndex(engine.Snapshot) < deposit.Till) throw new InvalidOperationException(string.Format("Can't withdraw before {0}", deposit.Till));
+ RemoveDepositFor(engine.Snapshot, 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(DataCache 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)
+ {
+ if (value > engine.ProtocolSettings.MaxValidUntilBlockIncrement / 2 || value < ProtocolSettings.Default.ValidatorsCount) throw new FormatException(string.Format("MaxNotValidBeforeDelta cannot be more than {0} or less than {1}", engine.ProtocolSettings.MaxValidUntilBlockIncrement / 2, ProtocolSettings.Default.ValidatorsCount));
+ if (!CheckCommittee(engine)) throw new InvalidOperationException();
+ engine.Snapshot.GetAndChange(CreateStorageKey(Prefix_MaxNotValidBeforeDelta)).Set(value);
+ }
+
+ ///
+ /// GetNotaryNodes returns public keys of notary nodes.
+ ///
+ /// DataCache
+ /// Public keys of notary nodes.
+ private ECPoint[] GetNotaryNodes(DataCache snapshot)
+ {
+ return RoleManagement.GetDesignatedByRole(snapshot, Role.P2PNotary, Ledger.CurrentIndex(snapshot) + 1);
+ }
+
+ ///
+ /// GetDepositFor returns state.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).Add(acc.ToArray()))?.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)
+ {
+ var indeposit = engine.Snapshot.GetAndChange(CreateStorageKey(Prefix_Deposit).Add(acc.ToArray()), () => 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).Add(acc.ToArray()));
+ }
+
+ ///
+ /// CalculateNotaryReward calculates the reward for a single notary node based on FEE's count and Notary nodes count.
+ ///
+ /// DataCache
+ ///
+ ///
+ /// result
+ private long CalculateNotaryReward(DataCache snapshot, long nFees, int notariesCount)
+ {
+ return (nFees * Policy.GetAttributeFee(snapshot, (byte)TransactionAttributeType.NotaryAssisted)) / notariesCount;
+ }
+
+ public class Deposit : IInteroperable
+ {
+ public BigInteger Amount;
+ public uint Till;
+
+ public void FromStackItem(StackItem stackItem)
+ {
+ Struct @struct = (Struct)stackItem;
+ Amount = @struct[0].GetInteger();
+ Till = (uint)@struct[1].GetInteger();
+ }
+
+ public StackItem ToStackItem(ReferenceCounter referenceCounter)
+ {
+ return new Struct(referenceCounter) { Amount, Till };
+ }
+ }
+ }
+}
diff --git a/src/Neo/SmartContract/Native/PolicyContract.cs b/src/Neo/SmartContract/Native/PolicyContract.cs
index 7b921d191f..aea3a3ab06 100644
--- a/src/Neo/SmartContract/Native/PolicyContract.cs
+++ b/src/Neo/SmartContract/Native/PolicyContract.cs
@@ -43,6 +43,11 @@ public sealed class PolicyContract : NativeContract
///
public const uint DefaultAttributeFee = 0;
+ ///
+ /// The default fee for NotaryAssisted attribute.
+ ///
+ public const uint DefaultNotaryAssistedAttributeFee = 1000_0000;
+
///
/// The maximum execution fee factor that the committee can set.
///
@@ -73,6 +78,7 @@ internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfor
engine.Snapshot.Add(CreateStorageKey(Prefix_FeePerByte), new StorageItem(DefaultFeePerByte));
engine.Snapshot.Add(CreateStorageKey(Prefix_ExecFeeFactor), new StorageItem(DefaultExecFeeFactor));
engine.Snapshot.Add(CreateStorageKey(Prefix_StoragePrice), new StorageItem(DefaultStoragePrice));
+ engine.Snapshot.Add(CreateStorageKey(Prefix_AttributeFee).Add((byte)TransactionAttributeType.NotaryAssisted), new StorageItem(DefaultNotaryAssistedAttributeFee));
}
return ContractTask.CompletedTask;
}
diff --git a/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs b/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs
index a3f33f26a7..6e11555848 100644
--- a/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs
+++ b/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs
@@ -16,6 +16,7 @@
using Neo.SmartContract;
using Neo.SmartContract.Native;
using Neo.VM;
+using Neo.VM.Types;
using System.IO;
using System.Numerics;
@@ -48,12 +49,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: TestBlockchain.TheNeoSystem.Settings);
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();
+ result.Should().BeOfType(typeof(VM.Types.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 Signer[] { new Signer() { Account = signFrom ? new UInt160(from) : null, Scopes = WitnessScope.Global } }, Attributes = System.Array.Empty() },
+ snapshot, persistingBlock, settings: TestBlockchain.TheNeoSystem.Settings);
+
+ 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/Network/P2P/Payloads/UT_NotaryAssisted.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs
new file mode 100644
index 0000000000..778fe7a53d
--- /dev/null
+++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs
@@ -0,0 +1,91 @@
+// Copyright (C) 2015-2024 The Neo Project.
+//
+// UT_NotaryAssisted.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.IO;
+using Neo.Network.P2P.Payloads;
+using Neo.SmartContract;
+using Neo.SmartContract.Native;
+using Neo.VM;
+using System;
+
+namespace Neo.UnitTests.Network.P2P.Payloads
+{
+ [TestClass]
+ public class UT_NotaryAssisted
+ {
+ // Use the hard-coded Notary hash value from NeoGo to ensure hashes are compatible.
+ private static readonly UInt160 notaryHash = UInt160.Parse("0xc1e14f19c3e60d0b9244d06dd7ba9b113135ec3b");
+
+ [TestMethod]
+ public void Size_Get()
+ {
+ var attr = new NotaryAssisted() { NKeys = 4 };
+ attr.Size.Should().Be(1 + 1);
+ }
+
+ [TestMethod]
+ public void ToJson()
+ {
+ var attr = new NotaryAssisted() { NKeys = 4 };
+ var json = attr.ToJson().ToString();
+ Assert.AreEqual(@"{""type"":""NotaryAssisted"",""nkeys"":4}", json);
+ }
+
+ [TestMethod]
+ public void DeserializeAndSerialize()
+ {
+ var attr = new NotaryAssisted() { NKeys = 4 };
+
+ var clone = attr.ToArray().AsSerializable();
+ Assert.AreEqual(clone.Type, attr.Type);
+
+ // As transactionAttribute
+ byte[] buffer = attr.ToArray();
+ var reader = new MemoryReader(buffer);
+ clone = TransactionAttribute.DeserializeFrom(ref reader) as NotaryAssisted;
+ Assert.AreEqual(clone.Type, attr.Type);
+
+ // Wrong type
+ buffer[0] = 0xff;
+ Assert.ThrowsException(() =>
+ {
+ var reader = new MemoryReader(buffer);
+ TransactionAttribute.DeserializeFrom(ref reader);
+ });
+ }
+
+ [TestMethod]
+ public void Verify()
+ {
+ var attr = new NotaryAssisted() { NKeys = 4 };
+
+ // Temporary use Notary contract hash stub for valid transaction.
+ var txGood = new Transaction { Signers = new Signer[] { new Signer() { Account = notaryHash } } };
+ var txBad = new Transaction { Signers = new Signer[] { new Signer() { Account = UInt160.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01") } } };
+ var snapshot = TestBlockchain.GetTestSnapshot();
+
+ Assert.IsTrue(attr.Verify(snapshot, txGood));
+ Assert.IsFalse(attr.Verify(snapshot, txBad));
+ }
+
+ [TestMethod]
+ public void CalculateNetworkFee()
+ {
+ var snapshot = TestBlockchain.GetTestSnapshot();
+ var attr = new NotaryAssisted() { NKeys = 4 };
+ var tx = new Transaction { Signers = new Signer[] { new Signer() { Account = notaryHash } } };
+
+ Assert.AreEqual((4 + 1) * 1000_0000, attr.CalculateNetworkFee(snapshot, tx));
+ }
+ }
+}
diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs
index 51617b6744..a7716923bd 100644
--- a/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs
+++ b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs
@@ -10,17 +10,36 @@
// modifications are permitted.
using FluentAssertions;
+using FluentAssertions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Neo.Cryptography.ECC;
+using Neo.IO;
using Neo.IO;
using Neo.Network.P2P.Payloads;
+using Neo.Network.P2P.Payloads;
+using Neo.Persistence;
using Neo.Persistence;
using Neo.SmartContract;
+using Neo.SmartContract;
using Neo.SmartContract.Native;
+using Neo.SmartContract.Native;
+using Neo.UnitTests.Extensions;
using Neo.UnitTests.Extensions;
+using Neo.VM;
+using Neo.Wallets;
+using System;
using System;
+using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Linq;
using System.Numerics;
+using System.Numerics;
+using System.Numerics;
using System.Threading.Tasks;
+using VMTypes = Neo.VM.Types;
+// using VMArray = Neo.VM.Types.Array;
namespace Neo.UnitTests.SmartContract.Native
{
@@ -151,5 +170,80 @@ 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 = new Witness() { InvocationScript = Array.Empty(), VerificationScript = Array.Empty() }
+ },
+ Transactions = new Transaction[] { tx1, tx2 }
+ };
+ var snapshot = _snapshot.CreateSnapshot();
+
+ // Designate single Notary node.
+ byte[] privateKey1 = new byte[32];
+ var rng1 = System.Security.Cryptography.RandomNumberGenerator.Create();
+ rng1.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();
+
+ var script = new ScriptBuilder();
+ script.EmitSysCall(ApplicationEngine.System_Contract_NativeOnPersist);
+ var engine = ApplicationEngine.Create(TriggerType.OnPersist, null, snapshot, persistingBlock, settings: TestBlockchain.TheNeoSystem.Settings);
+
+ // Check that block's Primary balance is 0.
+ ECPoint[] validators = NativeContract.NEO.GetNextBlockValidators(engine.Snapshot, engine.ProtocolSettings.ValidatorsCount);
+ var primary = Contract.CreateSignatureRedeemScript(validators[engine.PersistingBlock.PrimaryIndex]).ToScriptHash();
+ NativeContract.GAS.BalanceOf(engine.Snapshot, 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 minted to Notary nodes as a reward.
+ Assert.AreEqual(2 + 1 + 1, engine.Notifications.Count()); // burn tx1 and tx2 network fee + mint primary reward + transfer Notary node reward
+ Assert.AreEqual(netFee1 + netFee2 - expectedNotaryReward, engine.Notifications[2].State[2]);
+ NativeContract.GAS.BalanceOf(engine.Snapshot, primary).Should().Be(netFee1 + netFee2 - expectedNotaryReward);
+ Assert.AreEqual(expectedNotaryReward, engine.Notifications[3].State[2]);
+ NativeContract.GAS.BalanceOf(engine.Snapshot, key1.PublicKey.EncodePoint(true).ToScriptHash()).Should().Be(expectedNotaryReward);
+ }
}
}
diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs
index 1989b4323b..60d48c021a 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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1094259016},"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":"getStoragePrice","parameters":[],"returntype":"Integer","offset":28,"safe":true},{"name":"isBlocked","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":35,"safe":true},{"name":"setAttributeFee","parameters":[{"name":"attributeType","type":"Integer"},{"name":"value","type":"Integer"}],"returntype":"Void","offset":42,"safe":false},{"name":"setExecFeeFactor","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":49,"safe":false},{"name":"setFeePerByte","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":56,"safe":false},{"name":"setStoragePrice","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":63,"safe":false},{"name":"unblockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":70,"safe":false}],"events":[]},"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"}]}]},"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":[],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"acc","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"expirationOf","parameters":[{"name":"acc","type":"Hash160"}],"returntype":"Integer","offset":7,"safe":true},{"name":"getMaxNotValidBeforeDelta","parameters":[],"returntype":"Integer","offset":14,"safe":true},{"name":"lockDepositUntil","parameters":[{"name":"addr","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":"sig","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..dab9e38c20
--- /dev/null
+++ b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs
@@ -0,0 +1,597 @@
+// Copyright (C) 2015-2024 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.IO;
+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.Linq;
+using System.Numerics;
+using System.Threading.Tasks;
+using VMTypes = Neo.VM.Types;
+
+namespace Neo.UnitTests.SmartContract.Native
+{
+ [TestClass]
+ public class UT_Notary
+ {
+ private DataCache _snapshot;
+ private Block _persistingBlock;
+
+ [TestInitialize]
+ public void TestSetup()
+ {
+ _snapshot = TestBlockchain.GetTestSnapshot();
+ _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.CreateSnapshot();
+ 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 }));
+
+ // Non-GAS transfer should fail.
+ Assert.ThrowsException(() => NativeContract.NEO.Transfer(snapshot, from, NativeContract.Notary.Hash.ToArray(), BigInteger.Zero, true, persistingBlock));
+
+ // GAS transfer with invalid data format should fail.
+ Assert.ThrowsException(() => NativeContract.GAS.Transfer(snapshot, from, NativeContract.Notary.Hash.ToArray(), 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.ThrowsException(() => NativeContract.GAS.Transfer(snapshot, from, NativeContract.Notary.Hash.ToArray(), 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.ThrowsException(() => NativeContract.GAS.TransferWithTransaction(snapshot, from, NativeContract.Notary.Hash.ToArray(), 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.ThrowsException(() => NativeContract.GAS.TransferWithTransaction(snapshot, from, NativeContract.Notary.Hash.ToArray(), 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, NativeContract.Notary.Hash.ToArray(), 2 * 1000_0000 + 1, true, persistingBlock, data));
+ }
+
+ [TestMethod]
+ public void Check_ExpirationOf()
+ {
+ var snapshot = _snapshot.CreateSnapshot();
+ 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);
+
+ // 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);
+
+ // 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, NativeContract.Notary.Hash.ToArray(), 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, NativeContract.Notary.Hash.ToArray(), 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.CreateSnapshot();
+ 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.CreateSnapshot();
+ var persistingBlock = new Block { Header = new Header { Index = 1000 } };
+ UInt160 fromAddr = Contract.GetBFTAddress(TestProtocolSettings.Default.StandbyValidators);
+ byte[] from = fromAddr.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, NativeContract.Notary.Hash.ToArray(), 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, NativeContract.Notary.Hash.ToArray(), 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, NativeContract.Notary.Hash.ToArray(), 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: TestBlockchain.TheNeoSystem.Settings);
+ 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.CreateSnapshot();
+ 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");
+ Assert.ThrowsException(() => Call_Withdraw(snapshot, from, sideAccount.ToArray(), persistingBlock, false));
+
+ // Withdraw missing (zero) deposit should fail.
+ Assert.ThrowsException(() => Call_Withdraw(snapshot, sideAccount.ToArray(), sideAccount.ToArray(), persistingBlock));
+
+ // Withdraw before deposit expiration should fail.
+ Assert.ThrowsException(() => Call_Withdraw(snapshot, from, from, persistingBlock));
+
+ // 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);
+
+ // 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: TestBlockchain.TheNeoSystem.Settings);
+
+ 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: TestBlockchain.TheNeoSystem.Settings);
+
+ 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: TestBlockchain.TheNeoSystem.Settings);
+
+ 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: TestBlockchain.TheNeoSystem.Settings);
+
+ 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.CreateSnapshot();
+ 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: TestBlockchain.TheNeoSystem.Settings);
+ 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 = (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[] { tx2 }
+ };
+ var snapshot = _snapshot.CreateSnapshot();
+
+ // 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();
+
+ // 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, null, snapshot, persistingBlock, settings: TestBlockchain.TheNeoSystem.Settings);
+ engine.LoadScript(script.ToArray());
+ Assert.IsTrue(engine.Execute() == VMState.HALT);
+ snapshot.Commit();
+
+ // Process transaction that changes NotaryServiceFeePerKey after OnPersist.
+ ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), persistingBlock,
+ "setAttributeFee", new ContractParameter(ContractParameterType.Integer) { Value = (BigInteger)(byte)TransactionAttributeType.NotaryAssisted }, new ContractParameter(ContractParameterType.Integer) { Value = newNotaryAssistedFeePerKey });
+ ret.IsNull.Should().BeTrue();
+ snapshot.Commit();
+
+ // Process tx2 with NotaryAssisted attribute.
+ engine = ApplicationEngine.Create(TriggerType.Application, tx2, snapshot, persistingBlock, settings: TestBlockchain.TheNeoSystem.Settings, 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.Snapshot, 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.Snapshot, key1.PublicKey.EncodePoint(true).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.CreateSnapshot();
+
+ // 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: TestBlockchain.TheNeoSystem.Settings);
+
+ // Check that block's Primary balance is 0.
+ ECPoint[] validators = NativeContract.NEO.GetNextBlockValidators(engine.Snapshot, engine.ProtocolSettings.ValidatorsCount);
+ var primary = Contract.CreateSignatureRedeemScript(validators[engine.PersistingBlock.PrimaryIndex]).ToScriptHash();
+ NativeContract.GAS.BalanceOf(engine.Snapshot, 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.Snapshot, primary).Should().Be(netFee1 + netFee2 - expectedNotaryReward);
+ Assert.AreEqual(expectedNotaryReward / 2, engine.Notifications[3].State[2]);
+ NativeContract.GAS.BalanceOf(engine.Snapshot, key1.PublicKey.EncodePoint(true).ToScriptHash()).Should().Be(expectedNotaryReward / 2);
+ Assert.AreEqual(expectedNotaryReward / 2, engine.Notifications[4].State[2]);
+ NativeContract.GAS.BalanceOf(engine.Snapshot, key2.PublicKey.EncodePoint(true).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.cs b/tests/Neo.UnitTests/TestUtils.cs
index f8f63de53a..ca545805c5 100644
--- a/tests/Neo.UnitTests/TestUtils.cs
+++ b/tests/Neo.UnitTests/TestUtils.cs
@@ -133,6 +133,38 @@ public static Transaction GetTransaction(UInt160 sender)
};
}
+ public static Transaction GetTransaction(UInt160 sender, UInt160 signer)
+ {
+ var tx = GetTransaction(sender);
+ tx.Signers = new[]{ new Signer()
+ {
+ Account = sender,
+ Scopes = WitnessScope.CalledByEntry,
+ AllowedContracts = Array.Empty(),
+ AllowedGroups = Array.Empty(),
+ Rules = Array.Empty(),
+ },
+ new Signer()
+ {
+ Account = signer,
+ Scopes = WitnessScope.CalledByEntry,
+ AllowedContracts = Array.Empty(),
+ AllowedGroups = Array.Empty(),
+ Rules = Array.Empty(),
+ } };
+ tx.Witnesses = new Witness[]{ new Witness
+ {
+ InvocationScript = Array.Empty(),
+ VerificationScript = Array.Empty()
+ },
+ new Witness
+ {
+ InvocationScript = Array.Empty(),
+ VerificationScript = Array.Empty()
+ } };
+ return tx;
+ }
+
internal static ContractState GetContract(string method = "test", int parametersCount = 0)
{
NefFile nef = new()