diff --git a/src/Neo.CLI/CLI/MainService.Blockchain.cs b/src/Neo.CLI/CLI/MainService.Blockchain.cs index 090939de49..8a32eb8023 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/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/NotaryAssisted.cs b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs new file mode 100644 index 0000000000..0a110688a8 --- /dev/null +++ b/src/Neo/Network/P2P/Payloads/NotaryAssisted.cs @@ -0,0 +1,76 @@ +// 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 + { + /// + /// Native Notary contract hash stub used until native Notary contract is properly implemented. + /// + private static readonly UInt160 notaryHash = Neo.SmartContract.Helper.GetContractHash(UInt160.Zero, 0, "Notary"); + + /// + /// Indicates the number of keys participating in the transaction (main or fallback) signing process. + /// + public byte NKeys; + + public override TransactionAttributeType Type => TransactionAttributeType.NotaryAssisted; + + public override bool AllowMultiple => false; + + public override int Size => base.Size + sizeof(byte); + + protected override void DeserializeWithoutType(ref MemoryReader reader) + { + NKeys = reader.ReadByte(); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(NKeys); + } + + public override JObject ToJson() + { + JObject json = base.ToJson(); + json["nkeys"] = NKeys; + return json; + } + + public override bool Verify(DataCache snapshot, Transaction tx) + { + return tx.Signers.Any(p => p.Account.Equals(notaryHash)); + } + + /// + /// Calculates the network fee needed to pay for NotaryAssisted attribute. According to the + /// https://github.com/neo-project/neo/issues/1573#issuecomment-704874472, network fee consists of + /// the base Notary service fee per key multiplied by the expected number of transactions that should + /// be collected by the service to complete Notary request increased by one (for Notary node witness + /// itself). + /// + /// The snapshot used to read data. + /// The transaction to calculate. + /// The network fee of the NotaryAssisted attribute. + public override long CalculateNetworkFee(DataCache snapshot, Transaction tx) + { + return (NKeys + 1) * base.CalculateNetworkFee(snapshot, tx); + } + } +} diff --git a/src/Neo/Network/P2P/Payloads/Transaction.cs b/src/Neo/Network/P2P/Payloads/Transaction.cs index b922674d91..d3e3b06a8f 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.Type == TransactionAttributeType.NotaryAssisted && !settings.IsHardforkEnabled(Hardfork.HF_Echidna, height)) + return VerifyResult.InvalidAttribute; if (!attribute.Verify(snapshot, this)) return VerifyResult.InvalidAttribute; - else - attributesFee += attribute.CalculateNetworkFee(snapshot, this); + 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/Network/P2P/Payloads/TransactionAttributeType.cs b/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs index 116f136c07..7e7f4f4252 100644 --- a/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs +++ b/src/Neo/Network/P2P/Payloads/TransactionAttributeType.cs @@ -40,6 +40,12 @@ public enum TransactionAttributeType : byte /// Indicates that the transaction conflicts with . /// [ReflectionCache(typeof(Conflicts))] - Conflicts = 0x21 + Conflicts = 0x21, + + /// + /// Indicates that the transaction uses notary request service with number of keys. + /// + [ReflectionCache(typeof(NotaryAssisted))] + NotaryAssisted = 0x22 } } diff --git a/src/Neo/SmartContract/Native/GasToken.cs b/src/Neo/SmartContract/Native/GasToken.cs index b8a185b6f1..61c6180e9f 100644 --- a/src/Neo/SmartContract/Native/GasToken.cs +++ b/src/Neo/SmartContract/Native/GasToken.cs @@ -43,6 +43,14 @@ internal override async ContractTask OnPersistAsync(ApplicationEngine engine) { await Burn(engine, tx.Sender, tx.SystemFee + tx.NetworkFee); totalNetworkFee += tx.NetworkFee; + + // Reward for NotaryAssisted attribute will be minted to designated notary nodes + // by Notary contract. + var notaryAssisted = tx.GetAttribute(); + if (notaryAssisted is not null) + { + totalNetworkFee -= (notaryAssisted.NKeys + 1) * Policy.GetAttributeFee(engine.Snapshot, (byte)notaryAssisted.Type); + } } ECPoint[] validators = NEO.GetNextBlockValidators(engine.Snapshot, engine.ProtocolSettings.ValidatorsCount); UInt160 primary = Contract.CreateSignatureRedeemScript(validators[engine.PersistingBlock.PrimaryIndex]).ToScriptHash(); diff --git a/src/Neo/SmartContract/Native/PolicyContract.cs b/src/Neo/SmartContract/Native/PolicyContract.cs index 2da6255094..755dd3a30f 100644 --- a/src/Neo/SmartContract/Native/PolicyContract.cs +++ b/src/Neo/SmartContract/Native/PolicyContract.cs @@ -44,6 +44,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. /// @@ -75,6 +80,10 @@ 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_Echidna) + { + engine.Snapshot.Add(CreateStorageKey(Prefix_AttributeFee).Add((byte)TransactionAttributeType.NotaryAssisted), new StorageItem(DefaultNotaryAssistedAttributeFee)); + } return ContractTask.CompletedTask; } @@ -112,15 +121,41 @@ public uint GetStoragePrice(DataCache snapshot) } /// - /// Gets the fee for attribute. + /// Gets the fee for attribute before Echidna hardfork. + /// + /// The snapshot used to read data. + /// Attribute type excluding + /// The fee for attribute. + [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 Echidna hardfork. /// /// The snapshot used to read data. /// Attribute type /// The fee for attribute. - [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + [ContractMethod(true, Hardfork.HF_Echidna, 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; @@ -139,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 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_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 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_Echidna, 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(); 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(), }; } diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs new file mode 100644 index 0000000000..778fe7a53d --- /dev/null +++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_NotaryAssisted.cs @@ -0,0 +1,91 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_NotaryAssisted.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.IO; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using System; + +namespace Neo.UnitTests.Network.P2P.Payloads +{ + [TestClass] + public class UT_NotaryAssisted + { + // Use the hard-coded Notary hash value from NeoGo to ensure hashes are compatible. + private static readonly UInt160 notaryHash = UInt160.Parse("0xc1e14f19c3e60d0b9244d06dd7ba9b113135ec3b"); + + [TestMethod] + public void Size_Get() + { + var attr = new NotaryAssisted() { NKeys = 4 }; + attr.Size.Should().Be(1 + 1); + } + + [TestMethod] + public void ToJson() + { + var attr = new NotaryAssisted() { NKeys = 4 }; + var json = attr.ToJson().ToString(); + Assert.AreEqual(@"{""type"":""NotaryAssisted"",""nkeys"":4}", json); + } + + [TestMethod] + public void DeserializeAndSerialize() + { + var attr = new NotaryAssisted() { NKeys = 4 }; + + var clone = attr.ToArray().AsSerializable(); + Assert.AreEqual(clone.Type, attr.Type); + + // As transactionAttribute + byte[] buffer = attr.ToArray(); + var reader = new MemoryReader(buffer); + clone = TransactionAttribute.DeserializeFrom(ref reader) as NotaryAssisted; + Assert.AreEqual(clone.Type, attr.Type); + + // Wrong type + buffer[0] = 0xff; + Assert.ThrowsException(() => + { + var reader = new MemoryReader(buffer); + TransactionAttribute.DeserializeFrom(ref reader); + }); + } + + [TestMethod] + public void Verify() + { + var attr = new NotaryAssisted() { NKeys = 4 }; + + // Temporary use Notary contract hash stub for valid transaction. + var txGood = new Transaction { Signers = new Signer[] { new Signer() { Account = notaryHash } } }; + var txBad = new Transaction { Signers = new Signer[] { new Signer() { Account = UInt160.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01") } } }; + var snapshot = TestBlockchain.GetTestSnapshot(); + + Assert.IsTrue(attr.Verify(snapshot, txGood)); + Assert.IsFalse(attr.Verify(snapshot, txBad)); + } + + [TestMethod] + public void CalculateNetworkFee() + { + var snapshot = TestBlockchain.GetTestSnapshot(); + var attr = new NotaryAssisted() { NKeys = 4 }; + var tx = new Transaction { Signers = new Signer[] { new Signer() { Account = notaryHash } } }; + + Assert.AreEqual((4 + 1) * 1000_0000, attr.CalculateNetworkFee(snapshot, tx)); + } + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs index 51617b6744..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); + } } }