diff --git a/src/Neo/Network/P2P/Payloads/Conflicts.cs b/src/Neo/Network/P2P/Payloads/Conflicts.cs index db00b03259..f8ef364365 100644 --- a/src/Neo/Network/P2P/Payloads/Conflicts.cs +++ b/src/Neo/Network/P2P/Payloads/Conflicts.cs @@ -43,5 +43,10 @@ public override bool Verify(DataCache snapshot, Transaction tx) // on-chain transaction. return !NativeContract.Ledger.ContainsTransaction(snapshot, Hash); } + + public override long CalculateNetworkFee(DataCache snapshot, Transaction tx) + { + return tx.Signers.Length * base.CalculateNetworkFee(snapshot, tx); + } } } diff --git a/src/Neo/Network/P2P/Payloads/Transaction.cs b/src/Neo/Network/P2P/Payloads/Transaction.cs index 24581e2b18..73ae25bac8 100644 --- a/src/Neo/Network/P2P/Payloads/Transaction.cs +++ b/src/Neo/Network/P2P/Payloads/Transaction.cs @@ -365,10 +365,13 @@ public virtual VerifyResult VerifyStateDependent(ProtocolSettings settings, Data if (NativeContract.Policy.IsBlocked(snapshot, hash)) return VerifyResult.PolicyFail; 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; - long net_fee = NetworkFee - Size * NativeContract.Policy.GetFeePerByte(snapshot); + else + attributesFee += attribute.CalculateNetworkFee(snapshot, this); + long net_fee = NetworkFee - (Size * NativeContract.Policy.GetFeePerByte(snapshot)) - attributesFee; if (net_fee < 0) return VerifyResult.InsufficientFunds; if (net_fee > MaxVerificationGas) net_fee = MaxVerificationGas; diff --git a/src/Neo/Network/P2P/Payloads/TransactionAttribute.cs b/src/Neo/Network/P2P/Payloads/TransactionAttribute.cs index da8620a95c..0da87a94f4 100644 --- a/src/Neo/Network/P2P/Payloads/TransactionAttribute.cs +++ b/src/Neo/Network/P2P/Payloads/TransactionAttribute.cs @@ -8,12 +8,13 @@ // Redistribution and use in source and binary forms with or without // modifications are permitted. +using System; +using System.IO; using Neo.IO; using Neo.IO.Caching; using Neo.Json; using Neo.Persistence; -using System; -using System.IO; +using Neo.SmartContract.Native; namespace Neo.Network.P2P.Payloads { @@ -92,5 +93,7 @@ public void Serialize(BinaryWriter writer) /// The that contains the attribute. /// if the verification passes; otherwise, . public virtual bool Verify(DataCache snapshot, Transaction tx) => true; + + public virtual long CalculateNetworkFee(DataCache snapshot, Transaction tx) => NativeContract.Policy.GetAttributeFee(snapshot, (byte)Type); } } diff --git a/src/Neo/SmartContract/Native/PolicyContract.cs b/src/Neo/SmartContract/Native/PolicyContract.cs index 536bc65691..c754a15a93 100644 --- a/src/Neo/SmartContract/Native/PolicyContract.cs +++ b/src/Neo/SmartContract/Native/PolicyContract.cs @@ -10,9 +10,10 @@ #pragma warning disable IDE0051 -using Neo.Persistence; using System; using System.Numerics; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; namespace Neo.SmartContract.Native { @@ -36,11 +37,21 @@ public sealed class PolicyContract : NativeContract /// public const uint DefaultFeePerByte = 1000; + /// + /// The default fee for attribute. + /// + public const uint DefaultAttributeFee = 0; + /// /// The maximum execution fee factor that the committee can set. /// public const uint MaxExecFeeFactor = 100; + /// + /// The maximum fee for attribute that the committee can set. + /// + public const uint MaxAttributeFee = 10_0000_0000; + /// /// The maximum storage price that the committee can set. /// @@ -50,6 +61,7 @@ public sealed class PolicyContract : NativeContract private const byte Prefix_FeePerByte = 10; private const byte Prefix_ExecFeeFactor = 18; private const byte Prefix_StoragePrice = 19; + private const byte Prefix_AttributeFee = 20; internal PolicyContract() { @@ -96,6 +108,22 @@ public uint GetStoragePrice(DataCache snapshot) return (uint)(BigInteger)snapshot[CreateStorageKey(Prefix_StoragePrice)]; } + /// + /// Gets the fee for attribute. + /// + /// The snapshot used to read data. + /// Attribute type + /// The fee for attribute. + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public uint GetAttributeFee(DataCache snapshot, byte attributeType) + { + if (!Enum.IsDefined(typeof(TransactionAttributeType), attributeType)) throw new InvalidOperationException(); + StorageItem entry = snapshot.TryGet(CreateStorageKey(Prefix_AttributeFee).Add(attributeType)); + if (entry == null) return DefaultAttributeFee; + + return (uint)(BigInteger)entry; + } + /// /// Determines whether the specified account is blocked. /// @@ -108,6 +136,16 @@ 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) + { + if (!Enum.IsDefined(typeof(TransactionAttributeType), attributeType)) throw new InvalidOperationException(); + if (value > MaxAttributeFee) throw new ArgumentOutOfRangeException(nameof(value)); + if (!CheckCommittee(engine)) throw new InvalidOperationException(); + + engine.Snapshot.GetAndChange(CreateStorageKey(Prefix_AttributeFee).Add(attributeType), () => new StorageItem(DefaultAttributeFee)).Set(value); + } + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] private void SetFeePerByte(ApplicationEngine engine, long value) { diff --git a/src/Neo/Wallets/Wallet.cs b/src/Neo/Wallets/Wallet.cs index 7ca5aa8b7b..d908530987 100644 --- a/src/Neo/Wallets/Wallet.cs +++ b/src/Neo/Wallets/Wallet.cs @@ -8,6 +8,13 @@ // Redistribution and use in source and binary forms with or without // modifications are permitted. +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; using Neo.Cryptography; using Neo.IO; using Neo.Network.P2P.Payloads; @@ -17,13 +24,6 @@ using Neo.VM; using Neo.Wallets.NEP6; using Org.BouncyCastle.Crypto.Generators; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Text; using static Neo.SmartContract.Helper; using static Neo.Wallets.Helper; using ECPoint = Neo.Cryptography.ECC.ECPoint; @@ -661,6 +661,10 @@ public long CalculateNetworkFee(DataCache snapshot, Transaction tx, long maxExec // We can support more contract types in the future. } networkFee += size * NativeContract.Policy.GetFeePerByte(snapshot); + foreach (TransactionAttribute attr in tx.Attributes) + { + networkFee += attr.CalculateNetworkFee(snapshot, tx); + } return networkFee; } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs index 6730dd8825..6793c27614 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs @@ -1,3 +1,6 @@ +using System; +using System.Linq; +using System.Numerics; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Neo.IO; @@ -6,8 +9,6 @@ using Neo.SmartContract; using Neo.SmartContract.Native; using Neo.UnitTests.Extensions; -using System; -using System.Linq; namespace Neo.UnitTests.SmartContract.Native { @@ -33,6 +34,71 @@ public void Check_Default() var ret = NativeContract.Policy.Call(snapshot, "getFeePerByte"); ret.Should().BeOfType(); ret.GetInteger().Should().Be(1000); + + ret = NativeContract.Policy.Call(snapshot, "getAttributeFee", new ContractParameter(ContractParameterType.Integer) { Value = (BigInteger)(byte)TransactionAttributeType.Conflicts }); + ret.Should().BeOfType(); + ret.GetInteger().Should().Be(PolicyContract.DefaultAttributeFee); + + Assert.ThrowsException(() => NativeContract.Policy.Call(snapshot, "getAttributeFee", new ContractParameter(ContractParameterType.Integer) { Value = (BigInteger)byte.MaxValue })); + } + + [TestMethod] + public void Check_SetAttributeFee() + { + var snapshot = _snapshot.CreateSnapshot(); + + // Fake blockchain + Block block = new() + { + Header = new Header + { + Index = 1000, + PrevHash = UInt256.Zero + } + }; + + var attr = new ContractParameter(ContractParameterType.Integer) { Value = (BigInteger)(byte)TransactionAttributeType.Conflicts }; + + // Without signature + Assert.ThrowsException(() => + { + NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(), block, + "setAttributeFee", attr, new ContractParameter(ContractParameterType.Integer) { Value = 100500 }); + }); + + var ret = NativeContract.Policy.Call(snapshot, "getAttributeFee", attr); + ret.Should().BeOfType(); + ret.GetInteger().Should().Be(0); + + // With signature, wrong value + UInt160 committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot); + Assert.ThrowsException(() => + { + NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "setAttributeFee", attr, new ContractParameter(ContractParameterType.Integer) { Value = 11_0000_0000 }); + }); + + ret = NativeContract.Policy.Call(snapshot, "getAttributeFee", attr); + ret.Should().BeOfType(); + ret.GetInteger().Should().Be(0); + + // Proper set + ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "setAttributeFee", attr, new ContractParameter(ContractParameterType.Integer) { Value = 300300 }); + ret.IsNull.Should().BeTrue(); + + ret = NativeContract.Policy.Call(snapshot, "getAttributeFee", attr); + ret.Should().BeOfType(); + ret.GetInteger().Should().Be(300300); + + // Set to zero + ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), block, + "setAttributeFee", attr, new ContractParameter(ContractParameterType.Integer) { Value = 0 }); + ret.IsNull.Should().BeTrue(); + + ret = NativeContract.Policy.Call(snapshot, "getAttributeFee", attr); + ret.Should().BeOfType(); + ret.GetInteger().Should().Be(0); } [TestMethod]