From 6da4ae28163d84f93321919a51e6bb4927a674ca Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 6 Mar 2024 11:17:19 +0300 Subject: [PATCH 01/22] Implement NotaryAssisted transaction attribute Close #2896. Use a stub for native Notary contract hash since this contract is not implemented yet. Thus, technically, NotaryAssisted attribute verification will always fail on real network until native Notary is implemented. Signed-off-by: Anna Shaleva --- src/Neo.CLI/CLI/MainService.Blockchain.cs | 4 + .../Network/P2P/Payloads/NotaryAssisted.cs | 63 +++++++++++++ .../P2P/Payloads/TransactionAttributeType.cs | 8 +- .../SmartContract/Native/PolicyContract.cs | 6 ++ .../Network/P2P/Payloads/UT_NotaryAssisted.cs | 88 +++++++++++++++++++ 5 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 src/Neo/Network/P2P/Payloads/NotaryAssisted.cs create mode 100644 tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs 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..51ff6959dc --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs @@ -0,0 +1,63 @@ +// 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) + { + // Stub native Notary contract related check until the contract is implemented. + UInt160 notaryH = new UInt160(); + return tx.Signers.Any(p => p.Account.Equals(notaryH)); + } + + 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..192c90fdd5 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 is aimed to service notary request with . + /// + [ReflectionCache(typeof(NotaryAssisted))] + NotaryAssisted = 0x22 } } diff --git a/src/Neo/SmartContract/Native/PolicyContract.cs b/src/Neo/SmartContract/Native/PolicyContract.cs index 5b00af92ad..3ee5151aa8 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 Initialize(ApplicationEngine engine, Hardfork? ha 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/Network/P2P/Payloads/UT_NotaryAssisted.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs new file mode 100644 index 0000000000..b5894548f2 --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs @@ -0,0 +1,88 @@ +// 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 + { + [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 = UInt160.Zero } } }; + 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 = UInt160.Zero } } }; + + Assert.AreEqual((4 + 1) * 1000_0000, attr.CalculateNetworkFee(snapshot, tx)); + } + } +} From acec1b02509d2fed9499eade0354490d226ee876 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 6 Mar 2024 17:54:51 +0300 Subject: [PATCH 02/22] Payloads: add doc to CalculateNetworkFee method of NotaryAssisted attribute Signed-off-by: Anna Shaleva --- src/Neo/Network/P2P/Payloads/NotaryAssisted.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs index 51ff6959dc..3ae10de198 100644 --- a/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs +++ b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs @@ -55,6 +55,16 @@ public override bool Verify(DataCache snapshot, Transaction tx) return tx.Signers.Any(p => p.Account.Equals(notaryH)); } + /// + /// 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); From 1508d4f48f6a0ec894d83c8b2097935bc1bdff7d Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 7 Mar 2024 12:27:04 +0300 Subject: [PATCH 03/22] Native: add NotaryAssisted attributes handler to Gas OnPersist Transactions network fee should be split between Primary node and Notary nodes. Signed-off-by: Anna Shaleva --- src/Neo/SmartContract/Native/GasToken.cs | 8 ++ .../SmartContract/Native/UT_GasToken.cs | 74 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/src/Neo/SmartContract/Native/GasToken.cs b/src/Neo/SmartContract/Native/GasToken.cs index ce2f77ad2c..fede070d2e 100644 --- a/src/Neo/SmartContract/Native/GasToken.cs +++ b/src/Neo/SmartContract/Native/GasToken.cs @@ -43,6 +43,14 @@ internal override async ContractTask OnPersist(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/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs index 51617b6744..b1a1e38b79 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs @@ -10,17 +10,35 @@ // 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.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 +169,61 @@ 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(); + 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 + // will be minted to Notary nodes as a reward once Notary contract is implemented. + Assert.AreEqual(2 + 1, engine.Notifications.Count()); // burn tx1 and tx2 network fee + mint primary reward + Assert.AreEqual(netFee1 + netFee2 - expectedNotaryReward, engine.Notifications[2].State[2]); + NativeContract.GAS.BalanceOf(engine.Snapshot, primary).Should().Be(netFee1 + netFee2 - expectedNotaryReward); + } } } From 8b547c9c59cc4d32a87a6fc1e35b8a1d97907f15 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 7 Mar 2024 12:31:16 +0300 Subject: [PATCH 04/22] Payloads: adjust comment to NotaryAssisted attribute Signed-off-by: Anna Shaleva --- src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs b/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs index 192c90fdd5..276ef7710f 100644 --- a/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs +++ b/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs @@ -43,7 +43,7 @@ public enum TransactionAttributeType : byte Conflicts = 0x21, /// - /// Indicates that the transaction is aimed to service notary request with . + /// Indicates that the transaction uses notary request service with . /// [ReflectionCache(typeof(NotaryAssisted))] NotaryAssisted = 0x22 From b16a28c4348857acd3e10f2379c3463a3da90f8e Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 7 Mar 2024 12:38:59 +0300 Subject: [PATCH 05/22] Payloads: temporary use hard-coded Notary contract hash Once Notary contract is implemented, this hash will be replaced by a proper Notary contract hash. The exact value won't be changed since Notary contract has constant hash as any other native contract. Signed-off-by: Anna Shaleva --- src/Neo/Network/P2P/Payloads/NotaryAssisted.cs | 9 ++++++--- .../Network/P2P/Payloads/UT_NotaryAssisted.cs | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs index 3ae10de198..bdf7fb7552 100644 --- a/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs +++ b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs @@ -20,6 +20,11 @@ namespace Neo.Network.P2P.Payloads { public class NotaryAssisted : TransactionAttribute { + /// + /// Native Notary contract hash stub used until native Notary contract is properly implemented. + /// + private static readonly UInt160 notaryHash = UInt160.Parse("0xc1e14f19c3e60d0b9244d06dd7ba9b113135ec3b"); + /// /// Indicates the number of keys participating in the transaction (main or fallback) signing process. /// @@ -50,9 +55,7 @@ public override JObject ToJson() public override bool Verify(DataCache snapshot, Transaction tx) { - // Stub native Notary contract related check until the contract is implemented. - UInt160 notaryH = new UInt160(); - return tx.Signers.Any(p => p.Account.Equals(notaryH)); + return tx.Signers.Any(p => p.Account.Equals(notaryHash)); } /// diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs index b5894548f2..939a368c33 100644 --- a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs @@ -23,6 +23,8 @@ namespace Neo.UnitTests.Network.P2P.Payloads [TestClass] public class UT_NotaryAssisted { + private static readonly UInt160 notaryHash = UInt160.Parse("0xc1e14f19c3e60d0b9244d06dd7ba9b113135ec3b"); + [TestMethod] public void Size_Get() { @@ -67,7 +69,7 @@ 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 = UInt160.Zero } } }; + 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(); @@ -80,7 +82,7 @@ public void CalculateNetworkFee() { var snapshot = TestBlockchain.GetTestSnapshot(); var attr = new NotaryAssisted() { NKeys = 4 }; - var tx = new Transaction { Signers = new Signer[] { new Signer() { Account = UInt160.Zero } } }; + var tx = new Transaction { Signers = new Signer[] { new Signer() { Account = notaryHash } } }; Assert.AreEqual((4 + 1) * 1000_0000, attr.CalculateNetworkFee(snapshot, tx)); } From 24135ff59ceb3554f39ca30f5cfec68624e2571b Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 12 Mar 2024 22:14:29 +0300 Subject: [PATCH 06/22] Payloads: replace hard-coded Notary hash value with calculated one No functional changes, just a refactoring for better code readability. Signed-off-by: Anna Shaleva --- src/Neo/Network/P2P/Payloads/NotaryAssisted.cs | 2 +- tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs index bdf7fb7552..0a110688a8 100644 --- a/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs +++ b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs @@ -23,7 +23,7 @@ public class NotaryAssisted : TransactionAttribute /// /// Native Notary contract hash stub used until native Notary contract is properly implemented. /// - private static readonly UInt160 notaryHash = UInt160.Parse("0xc1e14f19c3e60d0b9244d06dd7ba9b113135ec3b"); + private static readonly UInt160 notaryHash = Neo.SmartContract.Helper.GetContractHash(UInt160.Zero, 0, "Notary"); /// /// Indicates the number of keys participating in the transaction (main or fallback) signing process. diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs index 939a368c33..778fe7a53d 100644 --- a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs @@ -23,6 +23,7 @@ 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] From 77ffe094368d14b2f8c089e9b52479d6f1a8cc4b Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 19 Mar 2024 12:11:02 +0300 Subject: [PATCH 07/22] Native: implement native Notary contract Close #2897. Depends on #3175. Signed-off-by: Anna Shaleva --- .../Network/P2P/Payloads/NotaryAssisted.cs | 7 +- .../SmartContract/Native/NativeContract.cs | 5 + src/Neo/SmartContract/Native/Notary.cs | 336 ++++++++++++++++++ .../Nep17NativeContractExtensions.cs | 8 +- .../SmartContract/Native/UT_GasToken.cs | 24 +- .../SmartContract/Native/UT_Notary.cs | 207 +++++++++++ 6 files changed, 578 insertions(+), 9 deletions(-) create mode 100644 src/Neo/SmartContract/Native/Notary.cs create mode 100644 tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs diff --git a/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs index 0a110688a8..5c115a9f0e 100644 --- a/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs +++ b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs @@ -20,11 +20,6 @@ namespace Neo.Network.P2P.Payloads { public class NotaryAssisted : TransactionAttribute { - /// - /// Native Notary contract hash stub used until native Notary contract is properly implemented. - /// - private static readonly UInt160 notaryHash = Neo.SmartContract.Helper.GetContractHash(UInt160.Zero, 0, "Notary"); - /// /// Indicates the number of keys participating in the transaction (main or fallback) signing process. /// @@ -55,7 +50,7 @@ public override JObject ToJson() public override bool Verify(DataCache snapshot, Transaction tx) { - return tx.Signers.Any(p => p.Account.Equals(notaryHash)); + return tx.Signers.Any(p => p.Account.Equals(NativeContract.Notary.Hash)); } /// diff --git a/src/Neo/SmartContract/Native/NativeContract.cs b/src/Neo/SmartContract/Native/NativeContract.cs index a07f0ac8ce..c1c5a23e92 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..2fea7fdb73 --- /dev/null +++ b/src/Neo/SmartContract/Native/Notary.cs @@ -0,0 +1,336 @@ +// 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.SmartContract.Iterators; +using Neo.SmartContract.Manifest; +using Neo.VM; +using Neo.VM.Types; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +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 Initialize(ApplicationEngine engine, Hardfork? hardfork) + { + if (hardfork == ActiveIn) + { + engine.Snapshot.Add(CreateStorageKey(Prefix_MaxNotValidBeforeDelta), new StorageItem(DefaultMaxNotValidBeforeDelta)); + } + return ContractTask.CompletedTask; + } + + internal override async ContractTask OnPersist(ApplicationEngine engine) + { + long nFees = 0; + ECPoint[] notaries = null; + foreach (Transaction tx in engine.PersistingBlock.Transactions) + { + var attr = tx.GetAttribute(); + if (attr is not null) + { + if (notaries is 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()); + var verified = false; + foreach (var n in notaries) + { + if (Crypto.VerifySignature(hash, sig, n)) + { + verified = true; + break; + } + } + return verified; + } + + /// + /// 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 then the chain's height {0} + 1", currentHeight + 2)); + if (deposit != null && till < deposit.Till) throw new ArgumentOutOfRangeException(string.Format("`till` shouldn't be less then 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 then {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; + Deposit deposit = GetDepositFor(engine.Snapshot, from); + if (deposit is null) 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); + + await engine.CallFromNativeContract(Hash, GAS.Hash, "transfer", Hash.ToArray(), receive.ToArray(), deposit.Amount, null); + 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/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs b/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs index a3f33f26a7..c290271116 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,17 @@ 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, params 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) diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs index b1a1e38b79..a7716923bd 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs @@ -31,6 +31,7 @@ using System; using System; using System; +using System.Collections.Generic; using System.Linq; using System.Linq; using System.Numerics; @@ -206,6 +207,23 @@ public void Check_OnPersist_NotaryAssisted() 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); @@ -220,10 +238,12 @@ public void Check_OnPersist_NotaryAssisted() Assert.IsTrue(engine.Execute() == VMState.HALT); // Check that proper amount of GAS was minted to block's Primary and the rest - // will be minted to Notary nodes as a reward once Notary contract is implemented. - Assert.AreEqual(2 + 1, engine.Notifications.Count()); // burn tx1 and tx2 network fee + mint primary reward + // 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_Notary.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs new file mode 100644 index 0000000000..456671a4f7 --- /dev/null +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs @@ -0,0 +1,207 @@ +// 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 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.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 +{ + [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(); + + + 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, (uint)5)); + } + + [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_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 + }; + } + } +} From cb4bdda47f26fb95cf8bb3faa1f17bf365eebf9d Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 21 Mar 2024 21:01:10 +0300 Subject: [PATCH 08/22] Native: fix typo in the exception message Signed-off-by: Anna Shaleva --- src/Neo/SmartContract/Native/Notary.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Neo/SmartContract/Native/Notary.cs b/src/Neo/SmartContract/Native/Notary.cs index 2fea7fdb73..0658c3281d 100644 --- a/src/Neo/SmartContract/Native/Notary.cs +++ b/src/Neo/SmartContract/Native/Notary.cs @@ -148,12 +148,12 @@ private void OnNEP17Payment(ApplicationEngine engine, UInt160 from, BigInteger a 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 then the chain's height {0} + 1", currentHeight + 2)); - if (deposit != null && till < deposit.Till) throw new ArgumentOutOfRangeException(string.Format("`till` shouldn't be less then the previous value {0}", deposit.Till)); + 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 then {0}, got {1}", 2 * feePerKey, amount)); + 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; } From 8f9f5f7d8091bd71319edabfb26bf2fab317d8d8 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 21 Mar 2024 21:06:06 +0300 Subject: [PATCH 09/22] Native: use more syntactic sugar No functional changes, just a refactoring. Signed-off-by: Anna Shaleva --- src/Neo/SmartContract/Native/Notary.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Neo/SmartContract/Native/Notary.cs b/src/Neo/SmartContract/Native/Notary.cs index 0658c3281d..f69b5ec4ad 100644 --- a/src/Neo/SmartContract/Native/Notary.cs +++ b/src/Neo/SmartContract/Native/Notary.cs @@ -67,7 +67,7 @@ internal override async ContractTask OnPersist(ApplicationEngine engine) var attr = tx.GetAttribute(); if (attr is not null) { - if (notaries is null) notaries = GetNotaryNodes(engine.Snapshot); + notaries ??= GetNotaryNodes(engine.Snapshot); var nKeys = attr.NKeys; nFees += (long)nKeys + 1; if (tx.Sender == Hash) @@ -114,15 +114,7 @@ private bool Verify(ApplicationEngine engine, byte[] sig) ECPoint[] notaries = GetNotaryNodes(engine.Snapshot); var hash = tx.GetSignData(engine.GetNetwork()); var verified = false; - foreach (var n in notaries) - { - if (Crypto.VerifySignature(hash, sig, n)) - { - verified = true; - break; - } - } - return verified; + return notaries.Any(n => Crypto.VerifySignature(hash, sig, n)); } /// From 02c6a890ca311d694bbafbe22b802aeb8ad7fe8e Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 4 Apr 2024 23:29:15 +0300 Subject: [PATCH 10/22] Notary: add unit tests for OnNEP17Payment and ExpirationOf methods Also, mark improper code of Withdraw method with TODO, it should be reworked. Signed-off-by: Anna Shaleva --- src/Neo/SmartContract/Native/Notary.cs | 12 +- .../Nep17NativeContractExtensions.cs | 23 +++- .../SmartContract/Native/UT_Notary.cs | 108 +++++++++++++++++- 3 files changed, 135 insertions(+), 8 deletions(-) diff --git a/src/Neo/SmartContract/Native/Notary.cs b/src/Neo/SmartContract/Native/Notary.cs index f69b5ec4ad..1ec62d8e4e 100644 --- a/src/Neo/SmartContract/Native/Notary.cs +++ b/src/Neo/SmartContract/Native/Notary.cs @@ -134,11 +134,9 @@ private void OnNEP17Payment(ApplicationEngine engine, UInt160 from, BigInteger a 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)); @@ -214,7 +212,7 @@ public BigInteger BalanceOf(DataCache snapshot, UInt160 acc) /// To Account /// Whether withdrawal was successfull. [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] - private async ContractTask Withdraw(ApplicationEngine engine, UInt160 from, UInt160 to) + private 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; @@ -223,8 +221,12 @@ private async ContractTask Withdraw(ApplicationEngine engine, UInt160 from if (Ledger.CurrentIndex(engine.Snapshot) < deposit.Till) throw new InvalidOperationException(string.Format("Can't withdraw before {0}", deposit.Till)); RemoveDepositFor(engine.Snapshot, from); - await engine.CallFromNativeContract(Hash, GAS.Hash, "transfer", Hash.ToArray(), receive.ToArray(), deposit.Amount, null); - return true; + // TODO: this code is invalid and doesn't work as expected. We need to throw an exception if 'transfer' call returns 'false'. + return engine.CallFromNativeContract(Hash, GAS.Hash, "transfer", Hash.ToArray(), receive.ToArray(), deposit.Amount, null); + // if (!transferOK) { + // throw new InvalidOperationException(string.Format("Transfer to {0} has failed", receive.ToString())); + // } + //return true; } /// diff --git a/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs b/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs index c290271116..6e11555848 100644 --- a/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs +++ b/tests/Neo.UnitTests/Extensions/Nep17NativeContractExtensions.cs @@ -53,7 +53,7 @@ public static bool Transfer(this NativeContract contract, DataCache snapshot, by 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, params object[] data) + 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); @@ -73,6 +73,27 @@ public static bool Transfer(this NativeContract contract, DataCache snapshot, by 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) + { + throw engine.FaultException; + } + + var result = engine.ResultStack.Pop(); + result.Should().BeOfType(typeof(VM.Types.Boolean)); + + return result.GetBoolean(); + } + public static BigInteger TotalSupply(this NativeContract contract, DataCache snapshot) { using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestBlockchain.TheNeoSystem.Settings); diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs index 456671a4f7..c3602950e2 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs @@ -40,6 +40,7 @@ using System.Numerics; using System.Threading.Tasks; using VMTypes = Neo.VM.Types; + // using VMArray = Neo.VM.Types.Array; namespace Neo.UnitTests.SmartContract.Native @@ -66,8 +67,9 @@ 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(); + var defaultNotaryServiceFeePerKey = 1000_0000; - + // 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 })); @@ -75,7 +77,109 @@ public void Check_OnNEP17Payment() 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, (uint)5)); + 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(); + var defaultNotaryServiceFeePerKey = 1000_0000; + + // 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 defaultTill = 5760; + Call_ExpirationOf(snapshot, to.ToArray(), persistingBlock).Should().Be(persistingBlock.Index - 1 + defaultTill); + + // 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); + + } + + 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_Withdraw(DataCache snapshot, byte[] from, byte[] to, Block persistingBlock) + { + using var engine = ApplicationEngine.Create(TriggerType.Application, new Transaction() { Signers = new Signer[] { new Signer() { Account = new UInt160(from), 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] From 21f5fcf99b458300ceb7e1e416b5e77cfb7bd8bf Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Mon, 8 Apr 2024 09:26:26 +0200 Subject: [PATCH 11/22] Fix transfer --- src/Neo/SmartContract/Native/Notary.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Neo/SmartContract/Native/Notary.cs b/src/Neo/SmartContract/Native/Notary.cs index 1ec62d8e4e..82e27b38eb 100644 --- a/src/Neo/SmartContract/Native/Notary.cs +++ b/src/Neo/SmartContract/Native/Notary.cs @@ -212,21 +212,19 @@ public BigInteger BalanceOf(DataCache snapshot, UInt160 acc) /// To Account /// Whether withdrawal was successfull. [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] - private ContractTask Withdraw(ApplicationEngine engine, UInt160 from, UInt160 to) + 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; - Deposit deposit = GetDepositFor(engine.Snapshot, from); - if (deposit is null) throw new InvalidOperationException(string.Format("Deposit of {0} is null", from.ToString())); + 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); - - // TODO: this code is invalid and doesn't work as expected. We need to throw an exception if 'transfer' call returns 'false'. - return engine.CallFromNativeContract(Hash, GAS.Hash, "transfer", Hash.ToArray(), receive.ToArray(), deposit.Amount, null); - // if (!transferOK) { - // throw new InvalidOperationException(string.Format("Transfer to {0} has failed", receive.ToString())); - // } - //return true; + await engine.CallFromNativeContract(Hash, GAS.Hash, "transfer", Hash.ToArray(), receive.ToArray(), deposit.Amount, null); + if (!engine.Pop().GetBoolean()) + { + throw new InvalidOperationException(string.Format("Transfer to {0} has failed", receive.ToString())); + } + return true; } /// From e8d384ef5bda96ff6b92dc93a72ad19baa735112 Mon Sep 17 00:00:00 2001 From: Shargon Date: Mon, 8 Apr 2024 00:32:07 -0700 Subject: [PATCH 12/22] Update tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs --- tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs index c3602950e2..c141fbb609 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs @@ -41,7 +41,6 @@ using System.Threading.Tasks; using VMTypes = Neo.VM.Types; -// using VMArray = Neo.VM.Types.Array; namespace Neo.UnitTests.SmartContract.Native { From 26c9430a397fc64c388e1116f7f7e11addb35ec8 Mon Sep 17 00:00:00 2001 From: Shargon Date: Mon, 8 Apr 2024 00:32:34 -0700 Subject: [PATCH 13/22] Update UT_Notary.cs --- tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs index c141fbb609..d4678ef283 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs @@ -41,7 +41,6 @@ using System.Threading.Tasks; using VMTypes = Neo.VM.Types; - namespace Neo.UnitTests.SmartContract.Native { [TestClass] From b23859f1d3d0de619942da6d44c6498545aac9bf Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Mon, 8 Apr 2024 09:37:06 +0200 Subject: [PATCH 14/22] fix --- src/Neo/SmartContract/Native/Notary.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo/SmartContract/Native/Notary.cs b/src/Neo/SmartContract/Native/Notary.cs index 82e27b38eb..e038ea7bda 100644 --- a/src/Neo/SmartContract/Native/Notary.cs +++ b/src/Neo/SmartContract/Native/Notary.cs @@ -219,7 +219,7 @@ private async ContractTask Withdraw(ApplicationEngine engine, UInt160 from 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); - await engine.CallFromNativeContract(Hash, GAS.Hash, "transfer", Hash.ToArray(), receive.ToArray(), deposit.Amount, null); + await engine.CallFromNativeContract(Hash, GAS.Hash, "transfer", Hash.ToArray(), receive.ToArray(), deposit.Amount, null); if (!engine.Pop().GetBoolean()) { throw new InvalidOperationException(string.Format("Transfer to {0} has failed", receive.ToString())); From ff03214f000968014b5fc7605ed977314796a241 Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Mon, 8 Apr 2024 11:03:37 +0200 Subject: [PATCH 15/22] Fix notary --- src/Neo/SmartContract/Native/Notary.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Neo/SmartContract/Native/Notary.cs b/src/Neo/SmartContract/Native/Notary.cs index e038ea7bda..1a295acc14 100644 --- a/src/Neo/SmartContract/Native/Notary.cs +++ b/src/Neo/SmartContract/Native/Notary.cs @@ -219,8 +219,7 @@ private async ContractTask Withdraw(ApplicationEngine engine, UInt160 from 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); - await engine.CallFromNativeContract(Hash, GAS.Hash, "transfer", Hash.ToArray(), receive.ToArray(), deposit.Amount, null); - if (!engine.Pop().GetBoolean()) + if (!await engine.CallFromNativeContract(Hash, GAS.Hash, "transfer", Hash.ToArray(), receive.ToArray(), deposit.Amount, StackItem.Null)) { throw new InvalidOperationException(string.Format("Transfer to {0} has failed", receive.ToString())); } From adf0bb0d582b0ca496c13b380bc9250416cb54ba Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Mon, 8 Apr 2024 11:04:02 +0200 Subject: [PATCH 16/22] Fix notary --- src/Neo/SmartContract/Native/Notary.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Neo/SmartContract/Native/Notary.cs b/src/Neo/SmartContract/Native/Notary.cs index 1a295acc14..1d212b5990 100644 --- a/src/Neo/SmartContract/Native/Notary.cs +++ b/src/Neo/SmartContract/Native/Notary.cs @@ -17,13 +17,9 @@ using Neo.Network.P2P; using Neo.Network.P2P.Payloads; using Neo.Persistence; -using Neo.SmartContract.Iterators; -using Neo.SmartContract.Manifest; using Neo.VM; using Neo.VM.Types; using System; -using System.Buffers.Binary; -using System.Collections.Generic; using System.Linq; using System.Numerics; using Array = Neo.VM.Types.Array; From 08da1f086d271671d97434c95a7abad1c94d93a6 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 8 Apr 2024 23:41:41 +0300 Subject: [PATCH 17/22] Notary: add unit tests for LockDepositUntil and BalanceOf methods Also, format code. Signed-off-by: Anna Shaleva --- .../SmartContract/Native/UT_Notary.cs | 191 ++++++++++++++++-- tests/Neo.UnitTests/TestUtils.cs | 32 +++ 2 files changed, 205 insertions(+), 18 deletions(-) diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs index d4678ef283..bcde973ffb 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs @@ -10,33 +10,19 @@ // 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.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; @@ -65,7 +51,6 @@ 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(); - var defaultNotaryServiceFeePerKey = 1000_0000; // Set proper current index for deposit's Till parameter check. var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); @@ -100,7 +85,6 @@ 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(); - var defaultNotaryServiceFeePerKey = 1000_0000; // Set proper current index for deposit's Till parameter check. var storageKey = new KeyBuilder(NativeContract.Ledger.Id, 12); @@ -131,8 +115,8 @@ public void Check_ExpirationOf() 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 defaultTill = 5760; - Call_ExpirationOf(snapshot, to.ToArray(), persistingBlock).Should().Be(persistingBlock.Index - 1 + defaultTill); + var defaultDeltaTill = 5760; + Call_ExpirationOf(snapshot, to.ToArray(), persistingBlock).Should().Be(persistingBlock.Index - 1 + defaultDeltaTill); // Withdraw own deposit. persistingBlock.Header.Index = till + 1; @@ -142,7 +126,162 @@ public void Check_ExpirationOf() // 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); + } + + 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) @@ -161,6 +300,22 @@ internal static BigInteger Call_ExpirationOf(DataCache snapshot, byte[] address, 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) { using var engine = ApplicationEngine.Create(TriggerType.Application, new Transaction() { Signers = new Signer[] { new Signer() { Account = new UInt160(from), Scopes = WitnessScope.Global } }, Attributes = System.Array.Empty() }, snapshot, persistingBlock, settings: TestBlockchain.TheNeoSystem.Settings); 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() From 670546971648b0c16f81b6a94a80b697e70242ab Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 10 Apr 2024 23:21:55 +0300 Subject: [PATCH 18/22] Notary: test GAS distribution with FeePerKey update Test the situation described in https://github.com/neo-project/neo/pull/3175/files/00b54ff6d20cc84b435beaa790fe72a9d8f78bec#r1530493475. Signed-off-by: Anna Shaleva --- .../SmartContract/Native/UT_Notary.cs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs index bcde973ffb..38c89499e8 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs @@ -358,6 +358,90 @@ public void Check_SetMaxNotValidBeforeDelta() 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() { From 8864ff520f38e035ee6136296e77d6d2427993c1 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 10 Apr 2024 23:44:31 +0300 Subject: [PATCH 19/22] Notary: add test for Withdraw Signed-off-by: Anna Shaleva --- .../SmartContract/Native/UT_Notary.cs | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs index 38c89499e8..dab9e38c20 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_Notary.cs @@ -268,6 +268,50 @@ public void Check_BalanceOf() 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); @@ -316,9 +360,14 @@ internal static bool Call_LockDepositUntil(DataCache snapshot, byte[] address, u return result.GetBoolean(); } - internal static bool Call_Withdraw(DataCache snapshot, byte[] from, byte[] to, Block persistingBlock) + internal static bool Call_Withdraw(DataCache snapshot, byte[] from, byte[] to, Block persistingBlock, bool witnessedByFrom = true) { - using var engine = ApplicationEngine.Create(TriggerType.Application, new Transaction() { Signers = new Signer[] { new Signer() { Account = new UInt160(from), Scopes = WitnessScope.Global } }, Attributes = System.Array.Empty() }, snapshot, persistingBlock, settings: TestBlockchain.TheNeoSystem.Settings); + 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); From 498f2ff6a76d2375fd9ff56e98375fa94bd4ab17 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 11 Apr 2024 12:26:48 +0300 Subject: [PATCH 20/22] Notary: remove unused code No functional changes, I just finally made my code analizer work properly. Signed-off-by: Anna Shaleva --- src/Neo/SmartContract/Native/Notary.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Neo/SmartContract/Native/Notary.cs b/src/Neo/SmartContract/Native/Notary.cs index 1d212b5990..ecf4907838 100644 --- a/src/Neo/SmartContract/Native/Notary.cs +++ b/src/Neo/SmartContract/Native/Notary.cs @@ -109,7 +109,6 @@ private bool Verify(ApplicationEngine engine, byte[] sig) } ECPoint[] notaries = GetNotaryNodes(engine.Snapshot); var hash = tx.GetSignData(engine.GetNetwork()); - var verified = false; return notaries.Any(n => Crypto.VerifySignature(hash, sig, n)); } From efa9958982cf44251f8fa50bc6711942775c93e3 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 11 Apr 2024 14:06:14 +0300 Subject: [PATCH 21/22] Attributtes: fix NotaryAssisted attribute documentation format Co-authored-by: Shargon --- src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs b/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs index 276ef7710f..7e7f4f4252 100644 --- a/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs +++ b/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs @@ -43,7 +43,7 @@ public enum TransactionAttributeType : byte Conflicts = 0x21, /// - /// Indicates that the transaction uses notary request service with . + /// Indicates that the transaction uses notary request service with number of keys. /// [ReflectionCache(typeof(NotaryAssisted))] NotaryAssisted = 0x22 From deafea9232a115d2cfb82e1f1abfc1c6726acc7b Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 23 May 2024 23:39:11 +0300 Subject: [PATCH 22/22] Native: update to the fresh master Fetch changes from the fresh master and fix build errors. Signed-off-by: Anna Shaleva --- src/Neo/SmartContract/Native/Notary.cs | 6 +++--- .../Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Neo/SmartContract/Native/Notary.cs b/src/Neo/SmartContract/Native/Notary.cs index ecf4907838..949a19cd3c 100644 --- a/src/Neo/SmartContract/Native/Notary.cs +++ b/src/Neo/SmartContract/Native/Notary.cs @@ -45,7 +45,7 @@ public sealed class Notary : NativeContract internal Notary() : base() { } - internal override ContractTask Initialize(ApplicationEngine engine, Hardfork? hardfork) + internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardfork) { if (hardfork == ActiveIn) { @@ -54,7 +54,7 @@ internal override ContractTask Initialize(ApplicationEngine engine, Hardfork? ha return ContractTask.CompletedTask; } - internal override async ContractTask OnPersist(ApplicationEngine engine) + internal override async ContractTask OnPersistAsync(ApplicationEngine engine) { long nFees = 0; ECPoint[] notaries = null; @@ -214,7 +214,7 @@ private async ContractTask Withdraw(ApplicationEngine engine, UInt160 from 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.CallFromNativeContract(Hash, GAS.Hash, "transfer", Hash.ToArray(), receive.ToArray(), deposit.Amount, StackItem.Null)) + 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())); } 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}}"""} }; }