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);
+ }
}
}