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