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 fbf5eae88b3cb3b2dd2fe9f7eae36cb977648d50 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 5 Jun 2024 19:54:43 +0300 Subject: [PATCH 07/22] NeoModules: integrate NotaryAssisted attribute Port the https://github.com/neo-project/neo-modules/pull/884. Signed-off-by: Anna Shaleva --- src/Plugins/RpcClient/Utility.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Plugins/RpcClient/Utility.cs b/src/Plugins/RpcClient/Utility.cs index 659942f8f8..eba6f0e7c9 100644 --- a/src/Plugins/RpcClient/Utility.cs +++ b/src/Plugins/RpcClient/Utility.cs @@ -206,6 +206,10 @@ public static TransactionAttribute TransactionAttributeFromJson(JObject json) { Hash = UInt256.Parse(json["hash"].AsString()) }, + TransactionAttributeType.NotaryAssisted => new NotaryAssisted() + { + NKeys = (byte)json["nkeys"].AsNumber() + }, _ => throw new FormatException(), }; } From 151f859ea0a9089d1efad15ba1620e4fbacc37b6 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 10 Jun 2024 19:41:26 +0300 Subject: [PATCH 08/22] Payloads: fix XML comment formatting 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 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 c229cd5939ed2f535a73fe6fa9e9bac97ea3a988 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 11 Jun 2024 09:37:07 +0300 Subject: [PATCH 09/22] P2P: move NotaryAssisted transaction attribute under D hardfork Signed-off-by: Anna Shaleva --- src/Neo/Network/P2P/Payloads/Transaction.cs | 7 +- .../SmartContract/Native/PolicyContract.cs | 76 +++++++++++++++++-- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/Neo/Network/P2P/Payloads/Transaction.cs b/src/Neo/Network/P2P/Payloads/Transaction.cs index b922674d91..8fc771ce7c 100644 --- a/src/Neo/Network/P2P/Payloads/Transaction.cs +++ b/src/Neo/Network/P2P/Payloads/Transaction.cs @@ -370,10 +370,13 @@ public virtual VerifyResult VerifyStateDependent(ProtocolSettings settings, Data if (!(context?.CheckTransaction(this, conflictsList, snapshot) ?? true)) return VerifyResult.InsufficientFunds; long attributesFee = 0; foreach (TransactionAttribute attribute in Attributes) + { if (!attribute.Verify(snapshot, this)) return VerifyResult.InvalidAttribute; - else - attributesFee += attribute.CalculateNetworkFee(snapshot, this); + if (attribute.Type == TransactionAttributeType.NotaryAssisted && !settings.IsHardforkEnabled(Hardfork.HF_Domovoi, height)) + return VerifyResult.InvalidAttribute; + attributesFee += attribute.CalculateNetworkFee(snapshot, this); + } long netFeeDatoshi = NetworkFee - (Size * NativeContract.Policy.GetFeePerByte(snapshot)) - attributesFee; if (netFeeDatoshi < 0) return VerifyResult.InsufficientFunds; diff --git a/src/Neo/SmartContract/Native/PolicyContract.cs b/src/Neo/SmartContract/Native/PolicyContract.cs index adeb2f1c19..7dcabe2771 100644 --- a/src/Neo/SmartContract/Native/PolicyContract.cs +++ b/src/Neo/SmartContract/Native/PolicyContract.cs @@ -79,6 +79,9 @@ internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfor engine.Snapshot.Add(CreateStorageKey(Prefix_FeePerByte), new StorageItem(DefaultFeePerByte)); engine.Snapshot.Add(CreateStorageKey(Prefix_ExecFeeFactor), new StorageItem(DefaultExecFeeFactor)); engine.Snapshot.Add(CreateStorageKey(Prefix_StoragePrice), new StorageItem(DefaultStoragePrice)); + } + if (hardfork == Hardfork.HF_Domovoi) + { engine.Snapshot.Add(CreateStorageKey(Prefix_AttributeFee).Add((byte)TransactionAttributeType.NotaryAssisted), new StorageItem(DefaultNotaryAssistedAttributeFee)); } return ContractTask.CompletedTask; @@ -118,15 +121,41 @@ public uint GetStoragePrice(DataCache snapshot) } /// - /// Gets the fee for attribute. + /// Gets the fee for attribute before Domovoi hardfork. + /// + /// The snapshot used to read data. + /// Attribute type excluding + /// The fee for attribute. + [ContractMethod(Hardfork.HF_Domovoi, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates, Name = "getAttributeFee")] + public uint GetAttributeFeeV0(DataCache snapshot, byte attributeType) + { + return GetAttributeFee(snapshot, attributeType, false); + } + + /// + /// Gets the fee for attribute after Domovoi hardfork. /// /// The snapshot used to read data. /// Attribute type /// The fee for attribute. - [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + [ContractMethod(true, Hardfork.HF_Domovoi, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] public uint GetAttributeFee(DataCache snapshot, byte attributeType) { - if (!Enum.IsDefined(typeof(TransactionAttributeType), attributeType)) throw new InvalidOperationException(); + return GetAttributeFee(snapshot, attributeType, true); + } + + /// + /// Generic handler for GetAttributeFeeV0 and GetAttributeFeeV1 that + /// gets the fee for attribute. + /// + /// The snapshot used to read data. + /// Attribute type + /// Whether to support attribute type. + /// The fee for attribute. + private uint GetAttributeFee(DataCache snapshot, byte attributeType, bool allowNotaryAssisted) + { + if (!Enum.IsDefined(typeof(TransactionAttributeType), attributeType) || (!allowNotaryAssisted && attributeType == (byte)(TransactionAttributeType.NotaryAssisted))) + throw new InvalidOperationException(); StorageItem entry = snapshot.TryGet(CreateStorageKey(Prefix_AttributeFee).Add(attributeType)); if (entry == null) return DefaultAttributeFee; @@ -145,10 +174,45 @@ public bool IsBlocked(DataCache snapshot, UInt160 account) return snapshot.Contains(CreateStorageKey(Prefix_BlockedAccount).Add(account)); } - [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] - private void SetAttributeFee(ApplicationEngine engine, byte attributeType, uint value) + /// + /// Sets the fee for attribute before Domovoi hardfork. + /// + /// The engine used to check committee witness and read data. + /// Attribute type excluding + /// Attribute fee value + /// The fee for attribute. + [ContractMethod(Hardfork.HF_Domovoi, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States, Name = "setAttributeFee")] + private void SetAttributeFeeV0(ApplicationEngine engine, byte attributeType, uint value) + { + SetAttributeFee(engine, attributeType, value, false); + } + + /// + /// Sets the fee for attribute after Domovoi hardfork. + /// + /// The engine used to check committee witness and read data. + /// Attribute type excluding + /// Attribute fee value + /// The fee for attribute. + [ContractMethod(true, Hardfork.HF_Domovoi, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States, Name = "setAttributeFee")] + private void SetAttributeFeeV1(ApplicationEngine engine, byte attributeType, uint value) + { + SetAttributeFee(engine, attributeType, value, true); + } + + /// + /// Generic handler for SetAttributeFeeV0 and SetAttributeFeeV1 that + /// gets the fee for attribute. + /// + /// The engine used to check committee witness and read data. + /// Attribute type + /// Attribute fee value + /// Whether to support attribute type. + /// The fee for attribute. + private void SetAttributeFee(ApplicationEngine engine, byte attributeType, uint value, bool allowNotaryAssisted) { - if (!Enum.IsDefined(typeof(TransactionAttributeType), attributeType)) throw new InvalidOperationException(); + if (!Enum.IsDefined(typeof(TransactionAttributeType), attributeType) || (!allowNotaryAssisted && attributeType == (byte)(TransactionAttributeType.NotaryAssisted))) + throw new InvalidOperationException(); if (value > MaxAttributeFee) throw new ArgumentOutOfRangeException(nameof(value)); if (!CheckCommittee(engine)) throw new InvalidOperationException(); From 4d5cee903fa77965d590a131243136b227c75c4b Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 24 Jun 2024 19:20:40 +0300 Subject: [PATCH 10/22] P2P: move NotaryAssisted transaction attribute under E hardfork D hardfork was occupied by 3.7.5, thus use the next available one. Signed-off-by: Anna Shaleva --- src/Neo/Hardfork.cs | 3 ++- src/Neo/Network/P2P/Payloads/Transaction.cs | 4 ++-- src/Neo/SmartContract/Native/PolicyContract.cs | 18 +++++++++--------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Neo/Hardfork.cs b/src/Neo/Hardfork.cs index 7bd3cc0aef..cc88a154d1 100644 --- a/src/Neo/Hardfork.cs +++ b/src/Neo/Hardfork.cs @@ -16,6 +16,7 @@ public enum Hardfork : byte HF_Aspidochelone, HF_Basilisk, HF_Cockatrice, - HF_Domovoi + HF_Domovoi, + HF_Echidna, } } diff --git a/src/Neo/Network/P2P/Payloads/Transaction.cs b/src/Neo/Network/P2P/Payloads/Transaction.cs index 8fc771ce7c..d3e3b06a8f 100644 --- a/src/Neo/Network/P2P/Payloads/Transaction.cs +++ b/src/Neo/Network/P2P/Payloads/Transaction.cs @@ -371,9 +371,9 @@ public virtual VerifyResult VerifyStateDependent(ProtocolSettings settings, Data long attributesFee = 0; foreach (TransactionAttribute attribute in Attributes) { - if (!attribute.Verify(snapshot, this)) + if (attribute.Type == TransactionAttributeType.NotaryAssisted && !settings.IsHardforkEnabled(Hardfork.HF_Echidna, height)) return VerifyResult.InvalidAttribute; - if (attribute.Type == TransactionAttributeType.NotaryAssisted && !settings.IsHardforkEnabled(Hardfork.HF_Domovoi, height)) + if (!attribute.Verify(snapshot, this)) return VerifyResult.InvalidAttribute; attributesFee += attribute.CalculateNetworkFee(snapshot, this); } diff --git a/src/Neo/SmartContract/Native/PolicyContract.cs b/src/Neo/SmartContract/Native/PolicyContract.cs index 7dcabe2771..755dd3a30f 100644 --- a/src/Neo/SmartContract/Native/PolicyContract.cs +++ b/src/Neo/SmartContract/Native/PolicyContract.cs @@ -80,7 +80,7 @@ internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfor engine.Snapshot.Add(CreateStorageKey(Prefix_ExecFeeFactor), new StorageItem(DefaultExecFeeFactor)); engine.Snapshot.Add(CreateStorageKey(Prefix_StoragePrice), new StorageItem(DefaultStoragePrice)); } - if (hardfork == Hardfork.HF_Domovoi) + if (hardfork == Hardfork.HF_Echidna) { engine.Snapshot.Add(CreateStorageKey(Prefix_AttributeFee).Add((byte)TransactionAttributeType.NotaryAssisted), new StorageItem(DefaultNotaryAssistedAttributeFee)); } @@ -121,24 +121,24 @@ public uint GetStoragePrice(DataCache snapshot) } /// - /// Gets the fee for attribute before Domovoi hardfork. + /// Gets the fee for attribute before Echidna hardfork. /// /// The snapshot used to read data. /// Attribute type excluding /// The fee for attribute. - [ContractMethod(Hardfork.HF_Domovoi, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates, Name = "getAttributeFee")] + [ContractMethod(Hardfork.HF_Echidna, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates, Name = "getAttributeFee")] public uint GetAttributeFeeV0(DataCache snapshot, byte attributeType) { return GetAttributeFee(snapshot, attributeType, false); } /// - /// Gets the fee for attribute after Domovoi hardfork. + /// Gets the fee for attribute after Echidna hardfork. /// /// The snapshot used to read data. /// Attribute type /// The fee for attribute. - [ContractMethod(true, Hardfork.HF_Domovoi, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + [ContractMethod(true, Hardfork.HF_Echidna, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] public uint GetAttributeFee(DataCache snapshot, byte attributeType) { return GetAttributeFee(snapshot, attributeType, true); @@ -175,26 +175,26 @@ public bool IsBlocked(DataCache snapshot, UInt160 account) } /// - /// Sets the fee for attribute before Domovoi hardfork. + /// Sets the fee for attribute before Echidna hardfork. /// /// The engine used to check committee witness and read data. /// Attribute type excluding /// Attribute fee value /// The fee for attribute. - [ContractMethod(Hardfork.HF_Domovoi, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States, Name = "setAttributeFee")] + [ContractMethod(Hardfork.HF_Echidna, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States, Name = "setAttributeFee")] private void SetAttributeFeeV0(ApplicationEngine engine, byte attributeType, uint value) { SetAttributeFee(engine, attributeType, value, false); } /// - /// Sets the fee for attribute after Domovoi hardfork. + /// Sets the fee for attribute after Echidna hardfork. /// /// The engine used to check committee witness and read data. /// Attribute type excluding /// Attribute fee value /// The fee for attribute. - [ContractMethod(true, Hardfork.HF_Domovoi, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States, Name = "setAttributeFee")] + [ContractMethod(true, Hardfork.HF_Echidna, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States, Name = "setAttributeFee")] private void SetAttributeFeeV1(ApplicationEngine engine, byte attributeType, uint value) { SetAttributeFee(engine, attributeType, value, true); From 8e33ca4cef8b1c638cf525fa7d5552d56d84b584 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 16 Jan 2025 23:26:15 +0300 Subject: [PATCH 11/22] tests: fix build errors No functional changes, just build fixes required by updated master branch. Signed-off-by: Anna Shaleva --- .../Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs | 5 +++-- tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs index 778fe7a53d..b578268155 100644 --- a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs @@ -11,6 +11,7 @@ using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Extensions; using Neo.IO; using Neo.Network.P2P.Payloads; using Neo.SmartContract; @@ -72,7 +73,7 @@ public void Verify() // Temporary use Notary contract hash stub for valid transaction. var txGood = new Transaction { Signers = new Signer[] { new Signer() { Account = notaryHash } } }; var txBad = new Transaction { Signers = new Signer[] { new Signer() { Account = UInt160.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01") } } }; - var snapshot = TestBlockchain.GetTestSnapshot(); + var snapshot = TestBlockchain.GetTestSnapshotCache(); Assert.IsTrue(attr.Verify(snapshot, txGood)); Assert.IsFalse(attr.Verify(snapshot, txBad)); @@ -81,7 +82,7 @@ public void Verify() [TestMethod] public void CalculateNetworkFee() { - var snapshot = TestBlockchain.GetTestSnapshot(); + var snapshot = TestBlockchain.GetTestSnapshotCache(); var attr = new NotaryAssisted() { NKeys = 4 }; var tx = new Transaction { Signers = new Signer[] { new Signer() { Account = notaryHash } } }; diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs index 95a6edf0d4..76b4384955 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs @@ -203,7 +203,7 @@ public void Check_OnPersist_NotaryAssisted() }, Transactions = new Transaction[] { tx1, tx2 } }; - var snapshot = _snapshot.CreateSnapshot(); + var snapshot = _snapshotCache.CloneCache(); var script = new ScriptBuilder(); script.EmitSysCall(ApplicationEngine.System_Contract_NativeOnPersist); var engine = ApplicationEngine.Create(TriggerType.OnPersist, null, snapshot, persistingBlock, settings: TestBlockchain.TheNeoSystem.Settings); From 08a41ceebbae83d6a6efca8e574d26f395bfef13 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 16 Jan 2025 23:31:05 +0300 Subject: [PATCH 12/22] NotaryAssisted: update copyright date Signed-off-by: Anna Shaleva --- src/Neo/Network/P2P/Payloads/NotaryAssisted.cs | 2 +- tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs index 0a110688a8..1ce7b91b57 100644 --- a/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs +++ b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015-2024 The Neo Project. +// Copyright (C) 2015-2025 The Neo Project. // // NotaryAssisted.cs file belongs to the neo project and is free // software distributed under the MIT software license, see the diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs index b578268155..028c2ae362 100644 --- a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2015-2024 The Neo Project. +// Copyright (C) 2015-2025 The Neo Project. // // UT_NotaryAssisted.cs file belongs to the neo project and is free // software distributed under the MIT software license, see the From ec4e3d63fa605657ff080874e990909e13b2e7c5 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 23 Jan 2025 16:58:43 +0300 Subject: [PATCH 13/22] SmartContract: refactor obsolete code Signed-off-by: Anna Shaleva --- src/Neo/SmartContract/Native/GasToken.cs | 2 +- src/Neo/SmartContract/Native/PolicyContract.cs | 2 +- .../SmartContract/Native/UT_GasToken.cs | 16 +++------------- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/Neo/SmartContract/Native/GasToken.cs b/src/Neo/SmartContract/Native/GasToken.cs index 9d5ef7d88e..313e789b9f 100644 --- a/src/Neo/SmartContract/Native/GasToken.cs +++ b/src/Neo/SmartContract/Native/GasToken.cs @@ -49,7 +49,7 @@ internal override async ContractTask OnPersistAsync(ApplicationEngine engine) var notaryAssisted = tx.GetAttribute(); if (notaryAssisted is not null) { - totalNetworkFee -= (notaryAssisted.NKeys + 1) * Policy.GetAttributeFee(engine.Snapshot, (byte)notaryAssisted.Type); + totalNetworkFee -= (notaryAssisted.NKeys + 1) * Policy.GetAttributeFee(engine.SnapshotCache, (byte)notaryAssisted.Type); } } ECPoint[] validators = NEO.GetNextBlockValidators(engine.SnapshotCache, engine.ProtocolSettings.ValidatorsCount); diff --git a/src/Neo/SmartContract/Native/PolicyContract.cs b/src/Neo/SmartContract/Native/PolicyContract.cs index c958fb3a7e..78171217a1 100644 --- a/src/Neo/SmartContract/Native/PolicyContract.cs +++ b/src/Neo/SmartContract/Native/PolicyContract.cs @@ -92,7 +92,7 @@ internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfor } if (hardfork == Hardfork.HF_Echidna) { - engine.Snapshot.Add(CreateStorageKey(Prefix_AttributeFee).Add((byte)TransactionAttributeType.NotaryAssisted), new StorageItem(DefaultNotaryAssistedAttributeFee)); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_AttributeFee).Add((byte)TransactionAttributeType.NotaryAssisted), new StorageItem(DefaultNotaryAssistedAttributeFee)); } return ContractTask.CompletedTask; } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs index 663d13e761..0ee1cf6227 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs @@ -15,25 +15,15 @@ using Neo.Extensions; 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; @@ -209,9 +199,9 @@ public void Check_OnPersist_NotaryAssisted() 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); + ECPoint[] validators = NativeContract.NEO.GetNextBlockValidators(engine.SnapshotCache, engine.ProtocolSettings.ValidatorsCount); var primary = Contract.CreateSignatureRedeemScript(validators[engine.PersistingBlock.PrimaryIndex]).ToScriptHash(); - NativeContract.GAS.BalanceOf(engine.Snapshot, primary).Should().Be(0); + NativeContract.GAS.BalanceOf(engine.SnapshotCache, primary).Should().Be(0); // Execute OnPersist script. engine.LoadScript(script.ToArray()); @@ -221,7 +211,7 @@ public void Check_OnPersist_NotaryAssisted() // 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); + NativeContract.GAS.BalanceOf(engine.SnapshotCache, primary).Should().Be(netFee1 + netFee2 - expectedNotaryReward); } } } From bf4b294939b428b6d9c387cd60de8805eed1a26f Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 23 Jan 2025 17:00:50 +0300 Subject: [PATCH 14/22] Persistance: fix UT Related to additional storage entries added by the previous changes. Signed-off-by: Anna Shaleva --- tests/Neo.UnitTests/Persistence/UT_MemoryStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Neo.UnitTests/Persistence/UT_MemoryStore.cs b/tests/Neo.UnitTests/Persistence/UT_MemoryStore.cs index c39d6cfd41..c2d0c51b27 100644 --- a/tests/Neo.UnitTests/Persistence/UT_MemoryStore.cs +++ b/tests/Neo.UnitTests/Persistence/UT_MemoryStore.cs @@ -136,7 +136,7 @@ public void NeoSystemStoreGetAndChange() var entries = storeView.Seek([], SeekDirection.Backward).ToArray(); // Memory store has different seek behavior than the snapshot - Assert.AreEqual(entries.Length, 37); + Assert.AreEqual(entries.Length, 38); } } } From d5f37df18e46a56c6e9a26ed90567410537ce833 Mon Sep 17 00:00:00 2001 From: Shargon Date: Thu, 23 Jan 2025 06:03:02 -0800 Subject: [PATCH 15/22] Clean comments --- tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs index 0ee1cf6227..3f7731d551 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs @@ -26,7 +26,6 @@ using System.Numerics; using System.Threading.Tasks; using VMTypes = Neo.VM.Types; -// using VMArray = Neo.VM.Types.Array; namespace Neo.UnitTests.SmartContract.Native { From 5648a9b20ad8c508b1a88bc030152df9791581e0 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Sun, 16 Feb 2025 20:20:19 +0300 Subject: [PATCH 16/22] Native: improve exception message for Policy's getFee/setFee Signed-off-by: Anna Shaleva --- src/Neo/SmartContract/Native/PolicyContract.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Neo/SmartContract/Native/PolicyContract.cs b/src/Neo/SmartContract/Native/PolicyContract.cs index b56f77986a..b591b56640 100644 --- a/src/Neo/SmartContract/Native/PolicyContract.cs +++ b/src/Neo/SmartContract/Native/PolicyContract.cs @@ -165,7 +165,7 @@ public uint GetAttributeFee(IReadOnlyStore snapshot, byte attributeType) private uint GetAttributeFee(IReadOnlyStore snapshot, byte attributeType, bool allowNotaryAssisted) { if (!Enum.IsDefined(typeof(TransactionAttributeType), attributeType) || (!allowNotaryAssisted && attributeType == (byte)(TransactionAttributeType.NotaryAssisted))) - throw new InvalidOperationException(); + throw new InvalidOperationException($"Unsupported value {attributeType} of {nameof(attributeType)}"); var key = CreateStorageKey(Prefix_AttributeFee).Add(attributeType); return snapshot.TryGet(key, out var item) ? (uint)(BigInteger)item : DefaultAttributeFee; @@ -221,7 +221,7 @@ private void SetAttributeFeeV1(ApplicationEngine engine, byte attributeType, uin private void SetAttributeFee(ApplicationEngine engine, byte attributeType, uint value, bool allowNotaryAssisted) { if (!Enum.IsDefined(typeof(TransactionAttributeType), attributeType) || (!allowNotaryAssisted && attributeType == (byte)(TransactionAttributeType.NotaryAssisted))) - throw new InvalidOperationException(); + throw new InvalidOperationException($"Unsupported value {attributeType} of {nameof(attributeType)}"); if (value > MaxAttributeFee) throw new ArgumentOutOfRangeException(nameof(value)); if (!CheckCommittee(engine)) throw new InvalidOperationException(); From 9385746300a146c92c463f789a3d8b188e1b435a Mon Sep 17 00:00:00 2001 From: Shargon Date: Sun, 16 Feb 2025 18:24:52 -0800 Subject: [PATCH 17/22] EC Recover (#3696) * EC Recover * Clean code * Fix ut * Fix ut * Unify hashing * Allow HashAlgorithm.None * Clean comment * Update src/Neo/Cryptography/Crypto.cs * Fix merge * Add None check * Fix GetHash * fix unit testing issues * Clean code * Update src/Neo/SmartContract/Native/CryptoLib.cs --------- Co-authored-by: Jimmy --- src/Neo/Cryptography/Crypto.cs | 180 ++++++++++++++---- src/Neo/Cryptography/ECC/ECCurve.cs | 16 +- src/Neo/Cryptography/HashAlgorithm.cs | 2 +- src/Neo/SmartContract/Native/CryptoLib.cs | 24 +++ .../Cryptography/ECC/UT_ECFieldElement.cs | 8 +- tests/Neo.UnitTests/Cryptography/UT_Crypto.cs | 148 ++++++++++++-- .../SmartContract/Native/UT_NativeContract.cs | 2 +- 7 files changed, 326 insertions(+), 54 deletions(-) diff --git a/src/Neo/Cryptography/Crypto.cs b/src/Neo/Cryptography/Crypto.cs index 723a3b3e10..18c400c828 100644 --- a/src/Neo/Cryptography/Crypto.cs +++ b/src/Neo/Cryptography/Crypto.cs @@ -15,7 +15,9 @@ using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Signers; using Org.BouncyCastle.Math; +using Org.BouncyCastle.Utilities.Encoders; using System; +using System.Linq; using System.Runtime.InteropServices; using System.Security.Cryptography; using ECPoint = Neo.Cryptography.ECC.ECPoint; @@ -27,22 +29,15 @@ namespace Neo.Cryptography /// public static class Crypto { + private static readonly BigInteger s_prime = new(1, + Hex.Decode("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F")); + private static readonly ECDsaCache CacheECDsa = new(); private static readonly bool IsOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); private static readonly ECCurve secP256k1 = ECCurve.CreateFromFriendlyName("secP256k1"); private static readonly X9ECParameters bouncySecp256k1 = SecNamedCurves.GetByName("secp256k1"); private static readonly X9ECParameters bouncySecp256r1 = SecNamedCurves.GetByName("secp256r1"); - /// - /// Holds domain parameters for Secp256r1 elliptic curve. - /// - private static readonly ECDomainParameters secp256r1DomainParams = new ECDomainParameters(bouncySecp256r1.Curve, bouncySecp256r1.G, bouncySecp256r1.N, bouncySecp256r1.H); - - /// - /// Holds domain parameters for Secp256k1 elliptic curve. - /// - private static readonly ECDomainParameters secp256k1DomainParams = new ECDomainParameters(bouncySecp256k1.Curve, bouncySecp256k1.G, bouncySecp256k1.N, bouncySecp256k1.H); - /// /// Calculates the 160-bit hash value of the specified message. /// @@ -89,18 +84,11 @@ public static byte[] Sign(byte[] message, byte[] priKey, ECC.ECCurve ecCurve = n { if (hashAlgorithm == HashAlgorithm.Keccak256 || (IsOSX && ecCurve == ECC.ECCurve.Secp256k1)) { - var domain = - ecCurve == null || ecCurve == ECC.ECCurve.Secp256r1 ? secp256r1DomainParams : - ecCurve == ECC.ECCurve.Secp256k1 ? secp256k1DomainParams : - throw new NotSupportedException(nameof(ecCurve)); var signer = new ECDsaSigner(); var privateKey = new BigInteger(1, priKey); - var priKeyParameters = new ECPrivateKeyParameters(privateKey, domain); + var priKeyParameters = new ECPrivateKeyParameters(privateKey, ecCurve.BouncyCastleDomainParams); signer.Init(true, priKeyParameters); - var messageHash = - hashAlgorithm == HashAlgorithm.SHA256 ? message.Sha256() : - hashAlgorithm == HashAlgorithm.Keccak256 ? message.Keccak256() : - throw new NotSupportedException(nameof(hashAlgorithm)); + var messageHash = GetMessageHash(message, hashAlgorithm); var signature = signer.GenerateSignature(messageHash); var signatureBytes = new byte[64]; @@ -157,30 +145,17 @@ public static bool VerifySignature(ReadOnlySpan message, ReadOnlySpan message, ReadOnlySpan + /// Get hash from message. + /// + /// Original message + /// The hash algorithm to be used hash the message, the default is SHA256. + /// Hashed message + public static byte[] GetMessageHash(byte[] message, HashAlgorithm hashAlgorithm = HashAlgorithm.SHA256) + { + return hashAlgorithm switch + { + HashAlgorithm.SHA256 => message.Sha256(), + HashAlgorithm.Keccak256 => message.Keccak256(), + _ => throw new NotSupportedException(nameof(hashAlgorithm)) + }; + } + + /// + /// Get hash from message. + /// + /// Original message + /// The hash algorithm to be used hash the message, the default is SHA256. + /// Hashed message + public static byte[] GetMessageHash(ReadOnlySpan message, HashAlgorithm hashAlgorithm = HashAlgorithm.SHA256) + { + return hashAlgorithm switch + { + HashAlgorithm.SHA256 => message.Sha256(), + HashAlgorithm.Keccak256 => message.Keccak256(), + _ => throw new NotSupportedException(nameof(hashAlgorithm)) + }; + } + + /// + /// Recovers the public key from a signature and message hash. + /// + /// Signature, either 65 bytes (r[32] || s[32] || v[1]) or + /// 64 bytes in “compact” form (r[32] || yParityAndS[32]). + /// 32-byte message hash + /// The recovered public key + /// Thrown if signature or hash is invalid + public static ECC.ECPoint ECRecover(byte[] signature, byte[] hash) + { + if (signature.Length != 65 && signature.Length != 64) + throw new ArgumentException("Signature must be 65 or 64 bytes", nameof(signature)); + if (hash.Length != 32) + throw new ArgumentException("Message hash must be 32 bytes", nameof(hash)); + + try + { + // Extract (r, s) and compute integer recId + BigInteger r, s; + int recId; + + if (signature.Length == 65) + { + // Format: r[32] || s[32] || v[1] + r = new BigInteger(1, [.. signature.Take(32)]); + s = new BigInteger(1, [.. signature.Skip(32).Take(32)]); + + // v could be 0..3 or 27..30 (Ethereum style). + var v = signature[64]; + recId = v >= 27 ? v - 27 : v; // normalize + if (recId < 0 || recId > 3) + throw new ArgumentException("Recovery value must be in [0..3] after normalization.", nameof(signature)); + } + else + { + // 64 bytes “compact” format: r[32] || yParityAndS[32] + // yParity is fused into the top bit of s. + + r = new BigInteger(1, [.. signature.Take(32)]); + var yParityAndS = new BigInteger(1, signature.Skip(32).ToArray()); + + // Mask out top bit to get s + var mask = BigInteger.One.ShiftLeft(255).Subtract(BigInteger.One); + s = yParityAndS.And(mask); + + // Extract yParity (0 or 1) + var yParity = yParityAndS.TestBit(255); + + // For “compact,” map parity to recId in [0..1]. + // For typical usage, recId in {0,1} is enough: + recId = yParity ? 1 : 0; + } + + // Decompose recId into i = recId >> 1 and yBit = recId & 1 + var iPart = recId >> 1; // usually 0..1 + var yBit = (recId & 1) == 1; + + // BouncyCastle curve constants + var n = ECC.ECCurve.Secp256k1.BouncyCastleCurve.N; + var e = new BigInteger(1, hash); + + // eInv = -e mod n + var eInv = BigInteger.Zero.Subtract(e).Mod(n); + // rInv = (r^-1) mod n + var rInv = r.ModInverse(n); + // srInv = (s * r^-1) mod n + var srInv = rInv.Multiply(s).Mod(n); + // eInvrInv = (eInv * r^-1) mod n + var eInvrInv = rInv.Multiply(eInv).Mod(n); + + // x = r + iPart * n + var x = r.Add(BigInteger.ValueOf(iPart).Multiply(n)); + // Verify x is within the curve prime + if (x.CompareTo(s_prime) >= 0) + throw new ArgumentException("x is out of range of the secp256k1 prime.", nameof(signature)); + + // Decompress to get R + var decompressedRKey = DecompressKey(ECC.ECCurve.Secp256k1.BouncyCastleCurve.Curve, x, yBit); + // Check that R is on curve + if (!decompressedRKey.Multiply(n).IsInfinity) + throw new ArgumentException("R point is not valid on this curve.", nameof(signature)); + + // Q = (eInv * G) + (srInv * R) + var q = Org.BouncyCastle.Math.EC.ECAlgorithms.SumOfTwoMultiplies( + ECC.ECCurve.Secp256k1.BouncyCastleCurve.G, eInvrInv, + decompressedRKey, srInv); + + return ECPoint.FromBytes(q.Normalize().GetEncoded(false), ECC.ECCurve.Secp256k1); + } + catch (Exception ex) + { + throw new ArgumentException("Invalid signature parameters", nameof(signature), ex); + } + } + + private static Org.BouncyCastle.Math.EC.ECPoint DecompressKey( + Org.BouncyCastle.Math.EC.ECCurve curve, BigInteger xBN, bool yBit) + { + var compEnc = X9IntegerConverter.IntegerToBytes(xBN, 1 + X9IntegerConverter.GetByteLength(curve)); + compEnc[0] = (byte)(yBit ? 0x03 : 0x02); + return curve.DecodePoint(compEnc); + } } } diff --git a/src/Neo/Cryptography/ECC/ECCurve.cs b/src/Neo/Cryptography/ECC/ECCurve.cs index c7b9dbe9bc..df7f8aa8e1 100644 --- a/src/Neo/Cryptography/ECC/ECCurve.cs +++ b/src/Neo/Cryptography/ECC/ECCurve.cs @@ -10,6 +10,7 @@ // modifications are permitted. using Neo.Extensions; +using Org.BouncyCastle.Crypto.Parameters; using System.Globalization; using System.Numerics; @@ -33,9 +34,14 @@ public class ECCurve /// public readonly ECPoint G; + public readonly Org.BouncyCastle.Asn1.X9.X9ECParameters BouncyCastleCurve; + /// + /// Holds domain parameters for Secp256r1 elliptic curve. + /// + public readonly ECDomainParameters BouncyCastleDomainParams; internal readonly int ExpectedECPointLength; - private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G) + private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G, string curveName) { this.Q = Q; ExpectedECPointLength = ((int)VM.Utility.GetBitLength(Q) + 7) / 8; @@ -44,6 +50,8 @@ private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G this.N = N; Infinity = new ECPoint(null, null, this); this.G = ECPoint.DecodePoint(G, this); + BouncyCastleCurve = Org.BouncyCastle.Asn1.Sec.SecNamedCurves.GetByName(curveName); + BouncyCastleDomainParams = new ECDomainParameters(BouncyCastleCurve.Curve, BouncyCastleCurve.G, BouncyCastleCurve.N, BouncyCastleCurve.H); } /// @@ -55,7 +63,8 @@ private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G BigInteger.Zero, 7, BigInteger.Parse("00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", NumberStyles.AllowHexSpecifier), - ("04" + "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798" + "483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8").HexToBytes() + ("04" + "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798" + "483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8").HexToBytes(), + "secp256k1" ); /// @@ -67,7 +76,8 @@ private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", NumberStyles.AllowHexSpecifier), BigInteger.Parse("005AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", NumberStyles.AllowHexSpecifier), BigInteger.Parse("00FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", NumberStyles.AllowHexSpecifier), - ("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes() + ("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes(), + "secp256r1" ); } } diff --git a/src/Neo/Cryptography/HashAlgorithm.cs b/src/Neo/Cryptography/HashAlgorithm.cs index 361dbd3b64..41a352b3f2 100644 --- a/src/Neo/Cryptography/HashAlgorithm.cs +++ b/src/Neo/Cryptography/HashAlgorithm.cs @@ -21,6 +21,6 @@ public enum HashAlgorithm : byte /// /// The Keccak256 hash algorithm. /// - Keccak256 = 0x01, + Keccak256 = 0x01 } } diff --git a/src/Neo/SmartContract/Native/CryptoLib.cs b/src/Neo/SmartContract/Native/CryptoLib.cs index 845eabb3c6..bb355d0805 100644 --- a/src/Neo/SmartContract/Native/CryptoLib.cs +++ b/src/Neo/SmartContract/Native/CryptoLib.cs @@ -33,6 +33,30 @@ public sealed partial class CryptoLib : NativeContract internal CryptoLib() : base() { } + /// + /// Recovers the public key from a secp256k1 signature in a single byte array format. + /// + /// The hash of the message that was signed. + /// The 65-byte signature in format: r[32] + s[32] + v[1]. 64-bytes for eip-2098, where v must be 27 or 28. + /// The recovered public key in compressed format, or null if recovery fails. + [ContractMethod(Hardfork.HF_Echidna, CpuFee = 1 << 15, Name = "recoverSecp256K1")] + public static byte[] RecoverSecp256K1(byte[] messageHash, byte[] signature) + { + // It will be checked in Crypto.ECRecover + // if (signature.Length != 65 && signature.Length != 64) + // throw new ArgumentException("Signature must be 65 or 64 bytes", nameof(signature)); + + try + { + var point = Crypto.ECRecover(signature, messageHash); + return point.EncodePoint(true); + } + catch + { + return null; + } + } + /// /// Computes the hash value for the specified byte array using the ripemd160 algorithm. /// diff --git a/tests/Neo.UnitTests/Cryptography/ECC/UT_ECFieldElement.cs b/tests/Neo.UnitTests/Cryptography/ECC/UT_ECFieldElement.cs index 80a33aad2d..b43b32558a 100644 --- a/tests/Neo.UnitTests/Cryptography/ECC/UT_ECFieldElement.cs +++ b/tests/Neo.UnitTests/Cryptography/ECC/UT_ECFieldElement.cs @@ -87,13 +87,15 @@ public void TestSqrt() ECFieldElement element = new(new BigInteger(100), ECCurve.Secp256k1); Assert.AreEqual(new ECFieldElement(BigInteger.Parse("115792089237316195423570985008687907853269984665640564039457584007908834671653"), ECCurve.Secp256k1), element.Sqrt()); - ConstructorInfo constructor = typeof(ECCurve).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(byte[]) }, null); - ECCurve testCruve = constructor.Invoke(new object[] { + var constructor = typeof(ECCurve).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, + [typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(byte[]), typeof(string)], null); + var testCruve = constructor.Invoke([ BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFF0", NumberStyles.AllowHexSpecifier), BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFF00", NumberStyles.AllowHexSpecifier), BigInteger.Parse("005AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", NumberStyles.AllowHexSpecifier), BigInteger.Parse("00FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", NumberStyles.AllowHexSpecifier), - ("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes() }) as ECCurve; + ("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes(), + "secp256k1"]) as ECCurve; element = new ECFieldElement(new BigInteger(200), testCruve); Assert.IsNull(element.Sqrt()); } diff --git a/tests/Neo.UnitTests/Cryptography/UT_Crypto.cs b/tests/Neo.UnitTests/Cryptography/UT_Crypto.cs index 1a046a1a5f..8748ad04ab 100644 --- a/tests/Neo.UnitTests/Cryptography/UT_Crypto.cs +++ b/tests/Neo.UnitTests/Cryptography/UT_Crypto.cs @@ -14,6 +14,7 @@ using Neo.Extensions; using Neo.Wallets; using System; +using System.Linq; using System.Security.Cryptography; using System.Text; using ECCurve = Neo.Cryptography.ECC.ECCurve; @@ -23,15 +24,12 @@ namespace Neo.UnitTests.Cryptography [TestClass] public class UT_Crypto { - private KeyPair key = null; - private readonly byte[] message = Encoding.Default.GetBytes("HelloWorld"); - private readonly byte[] wrongKey = new byte[33]; - private readonly byte[] wrongKey2 = new byte[36]; + private KeyPair _key = null; public static KeyPair GenerateKey(int privateKeyLength) { - byte[] privateKey = new byte[privateKeyLength]; - using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + var privateKey = new byte[privateKeyLength]; + using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(privateKey); } @@ -40,8 +38,8 @@ public static KeyPair GenerateKey(int privateKeyLength) public static KeyPair GenerateCertainKey(int privateKeyLength) { - byte[] privateKey = new byte[privateKeyLength]; - for (int i = 0; i < privateKeyLength; i++) + var privateKey = new byte[privateKeyLength]; + for (var i = 0; i < privateKeyLength; i++) { privateKey[i] = (byte)((byte)i % byte.MaxValue); } @@ -51,15 +49,18 @@ public static KeyPair GenerateCertainKey(int privateKeyLength) [TestInitialize] public void TestSetup() { - key = GenerateKey(32); + _key = GenerateKey(32); } [TestMethod] public void TestVerifySignature() { - byte[] signature = Crypto.Sign(message, key.PrivateKey); - Assert.IsTrue(Crypto.VerifySignature(message, signature, key.PublicKey)); + var message = Encoding.Default.GetBytes("HelloWorld"); + var signature = Crypto.Sign(message, _key.PrivateKey); + Assert.IsTrue(Crypto.VerifySignature(message, signature, _key.PublicKey)); + var wrongKey2 = new byte[36]; + var wrongKey = new byte[33]; wrongKey[0] = 0x02; Assert.IsFalse(Crypto.VerifySignature(message, signature, wrongKey, ECCurve.Secp256r1)); @@ -91,5 +92,130 @@ public void TestSecp256k1() Assert.IsTrue(Crypto.VerifySignature(message, signature, pubKey, ECCurve.Secp256k1)); } + + [TestMethod] + public void TestECRecover() + { + // Test case 1 + var message1 = "5c868fedb8026979ebd26f1ba07c27eedf4ff6d10443505a96ecaf21ba8c4f0937b3cd23ffdc3dd429d4cd1905fb8dbcceeff1350020e18b58d2ba70887baa3a9b783ad30d3fbf210331cdd7df8d77defa398cdacdfc2e359c7ba4cae46bb74401deb417f8b912a1aa966aeeba9c39c7dd22479ae2b30719dca2f2206c5eb4b7".HexToBytes(); + var messageHash1 = "5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0".HexToBytes(); + var signature1 = "45c0b7f8c09a9e1f1cea0c25785594427b6bf8f9f878a8af0b1abbb48e16d0920d8becd0c220f67c51217eecfd7184ef0732481c843857e6bc7fc095c4f6b78801".HexToBytes(); + var expectedPubKey1 = "034a071e8a6e10aada2b8cf39fa3b5fb3400b04e99ea8ae64ceea1a977dbeaf5d5".HexToBytes(); + + var recoveredKey1 = Crypto.ECRecover(signature1, messageHash1); + CollectionAssert.AreEqual(expectedPubKey1, recoveredKey1.EncodePoint(true)); + + // Test case 2 + var message2 = "17cd4a74d724d55355b6fb2b0759ca095298e3fd1856b87ca1cb2df5409058022736d21be071d820b16dfc441be97fbcea5df787edc886e759475469e2128b22f26b82ca993be6695ab190e673285d561d3b6d42fcc1edd6d12db12dcda0823e9d6079e7bc5ff54cd452dad308d52a15ce9c7edd6ef3dad6a27becd8e001e80f".HexToBytes(); + var messageHash2 = "586052916fb6f746e1d417766cceffbe1baf95579bab67ad49addaaa6e798862".HexToBytes(); + var signature2 = "4e0ea79d4a476276e4b067facdec7460d2c98c8a65326a6e5c998fd7c65061140e45aea5034af973410e65cf97651b3f2b976e3fc79c6a93065ed7cb69a2ab5a01".HexToBytes(); + var expectedPubKey2 = "02dbf1f4092deb3cfd4246b2011f7b24840bc5dbedae02f28471ce5b3bfbf06e71".HexToBytes(); + + var recoveredKey2 = Crypto.ECRecover(signature2, messageHash2); + CollectionAssert.AreEqual(expectedPubKey2, recoveredKey2.EncodePoint(true)); + + // Test case 3 - recovery param 0 + var message3 = "db0d31717b04802adbbae1997487da8773440923c09b869e12a57c36dda34af11b8897f266cd81c02a762c6b74ea6aaf45aaa3c52867eb8f270f5092a36b498f88b65b2ebda24afe675da6f25379d1e194d093e7a2f66e450568dbdffebff97c4597a00c96a5be9ba26deefcca8761c1354429622c8db269d6a0ec0cc7a8585c".HexToBytes(); + var messageHash3 = "c36d0ecf4bfd178835c97aae7585f6a87de7dfa23cc927944f99a8d60feff68b".HexToBytes(); + var signature3 = "f25b86e1d8a11d72475b3ed273b0781c7d7f6f9e1dae0dd5d3ee9b84f3fab89163d9c4e1391de077244583e9a6e3d8e8e1f236a3bf5963735353b93b1a3ba93500".HexToBytes(); + var expectedPubKey3 = "03414549fd05bfb7803ae507ff86b99becd36f8d66037a7f5ba612792841d42eb9".HexToBytes(); + + var recoveredKey3 = Crypto.ECRecover(signature3, messageHash3); + CollectionAssert.AreEqual(expectedPubKey3, recoveredKey3.EncodePoint(true)); + + // Test invalid cases + var invalidSignature = new byte[65]; + Assert.ThrowsException(() => Crypto.ECRecover(invalidSignature, messageHash1)); + + // Test with invalid recovery value + var invalidRecoverySignature = signature1.ToArray(); + invalidRecoverySignature[64] = 29; // Invalid recovery value + Assert.ThrowsException(() => Crypto.ECRecover(invalidRecoverySignature, messageHash1)); + + // Test with wrong message hash + var recoveredWrongHash = Crypto.ECRecover(signature1, messageHash2); + CollectionAssert.AreNotEquivalent(expectedPubKey1, recoveredWrongHash.EncodePoint(true)); + } + + [TestMethod] + public void TestERC2098() + { + // Test from https://eips.ethereum.org/EIPS/eip-2098 + + // Private Key: 0x1234567890123456789012345678901234567890123456789012345678901234 + // Message: "Hello World" + // Signature: + // r: 0x68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b90 + // s: 0x7e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064 + // v: 27 + + var privateKey = "1234567890123456789012345678901234567890123456789012345678901234".HexToBytes(); + var expectedPubKey1 = (ECCurve.Secp256k1.G * privateKey); + + Console.WriteLine($"Expected PubKey: {expectedPubKey1.ToArray().ToHexString()}"); + var message1 = Encoding.UTF8.GetBytes("Hello World"); + var messageHash1 = new byte[] { 0x19 }.Concat(Encoding.UTF8.GetBytes($"Ethereum Signed Message:\n{message1.Length}")).Concat(message1).ToArray().Keccak256(); + Console.WriteLine($"Message Hash: {Convert.ToHexString(messageHash1)}"); + + // Signature values from EIP-2098 test case + var r = "68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b90".HexToBytes(); + var s = "7e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064".HexToBytes(); + var signature1 = new byte[65]; + Array.Copy(r, 0, signature1, 0, 32); + Array.Copy(s, 0, signature1, 32, 32); + signature1[64] = 27; + + Console.WriteLine($"r: {Convert.ToHexString(signature1.Take(32).ToArray())}"); + Console.WriteLine($"s: {Convert.ToHexString(signature1.Skip(32).Take(32).ToArray())}"); + Console.WriteLine($"yParity: {(signature1[32] & 0x80) != 0}"); + + var recoveredKey1 = Crypto.ECRecover(signature1, messageHash1); + CollectionAssert.AreEqual(expectedPubKey1.EncodePoint(true), recoveredKey1.EncodePoint(true)); + + var sig = "68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b90".HexToBytes() + .Concat("7e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064".HexToBytes()) + .Concat(new byte[] { 0x1B }) + .ToArray(); + + var pubKey = Crypto.ECRecover(sig, messageHash1); + + Console.WriteLine($"Recovered PubKey: {pubKey.EncodePoint(true).ToHexString()}"); + Console.WriteLine($"Recovered PubKey: {recoveredKey1.EncodePoint(true).ToHexString()}"); + CollectionAssert.AreEqual(expectedPubKey1.EncodePoint(true), pubKey.EncodePoint(true)); + + // Private Key: 0x1234567890123456789012345678901234567890123456789012345678901234 + // Message: "It's a small(er) world" + // Signature: + // r: 0x9328da16089fcba9bececa81663203989f2df5fe1faa6291a45381c81bd17f76 + // s: 0x139c6d6b623b42da56557e5e734a43dc83345ddfadec52cbe24d0cc64f550793 + // v: 28 + + var message2Body = Encoding.UTF8.GetBytes("It's a small(er) world"); + var message2 = new byte[] { 0x19 }.Concat(Encoding.UTF8.GetBytes($"Ethereum Signed Message:\n{message2Body.Length}")).Concat(message2Body).ToArray(); + var messageHash2 = message2.Keccak256(); + Console.WriteLine($"\nMessage Hash 2: {Convert.ToHexString(messageHash2)}"); + + // Second test case from EIP-2098 + var r2 = "9328da16089fcba9bececa81663203989f2df5fe1faa6291a45381c81bd17f76".HexToBytes(); + var s2 = "939c6d6b623b42da56557e5e734a43dc83345ddfadec52cbe24d0cc64f550793".HexToBytes(); + var signature2 = new byte[64]; + + Array.Copy(r2, 0, signature2, 0, 32); + Array.Copy(s2, 0, signature2, 32, 32); + + Console.WriteLine($"r: {Convert.ToHexString(signature2.Take(32).ToArray())}"); + Console.WriteLine($"s: {Convert.ToHexString(signature2.Skip(32).Take(32).ToArray())}"); + Console.WriteLine($"yParity: {(signature2[31] & 0x80) != 0}"); + + var recoveredKey2 = Crypto.ECRecover(signature2, messageHash2); + CollectionAssert.AreEqual(expectedPubKey1.ToArray(), recoveredKey2.EncodePoint(true)); + + // Normal signature without recovery param + var verifySig = "9328da16089fcba9bececa81663203989f2df5fe1faa6291a45381c81bd17f76".HexToBytes() + .Concat("139c6d6b623b42da56557e5e734a43dc83345ddfadec52cbe24d0cc64f550793".HexToBytes()) + .ToArray(); + + Assert.IsTrue(Crypto.VerifySignature(message2, verifySig, recoveredKey2, Neo.Cryptography.HashAlgorithm.Keccak256)); + } } } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs index eb7d2a081a..93e5ace7e8 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs @@ -42,7 +42,7 @@ public void TestSetup() { {"ContractManagement", """{"id":-1,"updatecounter":0,"hash":"0xfffdc93764dbaddd97c48f252a53ea4643faa3fd","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1094259016},"manifest":{"name":"ContractManagement","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"deploy","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"}],"returntype":"Array","offset":0,"safe":false},{"name":"deploy","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Array","offset":7,"safe":false},{"name":"destroy","parameters":[],"returntype":"Void","offset":14,"safe":false},{"name":"getContract","parameters":[{"name":"hash","type":"Hash160"}],"returntype":"Array","offset":21,"safe":true},{"name":"getContractById","parameters":[{"name":"id","type":"Integer"}],"returntype":"Array","offset":28,"safe":true},{"name":"getContractHashes","parameters":[],"returntype":"InteropInterface","offset":35,"safe":true},{"name":"getMinimumDeploymentFee","parameters":[],"returntype":"Integer","offset":42,"safe":true},{"name":"hasMethod","parameters":[{"name":"hash","type":"Hash160"},{"name":"method","type":"String"},{"name":"pcount","type":"Integer"}],"returntype":"Boolean","offset":49,"safe":true},{"name":"setMinimumDeploymentFee","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":56,"safe":false},{"name":"update","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"}],"returntype":"Void","offset":63,"safe":false},{"name":"update","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","offset":70,"safe":false}],"events":[{"name":"Deploy","parameters":[{"name":"Hash","type":"Hash160"}]},{"name":"Update","parameters":[{"name":"Hash","type":"Hash160"}]},{"name":"Destroy","parameters":[{"name":"Hash","type":"Hash160"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}""" }, {"StdLib", """{"id":-2,"updatecounter":0,"hash":"0xacce6fd80d44e1796aa0c2c625e9e4e0ce39efc0","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2681632925},"manifest":{"name":"StdLib","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"atoi","parameters":[{"name":"value","type":"String"}],"returntype":"Integer","offset":0,"safe":true},{"name":"atoi","parameters":[{"name":"value","type":"String"},{"name":"base","type":"Integer"}],"returntype":"Integer","offset":7,"safe":true},{"name":"base58CheckDecode","parameters":[{"name":"s","type":"String"}],"returntype":"ByteArray","offset":14,"safe":true},{"name":"base58CheckEncode","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","offset":21,"safe":true},{"name":"base58Decode","parameters":[{"name":"s","type":"String"}],"returntype":"ByteArray","offset":28,"safe":true},{"name":"base58Encode","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","offset":35,"safe":true},{"name":"base64Decode","parameters":[{"name":"s","type":"String"}],"returntype":"ByteArray","offset":42,"safe":true},{"name":"base64Encode","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","offset":49,"safe":true},{"name":"base64UrlDecode","parameters":[{"name":"s","type":"String"}],"returntype":"String","offset":56,"safe":true},{"name":"base64UrlEncode","parameters":[{"name":"data","type":"String"}],"returntype":"String","offset":63,"safe":true},{"name":"deserialize","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"Any","offset":70,"safe":true},{"name":"itoa","parameters":[{"name":"value","type":"Integer"}],"returntype":"String","offset":77,"safe":true},{"name":"itoa","parameters":[{"name":"value","type":"Integer"},{"name":"base","type":"Integer"}],"returntype":"String","offset":84,"safe":true},{"name":"jsonDeserialize","parameters":[{"name":"json","type":"ByteArray"}],"returntype":"Any","offset":91,"safe":true},{"name":"jsonSerialize","parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","offset":98,"safe":true},{"name":"memoryCompare","parameters":[{"name":"str1","type":"ByteArray"},{"name":"str2","type":"ByteArray"}],"returntype":"Integer","offset":105,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"}],"returntype":"Integer","offset":112,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"}],"returntype":"Integer","offset":119,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"},{"name":"backward","type":"Boolean"}],"returntype":"Integer","offset":126,"safe":true},{"name":"serialize","parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","offset":133,"safe":true},{"name":"strLen","parameters":[{"name":"str","type":"String"}],"returntype":"Integer","offset":140,"safe":true},{"name":"stringSplit","parameters":[{"name":"str","type":"String"},{"name":"separator","type":"String"}],"returntype":"Array","offset":147,"safe":true},{"name":"stringSplit","parameters":[{"name":"str","type":"String"},{"name":"separator","type":"String"},{"name":"removeEmptyEntries","type":"Boolean"}],"returntype":"Array","offset":154,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, - {"CryptoLib", """{"id":-3,"updatecounter":0,"hash":"0x726cb6e0cd8628a1350a611384688911ab75f51b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":3581846399},"manifest":{"name":"CryptoLib","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"bls12381Add","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"InteropInterface","offset":0,"safe":true},{"name":"bls12381Deserialize","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"InteropInterface","offset":7,"safe":true},{"name":"bls12381Equal","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"Boolean","offset":14,"safe":true},{"name":"bls12381Mul","parameters":[{"name":"x","type":"InteropInterface"},{"name":"mul","type":"ByteArray"},{"name":"neg","type":"Boolean"}],"returntype":"InteropInterface","offset":21,"safe":true},{"name":"bls12381Pairing","parameters":[{"name":"g1","type":"InteropInterface"},{"name":"g2","type":"InteropInterface"}],"returntype":"InteropInterface","offset":28,"safe":true},{"name":"bls12381Serialize","parameters":[{"name":"g","type":"InteropInterface"}],"returntype":"ByteArray","offset":35,"safe":true},{"name":"keccak256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":42,"safe":true},{"name":"murmur32","parameters":[{"name":"data","type":"ByteArray"},{"name":"seed","type":"Integer"}],"returntype":"ByteArray","offset":49,"safe":true},{"name":"ripemd160","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":56,"safe":true},{"name":"sha256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":63,"safe":true},{"name":"verifyWithECDsa","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"},{"name":"curveHash","type":"Integer"}],"returntype":"Boolean","offset":70,"safe":true},{"name":"verifyWithEd25519","parameters":[{"name":"message","type":"ByteArray"},{"name":"publicKey","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":77,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, + {"CryptoLib", """{"id":-3,"updatecounter":0,"hash":"0x726cb6e0cd8628a1350a611384688911ab75f51b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":174904780},"manifest":{"name":"CryptoLib","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"bls12381Add","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"InteropInterface","offset":0,"safe":true},{"name":"bls12381Deserialize","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"InteropInterface","offset":7,"safe":true},{"name":"bls12381Equal","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"Boolean","offset":14,"safe":true},{"name":"bls12381Mul","parameters":[{"name":"x","type":"InteropInterface"},{"name":"mul","type":"ByteArray"},{"name":"neg","type":"Boolean"}],"returntype":"InteropInterface","offset":21,"safe":true},{"name":"bls12381Pairing","parameters":[{"name":"g1","type":"InteropInterface"},{"name":"g2","type":"InteropInterface"}],"returntype":"InteropInterface","offset":28,"safe":true},{"name":"bls12381Serialize","parameters":[{"name":"g","type":"InteropInterface"}],"returntype":"ByteArray","offset":35,"safe":true},{"name":"keccak256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":42,"safe":true},{"name":"murmur32","parameters":[{"name":"data","type":"ByteArray"},{"name":"seed","type":"Integer"}],"returntype":"ByteArray","offset":49,"safe":true},{"name":"recoverSecp256K1","parameters":[{"name":"messageHash","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"ByteArray","offset":56,"safe":true},{"name":"ripemd160","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":63,"safe":true},{"name":"sha256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":70,"safe":true},{"name":"verifyWithECDsa","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"},{"name":"curveHash","type":"Integer"}],"returntype":"Boolean","offset":77,"safe":true},{"name":"verifyWithEd25519","parameters":[{"name":"message","type":"ByteArray"},{"name":"publicKey","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":84,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, {"LedgerContract", """{"id":-4,"updatecounter":0,"hash":"0xda65b600f7124ce6c79950c1772a36403104f2be","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1110259869},"manifest":{"name":"LedgerContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"currentHash","parameters":[],"returntype":"Hash256","offset":0,"safe":true},{"name":"currentIndex","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"getBlock","parameters":[{"name":"indexOrHash","type":"ByteArray"}],"returntype":"Array","offset":14,"safe":true},{"name":"getTransaction","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Array","offset":21,"safe":true},{"name":"getTransactionFromBlock","parameters":[{"name":"blockIndexOrHash","type":"ByteArray"},{"name":"txIndex","type":"Integer"}],"returntype":"Array","offset":28,"safe":true},{"name":"getTransactionHeight","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Integer","offset":35,"safe":true},{"name":"getTransactionSigners","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Array","offset":42,"safe":true},{"name":"getTransactionVMState","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Integer","offset":49,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, {"NeoToken", """{"id":-5,"updatecounter":0,"hash":"0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":1991619121},"manifest":{"name":"NeoToken","groups":[],"features":{},"supportedstandards":["NEP-17","NEP-27"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"decimals","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"getAccountState","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Array","offset":14,"safe":true},{"name":"getAllCandidates","parameters":[],"returntype":"InteropInterface","offset":21,"safe":true},{"name":"getCandidateVote","parameters":[{"name":"pubKey","type":"PublicKey"}],"returntype":"Integer","offset":28,"safe":true},{"name":"getCandidates","parameters":[],"returntype":"Array","offset":35,"safe":true},{"name":"getCommittee","parameters":[],"returntype":"Array","offset":42,"safe":true},{"name":"getCommitteeAddress","parameters":[],"returntype":"Hash160","offset":49,"safe":true},{"name":"getGasPerBlock","parameters":[],"returntype":"Integer","offset":56,"safe":true},{"name":"getNextBlockValidators","parameters":[],"returntype":"Array","offset":63,"safe":true},{"name":"getRegisterPrice","parameters":[],"returntype":"Integer","offset":70,"safe":true},{"name":"onNEP17Payment","parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","offset":77,"safe":false},{"name":"registerCandidate","parameters":[{"name":"pubkey","type":"PublicKey"}],"returntype":"Boolean","offset":84,"safe":false},{"name":"setGasPerBlock","parameters":[{"name":"gasPerBlock","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"setRegisterPrice","parameters":[{"name":"registerPrice","type":"Integer"}],"returntype":"Void","offset":98,"safe":false},{"name":"symbol","parameters":[],"returntype":"String","offset":105,"safe":true},{"name":"totalSupply","parameters":[],"returntype":"Integer","offset":112,"safe":true},{"name":"transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Boolean","offset":119,"safe":false},{"name":"unclaimedGas","parameters":[{"name":"account","type":"Hash160"},{"name":"end","type":"Integer"}],"returntype":"Integer","offset":126,"safe":true},{"name":"unregisterCandidate","parameters":[{"name":"pubkey","type":"PublicKey"}],"returntype":"Boolean","offset":133,"safe":false},{"name":"vote","parameters":[{"name":"account","type":"Hash160"},{"name":"voteTo","type":"PublicKey"}],"returntype":"Boolean","offset":140,"safe":false}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]},{"name":"CandidateStateChanged","parameters":[{"name":"pubkey","type":"PublicKey"},{"name":"registered","type":"Boolean"},{"name":"votes","type":"Integer"}]},{"name":"Vote","parameters":[{"name":"account","type":"Hash160"},{"name":"from","type":"PublicKey"},{"name":"to","type":"PublicKey"},{"name":"amount","type":"Integer"}]},{"name":"CommitteeChanged","parameters":[{"name":"old","type":"Array"},{"name":"new","type":"Array"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, {"GasToken", """{"id":-6,"updatecounter":0,"hash":"0xd2a4cff31913016155e38e474a2c06d08be276cf","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2663858513},"manifest":{"name":"GasToken","groups":[],"features":{},"supportedstandards":["NEP-17"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"decimals","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"symbol","parameters":[],"returntype":"String","offset":14,"safe":true},{"name":"totalSupply","parameters":[],"returntype":"Integer","offset":21,"safe":true},{"name":"transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Boolean","offset":28,"safe":false}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, From 470e0d93da02908d55d0f3723cd99884dda82cba Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 17 Feb 2025 13:17:26 +0300 Subject: [PATCH 18/22] Improve MaxNestingDepth of WitnessCondition (#3761) * tests: add UT for nested WitnessCondition This just works as the code works. Signed-off-by: Roman Khimov * WitnessCondition: unify nesting checks, fix MaxNestingDepth 1. We have some duplication around "maxNestDepth <= 0" checks. 2. If we're to fix them by moving to DeserializeFrom() and thereby affecting all types of conditions we can discover a simple truth about conditions that was hidden previously: our real supported MaxNestingDepth is 3, not 2. 3. Test_WitnessCondition_Nesting works as before, check it there, we have And->And->Boolean and other nesting cases, 3-4 elements in the chain, not 2. Signed-off-by: Roman Khimov * WitnessCondition: unify JSON parsing as well Signed-off-by: Roman Khimov * tests: s/WitnessContition/WitnessCondition/ Fix typo. Signed-off-by: Roman Khimov * Format header * Format arrays in UT_WitnessCondition.cs --------- Signed-off-by: Roman Khimov Co-authored-by: Shargon Co-authored-by: Christopher Schuchardt Co-authored-by: NGD Admin <154295625+NGDAdmin@users.noreply.github.com> --- .../P2P/Payloads/Conditions/AndCondition.cs | 4 +- .../P2P/Payloads/Conditions/NotCondition.cs | 2 - .../P2P/Payloads/Conditions/OrCondition.cs | 4 +- .../Payloads/Conditions/WitnessCondition.cs | 6 +- ...essContition.cs => UT_WitnessCondition.cs} | 130 +++++++++++++++++- 5 files changed, 134 insertions(+), 12 deletions(-) rename tests/Neo.UnitTests/Network/P2P/Payloads/{UT_WitnessContition.cs => UT_WitnessCondition.cs} (75%) diff --git a/src/Neo/Network/P2P/Payloads/Conditions/AndCondition.cs b/src/Neo/Network/P2P/Payloads/Conditions/AndCondition.cs index 974bb5a16c..40b2e12a65 100644 --- a/src/Neo/Network/P2P/Payloads/Conditions/AndCondition.cs +++ b/src/Neo/Network/P2P/Payloads/Conditions/AndCondition.cs @@ -61,8 +61,7 @@ public override int GetHashCode() protected override void DeserializeWithoutType(ref MemoryReader reader, int maxNestDepth) { - if (maxNestDepth <= 0) throw new FormatException(); - Expressions = DeserializeConditions(ref reader, maxNestDepth - 1); + Expressions = DeserializeConditions(ref reader, maxNestDepth); if (Expressions.Length == 0) throw new FormatException(); } @@ -78,7 +77,6 @@ protected override void SerializeWithoutType(BinaryWriter writer) private protected override void ParseJson(JObject json, int maxNestDepth) { - if (maxNestDepth <= 0) throw new FormatException(); JArray expressions = (JArray)json["expressions"]; if (expressions.Count > MaxSubitems) throw new FormatException(); Expressions = expressions.Select(p => FromJson((JObject)p, maxNestDepth - 1)).ToArray(); diff --git a/src/Neo/Network/P2P/Payloads/Conditions/NotCondition.cs b/src/Neo/Network/P2P/Payloads/Conditions/NotCondition.cs index b1a294ad91..6dbf60cdab 100644 --- a/src/Neo/Network/P2P/Payloads/Conditions/NotCondition.cs +++ b/src/Neo/Network/P2P/Payloads/Conditions/NotCondition.cs @@ -60,7 +60,6 @@ public override int GetHashCode() protected override void DeserializeWithoutType(ref MemoryReader reader, int maxNestDepth) { - if (maxNestDepth <= 0) throw new FormatException(); Expression = DeserializeFrom(ref reader, maxNestDepth - 1); } @@ -76,7 +75,6 @@ protected override void SerializeWithoutType(BinaryWriter writer) private protected override void ParseJson(JObject json, int maxNestDepth) { - if (maxNestDepth <= 0) throw new FormatException(); Expression = FromJson((JObject)json["expression"], maxNestDepth - 1); } diff --git a/src/Neo/Network/P2P/Payloads/Conditions/OrCondition.cs b/src/Neo/Network/P2P/Payloads/Conditions/OrCondition.cs index d2501191cf..5fbc0b4a88 100644 --- a/src/Neo/Network/P2P/Payloads/Conditions/OrCondition.cs +++ b/src/Neo/Network/P2P/Payloads/Conditions/OrCondition.cs @@ -61,8 +61,7 @@ public override int GetHashCode() protected override void DeserializeWithoutType(ref MemoryReader reader, int maxNestDepth) { - if (maxNestDepth <= 0) throw new FormatException(); - Expressions = DeserializeConditions(ref reader, maxNestDepth - 1); + Expressions = DeserializeConditions(ref reader, maxNestDepth); if (Expressions.Length == 0) throw new FormatException(); } @@ -78,7 +77,6 @@ protected override void SerializeWithoutType(BinaryWriter writer) private protected override void ParseJson(JObject json, int maxNestDepth) { - if (maxNestDepth <= 0) throw new FormatException(); JArray expressions = (JArray)json["expressions"]; if (expressions.Count > MaxSubitems) throw new FormatException(); Expressions = expressions.Select(p => FromJson((JObject)p, maxNestDepth - 1)).ToArray(); diff --git a/src/Neo/Network/P2P/Payloads/Conditions/WitnessCondition.cs b/src/Neo/Network/P2P/Payloads/Conditions/WitnessCondition.cs index 16153a79a7..b45a5a08dd 100644 --- a/src/Neo/Network/P2P/Payloads/Conditions/WitnessCondition.cs +++ b/src/Neo/Network/P2P/Payloads/Conditions/WitnessCondition.cs @@ -25,7 +25,7 @@ namespace Neo.Network.P2P.Payloads.Conditions public abstract class WitnessCondition : IInteroperable, ISerializable { internal const int MaxSubitems = 16; - internal const int MaxNestingDepth = 2; + internal const int MaxNestingDepth = 3; /// /// The type of the . @@ -54,7 +54,7 @@ protected static WitnessCondition[] DeserializeConditions(ref MemoryReader reade { WitnessCondition[] conditions = new WitnessCondition[reader.ReadVarInt(MaxSubitems)]; for (int i = 0; i < conditions.Length; i++) - conditions[i] = DeserializeFrom(ref reader, maxNestDepth); + conditions[i] = DeserializeFrom(ref reader, maxNestDepth - 1); return conditions; } @@ -66,6 +66,7 @@ protected static WitnessCondition[] DeserializeConditions(ref MemoryReader reade /// The deserialized . public static WitnessCondition DeserializeFrom(ref MemoryReader reader, int maxNestDepth) { + if (maxNestDepth <= 0) throw new FormatException(); WitnessConditionType type = (WitnessConditionType)reader.ReadByte(); if (ReflectionCache.CreateInstance(type) is not WitnessCondition condition) throw new FormatException(); @@ -109,6 +110,7 @@ void ISerializable.Serialize(BinaryWriter writer) /// The converted . public static WitnessCondition FromJson(JObject json, int maxNestDepth) { + if (maxNestDepth <= 0) throw new FormatException(); WitnessConditionType type = Enum.Parse(json["type"].GetString()); if (ReflectionCache.CreateInstance(type) is not WitnessCondition condition) throw new FormatException("Invalid WitnessConditionType."); diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_WitnessContition.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_WitnessCondition.cs similarity index 75% rename from tests/Neo.UnitTests/Network/P2P/Payloads/UT_WitnessContition.cs rename to tests/Neo.UnitTests/Network/P2P/Payloads/UT_WitnessCondition.cs index 0f6c1c5081..9f0e0671b2 100644 --- a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_WitnessContition.cs +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_WitnessCondition.cs @@ -1,6 +1,6 @@ // Copyright (C) 2015-2025 The Neo Project. // -// UT_WitnessContition.cs file belongs to the neo project and is free +// UT_WitnessCondition.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 @@ -11,8 +11,11 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Neo.Cryptography.ECC; +using Neo.Extensions; +using Neo.IO; using Neo.Json; using Neo.Network.P2P.Payloads.Conditions; +using System; namespace Neo.UnitTests.Network.P2P.Payloads { @@ -347,7 +350,7 @@ public void TestFromJson2() var hash2 = UInt160.Parse("0xd2a4cff31913016155e38e474a2c06d08be276cf"); var jstr = "{\"type\":\"Or\",\"expressions\":[{\"type\":\"And\",\"expressions\":[{\"type\":\"CalledByContract\",\"hash\":\"0x0000000000000000000000000000000000000000\"},{\"type\":\"ScriptHash\",\"hash\":\"0xd2a4cff31913016155e38e474a2c06d08be276cf\"}]},{\"type\":\"Or\",\"expressions\":[{\"type\":\"CalledByGroup\",\"group\":\"03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c\"},{\"type\":\"Boolean\",\"expression\":true}]}]}"; var json = (JObject)JToken.Parse(jstr); - var condi = WitnessCondition.FromJson(json, 2); + var condi = WitnessCondition.FromJson(json, WitnessCondition.MaxNestingDepth); var or_condi = (OrCondition)condi; Assert.AreEqual(2, or_condi.Expressions.Length); var and_condi = (AndCondition)or_condi.Expressions[0]; @@ -363,5 +366,128 @@ public void TestFromJson2() Assert.IsTrue(cbgc.Group.Equals(point)); Assert.IsTrue(bc.Expression); } + + [TestMethod] + public void Test_WitnessCondition_Nesting() + { + WitnessCondition nested = new OrCondition + { + Expressions = [ + new OrCondition { Expressions = [new BooleanCondition { Expression = true }] } + ] + }; + + var buf = nested.ToArray(); + var reader = new MemoryReader(buf); + + var deser = WitnessCondition.DeserializeFrom(ref reader, WitnessCondition.MaxNestingDepth); + Assert.AreEqual(nested, deser); + + nested = new AndCondition + { + Expressions = [ + new AndCondition { Expressions = [new BooleanCondition { Expression = true }] } + ] + }; + + buf = nested.ToArray(); + reader = new MemoryReader(buf); + + deser = WitnessCondition.DeserializeFrom(ref reader, WitnessCondition.MaxNestingDepth); + Assert.AreEqual(nested, deser); + + nested = new NotCondition + { + Expression = new NotCondition + { + Expression = new BooleanCondition { Expression = true } + } + }; + + buf = nested.ToArray(); + reader = new MemoryReader(buf); + + deser = WitnessCondition.DeserializeFrom(ref reader, WitnessCondition.MaxNestingDepth); + Assert.AreEqual(nested, deser); + + // Overflow maxNestingDepth + nested = new OrCondition + { + Expressions = [ + new OrCondition { + Expressions = [ + new OrCondition { Expressions = [new BooleanCondition { Expression = true }] } + ] + } + ] + }; + + buf = nested.ToArray(); + reader = new MemoryReader(buf); + + var exceptionHappened = false; + // CS8175 prevents from using Assert.ThrowsException here + try + { + WitnessCondition.DeserializeFrom(ref reader, WitnessCondition.MaxNestingDepth); + } + catch (FormatException) + { + exceptionHappened = true; + } + Assert.IsTrue(exceptionHappened); + + nested = new AndCondition + { + Expressions = [ + new AndCondition { + Expressions = [ + new AndCondition { Expressions = [new BooleanCondition { Expression = true }] } + ] + } + ] + }; + + buf = nested.ToArray(); + reader = new MemoryReader(buf); + + exceptionHappened = false; + // CS8175 prevents from using Assert.ThrowsException here + try + { + WitnessCondition.DeserializeFrom(ref reader, WitnessCondition.MaxNestingDepth); + } + catch (FormatException) + { + exceptionHappened = true; + } + Assert.IsTrue(exceptionHappened); + + nested = new NotCondition + { + Expression = new NotCondition + { + Expression = new NotCondition + { + Expression = new BooleanCondition { Expression = true } + } + } + }; + + buf = nested.ToArray(); + reader = new MemoryReader(buf); + + exceptionHappened = false; + // CS8175 prevents from using Assert.ThrowsException here + try + { + WitnessCondition.DeserializeFrom(ref reader, WitnessCondition.MaxNestingDepth); + } + catch (FormatException) + { + exceptionHappened = true; + } + Assert.IsTrue(exceptionHappened); + } } } From f73364d28a656dad054ebc454c3bedaf84ca90f0 Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt Date: Mon, 17 Feb 2025 10:21:49 -0500 Subject: [PATCH 19/22] Add Support for Plugin Loading by Assembly (#3755) * Add Support for plugin loading * Added @shargon request of changes --------- Co-authored-by: Shargon Co-authored-by: NGD Admin <154295625+NGDAdmin@users.noreply.github.com> --- src/Plugins/Directory.Build.props | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Plugins/Directory.Build.props b/src/Plugins/Directory.Build.props index f5eaeb6226..b9a328c854 100644 --- a/src/Plugins/Directory.Build.props +++ b/src/Plugins/Directory.Build.props @@ -4,9 +4,10 @@ + true true - + From 7d73ebe0c43b58dd0b86551168bde854cd8741ae Mon Sep 17 00:00:00 2001 From: Shargon Date: Tue, 18 Feb 2025 01:33:13 -0800 Subject: [PATCH 20/22] Optimize `StorageKey` creation (#3756) * Optimize key builder * Optimize with GetSpan * Use Span * Clean * Rename * Update src/Neo/SmartContract/KeyBuilder.cs Co-authored-by: nan01ab * Update KeyBuilder.cs * @cschuchardt88 feedback * Avoid ToArray in StorageKey constructor * Use sizeof * Optimize ECPoint * Optimize ECPoint * Add ISerializableSpan * Move to StorageKey * Use ReadOnlySpan --------- Co-authored-by: nan01ab Co-authored-by: Christopher Schuchardt --- .../Native/ContractManagement.cs | 20 +- src/Neo/SmartContract/Native/FungibleToken.cs | 10 +- .../SmartContract/Native/LedgerContract.cs | 22 +- .../SmartContract/Native/NativeContract.cs | 41 +++- src/Neo/SmartContract/Native/NeoToken.cs | 40 ++-- .../SmartContract/Native/OracleContract.cs | 16 +- .../SmartContract/Native/PolicyContract.cs | 10 +- .../SmartContract/Native/RoleManagement.cs | 8 +- src/Neo/SmartContract/StorageKey.cs | 219 ++++++++++++++++++ tests/Neo.UnitTests/Ledger/UT_StorageKey.cs | 63 +++++ 10 files changed, 382 insertions(+), 67 deletions(-) diff --git a/src/Neo/SmartContract/Native/ContractManagement.cs b/src/Neo/SmartContract/Native/ContractManagement.cs index 4f64b6f990..7cdfdad91a 100644 --- a/src/Neo/SmartContract/Native/ContractManagement.cs +++ b/src/Neo/SmartContract/Native/ContractManagement.cs @@ -75,13 +75,13 @@ internal override async ContractTask OnPersistAsync(ApplicationEngine engine) if (contract.IsInitializeBlock(engine.ProtocolSettings, engine.PersistingBlock.Index, out var hfs)) { ContractState contractState = contract.GetContractState(engine.ProtocolSettings, engine.PersistingBlock.Index); - StorageItem state = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Contract).Add(contract.Hash)); + StorageItem state = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Contract, contract.Hash)); if (state is null) { // Create the contract state - engine.SnapshotCache.Add(CreateStorageKey(Prefix_Contract).Add(contract.Hash), new StorageItem(contractState)); - engine.SnapshotCache.Add(CreateStorageKey(Prefix_ContractHash).AddBigEndian(contract.Id), new StorageItem(contract.Hash.ToArray())); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_Contract, contract.Hash), new StorageItem(contractState)); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_ContractHash, contract.Id), new StorageItem(contract.Hash.ToArray())); // Initialize the native smart contract if it's active starting from the genesis. // If it's not the case, then hardfork-based initialization will be performed down below. @@ -141,7 +141,7 @@ private void SetMinimumDeploymentFee(ApplicationEngine engine, BigInteger value/ [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] public ContractState GetContract(IReadOnlyStore snapshot, UInt160 hash) { - var key = CreateStorageKey(Prefix_Contract).Add(hash); + var key = CreateStorageKey(Prefix_Contract, hash); return snapshot.TryGet(key, out var item) ? item.GetInteroperable(false) : null; } @@ -154,7 +154,7 @@ public ContractState GetContract(IReadOnlyStore snapshot, UInt160 hash) [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] public ContractState GetContractById(IReadOnlyStore snapshot, int id) { - var key = CreateStorageKey(Prefix_ContractHash).AddBigEndian(id); + var key = CreateStorageKey(Prefix_ContractHash, id); return snapshot.TryGet(key, out var item) ? GetContract(snapshot, new UInt160(item.Value.Span)) : null; } @@ -233,7 +233,7 @@ private async ContractTask Deploy(ApplicationEngine engine, byte[ if (Policy.IsBlocked(engine.SnapshotCache, hash)) throw new InvalidOperationException($"The contract {hash} has been blocked."); - StorageKey key = CreateStorageKey(Prefix_Contract).Add(hash); + StorageKey key = CreateStorageKey(Prefix_Contract, hash); if (engine.SnapshotCache.Contains(key)) throw new InvalidOperationException($"Contract Already Exists: {hash}"); ContractState contract = new() @@ -248,7 +248,7 @@ private async ContractTask Deploy(ApplicationEngine engine, byte[ if (!contract.Manifest.IsValid(engine.Limits, hash)) throw new InvalidOperationException($"Invalid Manifest: {hash}"); engine.SnapshotCache.Add(key, new StorageItem(contract)); - engine.SnapshotCache.Add(CreateStorageKey(Prefix_ContractHash).AddBigEndian(contract.Id), new StorageItem(hash.ToArray())); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_ContractHash, contract.Id), new StorageItem(hash.ToArray())); await OnDeployAsync(engine, contract, data, false); @@ -268,7 +268,7 @@ private ContractTask Update(ApplicationEngine engine, byte[] nefFile, byte[] man engine.AddFee(engine.StoragePrice * ((nefFile?.Length ?? 0) + (manifest?.Length ?? 0))); - var contract = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Contract).Add(engine.CallingScriptHash))?.GetInteroperable(false); + var contract = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Contract, engine.CallingScriptHash))?.GetInteroperable(false); if (contract is null) throw new InvalidOperationException($"Updating Contract Does Not Exist: {engine.CallingScriptHash}"); if (contract.UpdateCounter == ushort.MaxValue) throw new InvalidOperationException($"The contract reached the maximum number of updates."); @@ -300,11 +300,11 @@ private ContractTask Update(ApplicationEngine engine, byte[] nefFile, byte[] man private void Destroy(ApplicationEngine engine) { UInt160 hash = engine.CallingScriptHash; - StorageKey ckey = CreateStorageKey(Prefix_Contract).Add(hash); + StorageKey ckey = CreateStorageKey(Prefix_Contract, hash); ContractState contract = engine.SnapshotCache.TryGet(ckey)?.GetInteroperable(false); if (contract is null) return; engine.SnapshotCache.Delete(ckey); - engine.SnapshotCache.Delete(CreateStorageKey(Prefix_ContractHash).AddBigEndian(contract.Id)); + engine.SnapshotCache.Delete(CreateStorageKey(Prefix_ContractHash, contract.Id)); foreach (var (key, _) in engine.SnapshotCache.Find(StorageKey.CreateSearchPrefix(contract.Id, ReadOnlySpan.Empty))) engine.SnapshotCache.Delete(key); // lock contract diff --git a/src/Neo/SmartContract/Native/FungibleToken.cs b/src/Neo/SmartContract/Native/FungibleToken.cs index b4925e9d13..3edaef6475 100644 --- a/src/Neo/SmartContract/Native/FungibleToken.cs +++ b/src/Neo/SmartContract/Native/FungibleToken.cs @@ -74,7 +74,7 @@ internal async ContractTask Mint(ApplicationEngine engine, UInt160 account, BigI { if (amount.Sign < 0) throw new ArgumentOutOfRangeException(nameof(amount)); if (amount.IsZero) return; - StorageItem storage = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Account).Add(account), () => new StorageItem(new TState())); + StorageItem storage = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Account, account), () => new StorageItem(new TState())); TState state = storage.GetInteroperable(); OnBalanceChanging(engine, account, state, amount); state.Balance += amount; @@ -87,7 +87,7 @@ internal async ContractTask Burn(ApplicationEngine engine, UInt160 account, BigI { if (amount.Sign < 0) throw new ArgumentOutOfRangeException(nameof(amount)); if (amount.IsZero) return; - StorageKey key = CreateStorageKey(Prefix_Account).Add(account); + StorageKey key = CreateStorageKey(Prefix_Account, account); StorageItem storage = engine.SnapshotCache.GetAndChange(key); TState state = storage.GetInteroperable(); if (state.Balance < amount) throw new InvalidOperationException(); @@ -122,7 +122,7 @@ public virtual BigInteger TotalSupply(IReadOnlyStore snapshot) [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] public virtual BigInteger BalanceOf(IReadOnlyStore snapshot, UInt160 account) { - var key = CreateStorageKey(Prefix_Account).Add(account); + var key = CreateStorageKey(Prefix_Account, account); if (snapshot.TryGet(key, out var item)) return item.GetInteroperable().Balance; return BigInteger.Zero; @@ -136,7 +136,7 @@ private protected async ContractTask Transfer(ApplicationEngine engine, UI if (amount.Sign < 0) throw new ArgumentOutOfRangeException(nameof(amount)); if (!from.Equals(engine.CallingScriptHash) && !engine.CheckWitnessInternal(from)) return false; - StorageKey key_from = CreateStorageKey(Prefix_Account).Add(from); + StorageKey key_from = CreateStorageKey(Prefix_Account, from); StorageItem storage_from = engine.SnapshotCache.GetAndChange(key_from); if (amount.IsZero) { @@ -162,7 +162,7 @@ private protected async ContractTask Transfer(ApplicationEngine engine, UI engine.SnapshotCache.Delete(key_from); else state_from.Balance -= amount; - StorageKey key_to = CreateStorageKey(Prefix_Account).Add(to); + StorageKey key_to = CreateStorageKey(Prefix_Account, to); StorageItem storage_to = engine.SnapshotCache.GetAndChange(key_to, () => new StorageItem(new TState())); TState state_to = storage_to.GetInteroperable(); OnBalanceChanging(engine, to, state_to, amount); diff --git a/src/Neo/SmartContract/Native/LedgerContract.cs b/src/Neo/SmartContract/Native/LedgerContract.cs index 198c0d83cb..94e153a500 100644 --- a/src/Neo/SmartContract/Native/LedgerContract.cs +++ b/src/Neo/SmartContract/Native/LedgerContract.cs @@ -47,22 +47,22 @@ internal override ContractTask OnPersistAsync(ApplicationEngine engine) Transaction = p, State = VMState.NONE }).ToArray(); - engine.SnapshotCache.Add(CreateStorageKey(Prefix_BlockHash).AddBigEndian(engine.PersistingBlock.Index), new StorageItem(engine.PersistingBlock.Hash.ToArray())); - engine.SnapshotCache.Add(CreateStorageKey(Prefix_Block).Add(engine.PersistingBlock.Hash), new StorageItem(Trim(engine.PersistingBlock).ToArray())); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_BlockHash, engine.PersistingBlock.Index), new StorageItem(engine.PersistingBlock.Hash.ToArray())); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_Block, engine.PersistingBlock.Hash), new StorageItem(Trim(engine.PersistingBlock).ToArray())); foreach (TransactionState tx in transactions) { // It's possible that there are previously saved malicious conflict records for this transaction. // If so, then remove it and store the relevant transaction itself. - engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Transaction).Add(tx.Transaction.Hash), () => new StorageItem(new TransactionState())).FromReplica(new StorageItem(tx)); + engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Transaction, tx.Transaction.Hash), () => new StorageItem(new TransactionState())).FromReplica(new StorageItem(tx)); // Store transaction's conflicits. var conflictingSigners = tx.Transaction.Signers.Select(s => s.Account); foreach (var attr in tx.Transaction.GetAttributes()) { - engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Transaction).Add(attr.Hash), () => new StorageItem(new TransactionState())).FromReplica(new StorageItem(new TransactionState() { BlockIndex = engine.PersistingBlock.Index })); + engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Transaction, attr.Hash), () => new StorageItem(new TransactionState())).FromReplica(new StorageItem(new TransactionState() { BlockIndex = engine.PersistingBlock.Index })); foreach (var signer in conflictingSigners) { - engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Transaction).Add(attr.Hash).Add(signer), () => new StorageItem(new TransactionState())).FromReplica(new StorageItem(new TransactionState() { BlockIndex = engine.PersistingBlock.Index })); + engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Transaction, attr.Hash, signer), () => new StorageItem(new TransactionState())).FromReplica(new StorageItem(new TransactionState() { BlockIndex = engine.PersistingBlock.Index })); } } } @@ -105,7 +105,7 @@ public UInt256 GetBlockHash(IReadOnlyStore snapshot, uint index) if (snapshot is null) throw new ArgumentNullException(nameof(snapshot)); - var key = CreateStorageKey(Prefix_BlockHash).AddBigEndian(index); + var key = CreateStorageKey(Prefix_BlockHash, index); return snapshot.TryGet(key, out var item) ? new UInt256(item.Value.Span) : null; } @@ -150,7 +150,7 @@ public bool ContainsBlock(IReadOnlyStore snapshot, UInt256 hash) if (snapshot is null) throw new ArgumentNullException(nameof(snapshot)); - return snapshot.Contains(CreateStorageKey(Prefix_Block).Add(hash)); + return snapshot.Contains(CreateStorageKey(Prefix_Block, hash)); } /// @@ -188,7 +188,7 @@ public bool ContainsConflictHash(IReadOnlyStore snapshot, UInt256 hash, IEnumera throw new ArgumentNullException(nameof(signers)); // Check the dummy stub firstly to define whether there's exist at least one conflict record. - var key = CreateStorageKey(Prefix_Transaction).Add(hash); + var key = CreateStorageKey(Prefix_Transaction, hash); var stub = snapshot.TryGet(key, out var item) ? item.GetInteroperable() : null; if (stub is null || stub.Transaction is not null || !IsTraceableBlock(snapshot, stub.BlockIndex, maxTraceableBlocks)) return false; @@ -196,7 +196,7 @@ public bool ContainsConflictHash(IReadOnlyStore snapshot, UInt256 hash, IEnumera // At least one conflict record is found, then need to check signers intersection. foreach (var signer in signers) { - key = CreateStorageKey(Prefix_Transaction).Add(hash).Add(signer); + key = CreateStorageKey(Prefix_Transaction, hash, signer); var state = snapshot.TryGet(key, out var tx) ? tx.GetInteroperable() : null; if (state is not null && IsTraceableBlock(snapshot, state.BlockIndex, maxTraceableBlocks)) return true; @@ -216,7 +216,7 @@ public TrimmedBlock GetTrimmedBlock(IReadOnlyStore snapshot, UInt256 hash) if (snapshot is null) throw new ArgumentNullException(nameof(snapshot)); - var key = CreateStorageKey(Prefix_Block).Add(hash); + var key = CreateStorageKey(Prefix_Block, hash); if (snapshot.TryGet(key, out var item)) return item.Value.AsSerializable(); return null; @@ -303,7 +303,7 @@ public TransactionState GetTransactionState(IReadOnlyStore snapshot, UInt256 has if (snapshot is null) throw new ArgumentNullException(nameof(snapshot)); - var key = CreateStorageKey(Prefix_Transaction).Add(hash); + var key = CreateStorageKey(Prefix_Transaction, hash); var state = snapshot.TryGet(key, out var item) ? item.GetInteroperable() : null; return state?.Transaction is null ? null : state; } diff --git a/src/Neo/SmartContract/Native/NativeContract.cs b/src/Neo/SmartContract/Native/NativeContract.cs index 16fcbebe2d..baeb6b4622 100644 --- a/src/Neo/SmartContract/Native/NativeContract.cs +++ b/src/Neo/SmartContract/Native/NativeContract.cs @@ -9,6 +9,7 @@ // Redistribution and use in source and binary forms with or without // modifications are permitted. +using Neo.Cryptography.ECC; using Neo.SmartContract.Manifest; using Neo.VM; using System; @@ -347,10 +348,42 @@ protected static bool CheckCommittee(ApplicationEngine engine) return engine.CheckWitnessInternal(committeeMultiSigAddr); } - private protected KeyBuilder CreateStorageKey(byte prefix) - { - return new KeyBuilder(Id, prefix); - } + #region Storage keys + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix) => StorageKey.Create(Id, prefix); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, byte data) => StorageKey.Create(Id, prefix, data); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, int bigEndianKey) => StorageKey.Create(Id, prefix, bigEndianKey); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, uint bigEndianKey) => StorageKey.Create(Id, prefix, bigEndianKey); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, long bigEndianKey) => StorageKey.Create(Id, prefix, bigEndianKey); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, ulong bigEndianKey) => StorageKey.Create(Id, prefix, bigEndianKey); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, ReadOnlySpan content) => StorageKey.Create(Id, prefix, content); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, UInt160 hash) => StorageKey.Create(Id, prefix, hash); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, UInt256 hash) => StorageKey.Create(Id, prefix, hash); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, ECPoint pubKey) => StorageKey.Create(Id, prefix, pubKey); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, UInt256 hash, UInt160 signer) => StorageKey.Create(Id, prefix, hash, signer); + + #endregion /// /// Gets the native contract with the specified hash. diff --git a/src/Neo/SmartContract/Native/NeoToken.cs b/src/Neo/SmartContract/Native/NeoToken.cs index fdf4454c7e..4d7198358f 100644 --- a/src/Neo/SmartContract/Native/NeoToken.cs +++ b/src/Neo/SmartContract/Native/NeoToken.cs @@ -94,7 +94,7 @@ internal override void OnBalanceChanging(ApplicationEngine engine, UInt160 accou if (amount.IsZero) return; if (state.VoteTo is null) return; engine.SnapshotCache.GetAndChange(_votersCount).Add(amount); - StorageKey key = CreateStorageKey(Prefix_Candidate).Add(state.VoteTo); + StorageKey key = CreateStorageKey(Prefix_Candidate, state.VoteTo); CandidateState candidate = engine.SnapshotCache.GetAndChange(key).GetInteroperable(); candidate.Votes += amount; CheckCandidate(engine.SnapshotCache, state.VoteTo, candidate); @@ -130,7 +130,7 @@ private GasDistribution DistributeGas(ApplicationEngine engine, UInt160 account, state.BalanceHeight = engine.PersistingBlock.Index; if (state.VoteTo is not null) { - var keyLastest = CreateStorageKey(Prefix_VoterRewardPerCommittee).Add(state.VoteTo); + var keyLastest = CreateStorageKey(Prefix_VoterRewardPerCommittee, state.VoteTo); var latestGasPerVote = engine.SnapshotCache.TryGet(keyLastest) ?? BigInteger.Zero; state.LastGasPerVote = latestGasPerVote; } @@ -154,7 +154,7 @@ private BigInteger CalculateBonus(DataCache snapshot, NeoAccountState state, uin BigInteger neoHolderReward = CalculateNeoHolderReward(snapshot, state.Balance, state.BalanceHeight, end); if (state.VoteTo is null) return neoHolderReward; - var keyLastest = CreateStorageKey(Prefix_VoterRewardPerCommittee).Add(state.VoteTo); + var keyLastest = CreateStorageKey(Prefix_VoterRewardPerCommittee, state.VoteTo); var latestGasPerVote = snapshot.TryGet(keyLastest) ?? BigInteger.Zero; var voteReward = state.Balance * (latestGasPerVote - state.LastGasPerVote) / 100000000L; @@ -185,8 +185,8 @@ private void CheckCandidate(DataCache snapshot, ECPoint pubkey, CandidateState c { if (!candidate.Registered && candidate.Votes.IsZero) { - snapshot.Delete(CreateStorageKey(Prefix_VoterRewardPerCommittee).Add(pubkey)); - snapshot.Delete(CreateStorageKey(Prefix_Candidate).Add(pubkey)); + snapshot.Delete(CreateStorageKey(Prefix_VoterRewardPerCommittee, pubkey)); + snapshot.Delete(CreateStorageKey(Prefix_Candidate, pubkey)); } } @@ -205,7 +205,7 @@ internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfor var cachedCommittee = new CachedCommittee(engine.ProtocolSettings.StandbyCommittee.Select(p => (p, BigInteger.Zero))); engine.SnapshotCache.Add(CreateStorageKey(Prefix_Committee), new StorageItem(cachedCommittee)); engine.SnapshotCache.Add(_votersCount, new StorageItem(Array.Empty())); - engine.SnapshotCache.Add(CreateStorageKey(Prefix_GasPerBlock).AddBigEndian(0u), new StorageItem(5 * GAS.Factor)); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_GasPerBlock, 0u), new StorageItem(5 * GAS.Factor)); engine.SnapshotCache.Add(_registerPrice, new StorageItem(1000 * GAS.Factor)); return Mint(engine, Contract.GetBFTAddress(engine.ProtocolSettings.StandbyValidators), TotalAmount, false); } @@ -269,7 +269,7 @@ internal override async ContractTask PostPersistAsync(ApplicationEngine engine) if (Votes > 0) { BigInteger voterSumRewardPerNEO = factor * voterRewardOfEachCommittee / Votes; - StorageKey voterRewardKey = CreateStorageKey(Prefix_VoterRewardPerCommittee).Add(PublicKey); + StorageKey voterRewardKey = CreateStorageKey(Prefix_VoterRewardPerCommittee, PublicKey); StorageItem lastRewardPerNeo = engine.SnapshotCache.GetAndChange(voterRewardKey, () => new StorageItem(BigInteger.Zero)); lastRewardPerNeo.Add(voterSumRewardPerNEO); } @@ -284,8 +284,8 @@ private void SetGasPerBlock(ApplicationEngine engine, BigInteger gasPerBlock) throw new ArgumentOutOfRangeException(nameof(gasPerBlock)); if (!CheckCommittee(engine)) throw new InvalidOperationException(); - uint index = engine.PersistingBlock.Index + 1; - StorageItem entry = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_GasPerBlock).AddBigEndian(index), () => new StorageItem(gasPerBlock)); + var index = engine.PersistingBlock.Index + 1; + var entry = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_GasPerBlock, index), () => new StorageItem(gasPerBlock)); entry.Set(gasPerBlock); } @@ -323,8 +323,8 @@ public long GetRegisterPrice(IReadOnlyStore snapshot) private IEnumerable<(uint Index, BigInteger GasPerBlock)> GetSortedGasRecords(DataCache snapshot, uint end) { - byte[] key = CreateStorageKey(Prefix_GasPerBlock).AddBigEndian(end).ToArray(); - byte[] boundary = CreateStorageKey(Prefix_GasPerBlock).ToArray(); + var key = CreateStorageKey(Prefix_GasPerBlock, end).ToArray(); + var boundary = CreateStorageKey(Prefix_GasPerBlock).ToArray(); return snapshot.FindRange(key, boundary, SeekDirection.Backward) .Select(u => (BinaryPrimitives.ReadUInt32BigEndian(u.Key.Key.Span[^sizeof(uint)..]), (BigInteger)u.Value)); } @@ -339,7 +339,7 @@ public long GetRegisterPrice(IReadOnlyStore snapshot) [ContractMethod(CpuFee = 1 << 17, RequiredCallFlags = CallFlags.ReadStates)] public BigInteger UnclaimedGas(DataCache snapshot, UInt160 account, uint end) { - StorageItem storage = snapshot.TryGet(CreateStorageKey(Prefix_Account).Add(account)); + StorageItem storage = snapshot.TryGet(CreateStorageKey(Prefix_Account, account)); if (storage is null) return BigInteger.Zero; NeoAccountState state = storage.GetInteroperable(); return CalculateBonus(snapshot, state, end); @@ -381,7 +381,7 @@ private bool RegisterInternal(ApplicationEngine engine, ECPoint pubkey) { if (!engine.CheckWitnessInternal(Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash())) return false; - StorageKey key = CreateStorageKey(Prefix_Candidate).Add(pubkey); + StorageKey key = CreateStorageKey(Prefix_Candidate, pubkey); StorageItem item = engine.SnapshotCache.GetAndChange(key, () => new StorageItem(new CandidateState())); CandidateState state = item.GetInteroperable(); if (state.Registered) return true; @@ -397,7 +397,7 @@ private bool UnregisterCandidate(ApplicationEngine engine, ECPoint pubkey) { if (!engine.CheckWitnessInternal(Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash())) return false; - StorageKey key = CreateStorageKey(Prefix_Candidate).Add(pubkey); + StorageKey key = CreateStorageKey(Prefix_Candidate, pubkey); if (engine.SnapshotCache.TryGet(key) is null) return true; StorageItem item = engine.SnapshotCache.GetAndChange(key); CandidateState state = item.GetInteroperable(); @@ -414,13 +414,13 @@ private bool UnregisterCandidate(ApplicationEngine engine, ECPoint pubkey) private async ContractTask Vote(ApplicationEngine engine, UInt160 account, ECPoint voteTo) { if (!engine.CheckWitnessInternal(account)) return false; - NeoAccountState state_account = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Account).Add(account))?.GetInteroperable(); + NeoAccountState state_account = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Account, account))?.GetInteroperable(); if (state_account is null) return false; if (state_account.Balance == 0) return false; CandidateState validator_new = null; if (voteTo != null) { - validator_new = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Candidate).Add(voteTo))?.GetInteroperable(); + validator_new = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Candidate, voteTo))?.GetInteroperable(); if (validator_new is null) return false; if (!validator_new.Registered) return false; } @@ -435,7 +435,7 @@ private async ContractTask Vote(ApplicationEngine engine, UInt160 account, GasDistribution gasDistribution = DistributeGas(engine, account, state_account); if (state_account.VoteTo != null) { - StorageKey key = CreateStorageKey(Prefix_Candidate).Add(state_account.VoteTo); + StorageKey key = CreateStorageKey(Prefix_Candidate, state_account.VoteTo); StorageItem storage_validator = engine.SnapshotCache.GetAndChange(key); CandidateState state_validator = storage_validator.GetInteroperable(); state_validator.Votes -= state_account.Balance; @@ -443,7 +443,7 @@ private async ContractTask Vote(ApplicationEngine engine, UInt160 account, } if (voteTo != null && voteTo != state_account.VoteTo) { - StorageKey voterRewardKey = CreateStorageKey(Prefix_VoterRewardPerCommittee).Add(voteTo); + StorageKey voterRewardKey = CreateStorageKey(Prefix_VoterRewardPerCommittee, voteTo); var latestGasPerVote = engine.SnapshotCache.TryGet(voterRewardKey) ?? BigInteger.Zero; state_account.LastGasPerVote = latestGasPerVote; } @@ -512,7 +512,7 @@ private IIterator GetAllCandidates(DataCache snapshot) [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] public BigInteger GetCandidateVote(IReadOnlyStore snapshot, ECPoint pubKey) { - var key = CreateStorageKey(Prefix_Candidate).Add(pubKey); + var key = CreateStorageKey(Prefix_Candidate, pubKey); var state = snapshot.TryGet(key, out var item) ? item.GetInteroperable() : null; return state?.Registered == true ? state.Votes : -1; } @@ -537,7 +537,7 @@ public ECPoint[] GetCommittee(IReadOnlyStore snapshot) [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] public NeoAccountState GetAccountState(IReadOnlyStore snapshot, UInt160 account) { - var key = CreateStorageKey(Prefix_Account).Add(account); + var key = CreateStorageKey(Prefix_Account, account); return snapshot.TryGet(key, out var item) ? item.GetInteroperable() : null; } diff --git a/src/Neo/SmartContract/Native/OracleContract.cs b/src/Neo/SmartContract/Native/OracleContract.cs index 45c5e0840c..eacc74b562 100644 --- a/src/Neo/SmartContract/Native/OracleContract.cs +++ b/src/Neo/SmartContract/Native/OracleContract.cs @@ -103,7 +103,7 @@ private UInt256 GetOriginalTxid(ApplicationEngine engine) /// The pending request. Or if no request with the specified id is found. public OracleRequest GetRequest(IReadOnlyStore snapshot, ulong id) { - var key = CreateStorageKey(Prefix_Request).AddBigEndian(id); + var key = CreateStorageKey(Prefix_Request, id); return snapshot.TryGet(key, out var item) ? item.GetInteroperable() : null; } @@ -127,17 +127,17 @@ public OracleRequest GetRequest(IReadOnlyStore snapshot, ulong id) /// All the requests with the specified url. public IEnumerable<(ulong, OracleRequest)> GetRequestsByUrl(IReadOnlyStore snapshot, string url) { - var listKey = CreateStorageKey(Prefix_IdList).Add(GetUrlHash(url)); + var listKey = CreateStorageKey(Prefix_IdList, GetUrlHash(url)); IdList list = snapshot.TryGet(listKey, out var item) ? item.GetInteroperable() : null; if (list is null) yield break; foreach (ulong id in list) { - var key = CreateStorageKey(Prefix_Request).AddBigEndian(id); + var key = CreateStorageKey(Prefix_Request, id); yield return (id, snapshot[key].GetInteroperable()); } } - private static byte[] GetUrlHash(string url) + private static ReadOnlySpan GetUrlHash(string url) { return Crypto.Hash160(Utility.StrictUTF8.GetBytes(url)); } @@ -162,13 +162,13 @@ internal override async ContractTask PostPersistAsync(ApplicationEngine engine) if (response is null) continue; //Remove the request from storage - StorageKey key = CreateStorageKey(Prefix_Request).AddBigEndian(response.Id); + StorageKey key = CreateStorageKey(Prefix_Request, response.Id); OracleRequest request = engine.SnapshotCache.TryGet(key)?.GetInteroperable(); if (request == null) continue; engine.SnapshotCache.Delete(key); //Remove the id from IdList - key = CreateStorageKey(Prefix_IdList).Add(GetUrlHash(request.Url)); + key = CreateStorageKey(Prefix_IdList, GetUrlHash(request.Url)); IdList list = engine.SnapshotCache.GetAndChange(key).GetInteroperable(); if (!list.Remove(response.Id)) throw new InvalidOperationException(); if (list.Count == 0) engine.SnapshotCache.Delete(key); @@ -215,7 +215,7 @@ private async ContractTask Request(ApplicationEngine engine, string url, string //Put the request to storage if (ContractManagement.GetContract(engine.SnapshotCache, engine.CallingScriptHash) is null) throw new InvalidOperationException(); - engine.SnapshotCache.Add(CreateStorageKey(Prefix_Request).AddBigEndian(id), new StorageItem(new OracleRequest + engine.SnapshotCache.Add(CreateStorageKey(Prefix_Request, id), new StorageItem(new OracleRequest { OriginalTxid = GetOriginalTxid(engine), GasForResponse = gasForResponse, @@ -227,7 +227,7 @@ private async ContractTask Request(ApplicationEngine engine, string url, string })); //Add the id to the IdList - var list = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_IdList).Add(GetUrlHash(url)), () => new StorageItem(new IdList())).GetInteroperable(); + var list = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_IdList, GetUrlHash(url)), () => new StorageItem(new IdList())).GetInteroperable(); if (list.Count >= 256) throw new InvalidOperationException("There are too many pending responses for this url"); list.Add(id); diff --git a/src/Neo/SmartContract/Native/PolicyContract.cs b/src/Neo/SmartContract/Native/PolicyContract.cs index b591b56640..049a051db2 100644 --- a/src/Neo/SmartContract/Native/PolicyContract.cs +++ b/src/Neo/SmartContract/Native/PolicyContract.cs @@ -167,7 +167,7 @@ private uint GetAttributeFee(IReadOnlyStore snapshot, byte attributeType, bool a if (!Enum.IsDefined(typeof(TransactionAttributeType), attributeType) || (!allowNotaryAssisted && attributeType == (byte)(TransactionAttributeType.NotaryAssisted))) throw new InvalidOperationException($"Unsupported value {attributeType} of {nameof(attributeType)}"); - var key = CreateStorageKey(Prefix_AttributeFee).Add(attributeType); + var key = CreateStorageKey(Prefix_AttributeFee, attributeType); return snapshot.TryGet(key, out var item) ? (uint)(BigInteger)item : DefaultAttributeFee; } @@ -180,7 +180,7 @@ private uint GetAttributeFee(IReadOnlyStore snapshot, byte attributeType, bool a [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] public bool IsBlocked(IReadOnlyStore snapshot, UInt160 account) { - return snapshot.Contains(CreateStorageKey(Prefix_BlockedAccount).Add(account)); + return snapshot.Contains(CreateStorageKey(Prefix_BlockedAccount, account)); } /// @@ -225,7 +225,7 @@ private void SetAttributeFee(ApplicationEngine engine, byte attributeType, uint if (value > MaxAttributeFee) throw new ArgumentOutOfRangeException(nameof(value)); if (!CheckCommittee(engine)) throw new InvalidOperationException(); - engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_AttributeFee).Add(attributeType), () => new StorageItem(DefaultAttributeFee)).Set(value); + engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_AttributeFee, attributeType), () => new StorageItem(DefaultAttributeFee)).Set(value); } [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] @@ -263,7 +263,7 @@ internal bool BlockAccount(DataCache snapshot, UInt160 account) { if (IsNative(account)) throw new InvalidOperationException("It's impossible to block a native contract."); - var key = CreateStorageKey(Prefix_BlockedAccount).Add(account); + var key = CreateStorageKey(Prefix_BlockedAccount, account); if (snapshot.Contains(key)) return false; snapshot.Add(key, new StorageItem(Array.Empty())); @@ -275,7 +275,7 @@ private bool UnblockAccount(ApplicationEngine engine, UInt160 account) { if (!CheckCommittee(engine)) throw new InvalidOperationException(); - var key = CreateStorageKey(Prefix_BlockedAccount).Add(account); + var key = CreateStorageKey(Prefix_BlockedAccount, account); if (!engine.SnapshotCache.Contains(key)) return false; engine.SnapshotCache.Delete(key); diff --git a/src/Neo/SmartContract/Native/RoleManagement.cs b/src/Neo/SmartContract/Native/RoleManagement.cs index 62706a854e..b5df9c3b20 100644 --- a/src/Neo/SmartContract/Native/RoleManagement.cs +++ b/src/Neo/SmartContract/Native/RoleManagement.cs @@ -52,8 +52,8 @@ public ECPoint[] GetDesignatedByRole(DataCache snapshot, Role role, uint index) throw new ArgumentOutOfRangeException(nameof(role)); if (Ledger.CurrentIndex(snapshot) + 1 < index) throw new ArgumentOutOfRangeException(nameof(index)); - byte[] key = CreateStorageKey((byte)role).AddBigEndian(index).ToArray(); - byte[] boundary = CreateStorageKey((byte)role).ToArray(); + var key = CreateStorageKey((byte)role, index).ToArray(); + var boundary = CreateStorageKey((byte)role).ToArray(); return snapshot.FindRange(key, boundary, SeekDirection.Backward) .Select(u => u.Value.GetInteroperable().ToArray()) .FirstOrDefault() ?? []; @@ -70,8 +70,8 @@ private void DesignateAsRole(ApplicationEngine engine, Role role, ECPoint[] node throw new InvalidOperationException(nameof(DesignateAsRole)); if (engine.PersistingBlock is null) throw new InvalidOperationException(nameof(DesignateAsRole)); - uint index = engine.PersistingBlock.Index + 1; - var key = CreateStorageKey((byte)role).AddBigEndian(index); + var index = engine.PersistingBlock.Index + 1; + var key = CreateStorageKey((byte)role, index); if (engine.SnapshotCache.Contains(key)) throw new InvalidOperationException(); NodeList list = new(); diff --git a/src/Neo/SmartContract/StorageKey.cs b/src/Neo/SmartContract/StorageKey.cs index dcfccae6e5..9124e022b8 100644 --- a/src/Neo/SmartContract/StorageKey.cs +++ b/src/Neo/SmartContract/StorageKey.cs @@ -11,7 +11,9 @@ #nullable enable +using Neo.Cryptography.ECC; using Neo.Extensions; +using Neo.IO; using System; using System.Buffers.Binary; using System.Runtime.CompilerServices; @@ -38,11 +40,228 @@ public sealed record StorageKey // NOTE: StorageKey is readonly, so we can cache the hash code. private int _hashCode = 0; + public const int PrefixLength = sizeof(int) + sizeof(byte); + + private const int ByteLength = PrefixLength + sizeof(byte); + private const int Int32Length = PrefixLength + sizeof(int); + private const int Int64Length = PrefixLength + sizeof(long); + private const int UInt160Length = PrefixLength + UInt160.Length; + private const int UInt256Length = PrefixLength + UInt256.Length; + private const int UInt256UInt160Length = PrefixLength + UInt256.Length + UInt160.Length; + + #region Static methods + + /// + /// Create StorageKey + /// + /// Data to write + /// The id of the contract. + /// The prefix of the key. + /// The class + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void FillHeader(Span data, int id, byte prefix) + { + BinaryPrimitives.WriteInt32LittleEndian(data, id); + data[sizeof(int)] = prefix; + } + + /// + /// Create StorageKey + /// + /// The id of the contract. + /// The prefix of the key. + /// The class + public static StorageKey Create(int id, byte prefix) + { + var data = new byte[PrefixLength]; + FillHeader(data, id, prefix); + return new(id, data); + } + + /// + /// Create StorageKey + /// + /// The id of the contract. + /// The prefix of the key. + /// Content + /// The class + public static StorageKey Create(int id, byte prefix, byte content) + { + var data = new byte[ByteLength]; + FillHeader(data, id, prefix); + data[PrefixLength] = content; + return new(id, data); + } + + /// + /// Create StorageKey + /// + /// The id of the contract. + /// The prefix of the key. + /// Hash + /// The class + public static StorageKey Create(int id, byte prefix, UInt160 hash) + { + var data = new byte[UInt160Length]; + FillHeader(data, id, prefix); + hash.GetSpan().CopyTo(data.AsSpan()[PrefixLength..]); + return new(id, data); + } + + /// + /// Create StorageKey + /// + /// The id of the contract. + /// The prefix of the key. + /// Hash + /// The class + public static StorageKey Create(int id, byte prefix, UInt256 hash) + { + var data = new byte[UInt256Length]; + FillHeader(data, id, prefix); + hash.GetSpan().CopyTo(data.AsSpan()[PrefixLength..]); + return new(id, data); + } + + /// + /// Create StorageKey + /// + /// The id of the contract. + /// The prefix of the key. + /// Public key + /// The class + public static StorageKey Create(int id, byte prefix, ECPoint publicKey) + { + return Create(id, prefix, publicKey.GetSpan()); + } + + /// + /// Create StorageKey + /// + /// The id of the contract. + /// The prefix of the key. + /// Hash + /// Signer + /// The class + public static StorageKey Create(int id, byte prefix, UInt256 hash, UInt160 signer) + { + var data = new byte[UInt256UInt160Length]; + FillHeader(data, id, prefix); + hash.GetSpan().CopyTo(data.AsSpan()[PrefixLength..]); + signer.GetSpan().CopyTo(data.AsSpan()[UInt256Length..]); + return new(id, data); + } + + /// + /// Create StorageKey + /// + /// The id of the contract. + /// The prefix of the key. + /// Big Endian key. + /// The class + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static StorageKey Create(int id, byte prefix, int bigEndian) + { + var data = new byte[Int32Length]; + FillHeader(data, id, prefix); + BinaryPrimitives.WriteInt32BigEndian(data.AsSpan()[PrefixLength..], bigEndian); + return new(id, data); + } + + /// + /// Create StorageKey + /// + /// The id of the contract. + /// The prefix of the key. + /// Big Endian key. + /// The class + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static StorageKey Create(int id, byte prefix, uint bigEndian) + { + var data = new byte[Int32Length]; + FillHeader(data, id, prefix); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan()[PrefixLength..], bigEndian); + return new(id, data); + } + + /// + /// Create StorageKey + /// + /// The id of the contract. + /// The prefix of the key. + /// Big Endian key. + /// The class + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static StorageKey Create(int id, byte prefix, long bigEndian) + { + var data = new byte[Int64Length]; + FillHeader(data, id, prefix); + BinaryPrimitives.WriteInt64BigEndian(data.AsSpan()[PrefixLength..], bigEndian); + return new(id, data); + } + + /// + /// Create StorageKey + /// + /// The id of the contract. + /// The prefix of the key. + /// Big Endian key. + /// The class + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static StorageKey Create(int id, byte prefix, ulong bigEndian) + { + var data = new byte[Int64Length]; + FillHeader(data, id, prefix); + BinaryPrimitives.WriteUInt64BigEndian(data.AsSpan()[PrefixLength..], bigEndian); + return new(id, data); + } + + /// + /// Create StorageKey + /// + /// The id of the contract. + /// The prefix of the key. + /// Content + /// The class + public static StorageKey Create(int id, byte prefix, ReadOnlySpan content) + { + var data = new byte[PrefixLength + content.Length]; + FillHeader(data, id, prefix); + content.CopyTo(data.AsSpan()[PrefixLength..]); + return new(id, data); + } + + /// + /// Create StorageKey + /// + /// The id of the contract. + /// The prefix of the key. + /// Content + /// The class + public static StorageKey Create(int id, byte prefix, ISerializableSpan content) + { + return Create(id, prefix, content.GetSpan()); + } + + #endregion + public StorageKey() { _cache = null; } + /// + /// Initializes a new instance of the class. + /// + /// Contract Id + /// The cached byte array. NOTE: It must be read-only and can be modified by the caller. + private StorageKey(int id, byte[] cache) + { + _cache = cache; + Id = id; + Key = cache.AsMemory(sizeof(int)); + } + /// /// Initializes a new instance of the class. /// diff --git a/tests/Neo.UnitTests/Ledger/UT_StorageKey.cs b/tests/Neo.UnitTests/Ledger/UT_StorageKey.cs index d2a2ac86bb..03a9bb447b 100644 --- a/tests/Neo.UnitTests/Ledger/UT_StorageKey.cs +++ b/tests/Neo.UnitTests/Ledger/UT_StorageKey.cs @@ -10,6 +10,7 @@ // modifications are permitted. using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Cryptography.ECC; using Neo.Extensions; using Neo.SmartContract; using System; @@ -19,6 +20,68 @@ namespace Neo.UnitTests.Ledger [TestClass] public class UT_StorageKey { + [TestMethod] + public void SameTest() + { + // None + var key = new KeyBuilder(1, 2); + CollectionAssert.AreEqual(key.ToArray(), StorageKey.Create(1, 2).ToArray()); + + // Byte[] + key = new KeyBuilder(1, 2); + key.Add([3, 4]); + CollectionAssert.AreEqual(key.ToArray(), StorageKey.Create(1, 2, [3, 4]).ToArray()); + + // Byte + key = new KeyBuilder(1, 2); + key.Add((byte)3); + CollectionAssert.AreEqual(key.ToArray(), StorageKey.Create(1, 2, (byte)3).ToArray()); + + // Int + key = new KeyBuilder(1, 2); + key.AddBigEndian((int)3); + CollectionAssert.AreEqual(key.ToArray(), StorageKey.Create(1, 2, (int)3).ToArray()); + + // UInt + key = new KeyBuilder(1, 2); + key.AddBigEndian((uint)3); + CollectionAssert.AreEqual(key.ToArray(), StorageKey.Create(1, 2, (uint)3).ToArray()); + + // Long + key = new KeyBuilder(1, 2); + key.AddBigEndian((long)3); + CollectionAssert.AreEqual(key.ToArray(), StorageKey.Create(1, 2, (long)3).ToArray()); + + // ULong + key = new KeyBuilder(1, 2); + key.AddBigEndian((ulong)3); + CollectionAssert.AreEqual(key.ToArray(), StorageKey.Create(1, 2, (ulong)3).ToArray()); + + // UInt160 + key = new KeyBuilder(1, 2); + key.Add(UInt160.Parse("2d3b96ae1bcc5a585e075e3b81920210dec16302")); + CollectionAssert.AreEqual(key.ToArray(), StorageKey.Create(1, 2, UInt160.Parse("2d3b96ae1bcc5a585e075e3b81920210dec16302")).ToArray()); + + // UInt256 + key = new KeyBuilder(1, 2); + key.Add(UInt256.Parse("0x761a9bb72ca2a63984db0cc43f943a2a25e464f62d1a91114c2b6fbbfd24b51d")); + CollectionAssert.AreEqual(key.ToArray(), StorageKey.Create(1, 2, + UInt256.Parse("0x761a9bb72ca2a63984db0cc43f943a2a25e464f62d1a91114c2b6fbbfd24b51d")).ToArray()); + + // UInt256+UInt160 + key = new KeyBuilder(1, 2); + key.Add(UInt256.Parse("0x761a9bb72ca2a63984db0cc43f943a2a25e464f62d1a91114c2b6fbbfd24b51d")); + key.Add(UInt160.Parse("2d3b96ae1bcc5a585e075e3b81920210dec16302")); + CollectionAssert.AreEqual(key.ToArray(), StorageKey.Create(1, 2, + UInt256.Parse("0x761a9bb72ca2a63984db0cc43f943a2a25e464f62d1a91114c2b6fbbfd24b51d"), + UInt160.Parse("2d3b96ae1bcc5a585e075e3b81920210dec16302")).ToArray()); + + // ISerializable + key = new KeyBuilder(1, 2); + key.Add(ECCurve.Secp256r1.G); + CollectionAssert.AreEqual(key.ToArray(), StorageKey.Create(1, 2, ECCurve.Secp256r1.G).ToArray()); + } + [TestMethod] public void Id_Get() { From cb41f9228e56dfce8af2b65023f5dfe191f53586 Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt Date: Tue, 18 Feb 2025 04:39:41 -0500 Subject: [PATCH 21/22] [`Fix`] Linux File Access (#3769) * Add some stuff to hope once and for all fix the linux workflows * update workflow * update nuget.yml * Change dig to detailed, log too big * Fix targets * revert main.yml * Update .github/workflows/main.yml * disable BuildInParallel --------- Co-authored-by: Jimmy Co-authored-by: Shargon --- src/Plugins/LevelDBStore/LevelDBStore.csproj | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Plugins/LevelDBStore/LevelDBStore.csproj b/src/Plugins/LevelDBStore/LevelDBStore.csproj index b28a51a520..0757695ef7 100644 --- a/src/Plugins/LevelDBStore/LevelDBStore.csproj +++ b/src/Plugins/LevelDBStore/LevelDBStore.csproj @@ -1,7 +1,8 @@ - + net9.0 + false Neo.Plugins.Storage.LevelDBStore Neo.Plugins.Storage true @@ -9,10 +10,11 @@ ../../../bin/$(PackageId) - - - - + + + + + @@ -23,7 +25,7 @@ - + true false Always From 8af28d108e9026486e2d28c10679dccacf22d7e7 Mon Sep 17 00:00:00 2001 From: Shargon Date: Tue, 18 Feb 2025 02:10:07 -0800 Subject: [PATCH 22/22] Update src/Neo/SmartContract/Native/PolicyContract.cs --- src/Neo/SmartContract/Native/PolicyContract.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo/SmartContract/Native/PolicyContract.cs b/src/Neo/SmartContract/Native/PolicyContract.cs index 049a051db2..cce437d8f0 100644 --- a/src/Neo/SmartContract/Native/PolicyContract.cs +++ b/src/Neo/SmartContract/Native/PolicyContract.cs @@ -92,7 +92,7 @@ internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfor } if (hardfork == Hardfork.HF_Echidna) { - engine.SnapshotCache.Add(CreateStorageKey(Prefix_AttributeFee).Add((byte)TransactionAttributeType.NotaryAssisted), new StorageItem(DefaultNotaryAssistedAttributeFee)); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_AttributeFee, (byte)TransactionAttributeType.NotaryAssisted), new StorageItem(DefaultNotaryAssistedAttributeFee)); } return ContractTask.CompletedTask; }