diff --git a/src/Neo/SmartContract/ApplicationEngine.cs b/src/Neo/SmartContract/ApplicationEngine.cs index e3b10b746d..87eda1774b 100644 --- a/src/Neo/SmartContract/ApplicationEngine.cs +++ b/src/Neo/SmartContract/ApplicationEngine.cs @@ -300,6 +300,14 @@ protected static void OnSysCall(ExecutionEngine engine, Instruction instruction) /// The amount of GAS, in the unit of datoshi, 1 datoshi = 1e-8 GAS, to be added. protected internal void AddFee(long datoshi) { + // Check whitelist + + if (CurrentContext?.GetState()?.WhiteListed == true) + { + // The execution is whitelisted + return; + } + #pragma warning disable CS0618 // Type or member is obsolete FeeConsumed = GasConsumed = checked(FeeConsumed + datoshi); #pragma warning restore CS0618 // Type or member is obsolete @@ -342,12 +350,21 @@ private ExecutionContext CallContractInternal(ContractState contract, ContractMe else { var executingContract = IsHardforkEnabled(Hardfork.HF_Domovoi) - ? state.Contract // use executing contract state to avoid possible contract update/destroy side-effects, ref. https://github.com/neo-project/neo/pull/3290. - : NativeContract.ContractManagement.GetContract(SnapshotCache, CurrentScriptHash!); + ? state.Contract // use executing contract state to avoid possible contract update/destroy side-effects, ref. https://github.com/neo-project/neo/pull/3290. + : NativeContract.ContractManagement.GetContract(SnapshotCache, CurrentScriptHash!); if (executingContract?.CanCall(contract, method.Name) == false) throw new InvalidOperationException($"Cannot Call Method {method.Name} Of Contract {contract.Hash} From Contract {CurrentScriptHash}"); } + // Check whitelist + + if (IsHardforkEnabled(Hardfork.HF_Faun) && + NativeContract.Policy.IsWhitelistFeeContract(SnapshotCache, contract.Hash, method.Name, method.Parameters.Length, out var fixedFee)) + { + AddFee(fixedFee.Value); + state.WhiteListed = true; + } + if (invocationCounter.TryGetValue(contract.Hash, out var counter)) { invocationCounter[contract.Hash] = counter + 1; diff --git a/src/Neo/SmartContract/ExecutionContextState.cs b/src/Neo/SmartContract/ExecutionContextState.cs index e5b10adf32..44be42e2b7 100644 --- a/src/Neo/SmartContract/ExecutionContextState.cs +++ b/src/Neo/SmartContract/ExecutionContextState.cs @@ -53,5 +53,10 @@ public class ExecutionContextState public int NotificationCount { get; set; } public bool IsDynamicCall { get; set; } + + /// + /// True if the execution is whitelisted by committee + /// + public bool WhiteListed { get; set; } = false; } } diff --git a/src/Neo/SmartContract/Manifest/ContractAbi.cs b/src/Neo/SmartContract/Manifest/ContractAbi.cs index 054a972e56..998efe92bb 100644 --- a/src/Neo/SmartContract/Manifest/ContractAbi.cs +++ b/src/Neo/SmartContract/Manifest/ContractAbi.cs @@ -88,8 +88,10 @@ public static ContractAbi FromJson(JObject json) if (pcount >= 0) { methodDictionary ??= Methods.ToDictionary(p => (p.Name, p.Parameters.Length)); - methodDictionary.TryGetValue((name, pcount), out var method); - return method; + if (methodDictionary.TryGetValue((name, pcount), out var method)) + return method; + + return null; } else { diff --git a/src/Neo/SmartContract/Native/ContractManagement.cs b/src/Neo/SmartContract/Native/ContractManagement.cs index d71771189e..f00eadc215 100644 --- a/src/Neo/SmartContract/Native/ContractManagement.cs +++ b/src/Neo/SmartContract/Native/ContractManagement.cs @@ -369,6 +369,11 @@ private ContractTask Update(ApplicationEngine engine, byte[] nefFile, byte[] man } Helper.Check(new Script(contract.Nef.Script, engine.IsHardforkEnabled(Hardfork.HF_Basilisk)), contract.Manifest.Abi); contract.UpdateCounter++; // Increase update counter + + // Clean whitelist (emit event if exists) + + Policy.CleanWhitelist(engine, contract.Hash); + return OnDeployAsync(engine, contract, data, true); } diff --git a/src/Neo/SmartContract/Native/NativeContract.cs b/src/Neo/SmartContract/Native/NativeContract.cs index dc514468a4..bda8cdc4c6 100644 --- a/src/Neo/SmartContract/Native/NativeContract.cs +++ b/src/Neo/SmartContract/Native/NativeContract.cs @@ -394,6 +394,9 @@ protected static void AssertCommittee(ApplicationEngine engine) [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected StorageKey CreateStorageKey(byte prefix, UInt256 hash, UInt160 signer) => StorageKey.Create(Id, prefix, hash, signer); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected StorageKey CreateStorageKey(byte prefix, UInt160 hash, string methodName, int bigEndianKey) => StorageKey.Create(Id, prefix, hash, methodName, bigEndianKey); + #endregion /// @@ -432,8 +435,13 @@ internal async void Invoke(ApplicationEngine engine, byte version) var state = context.GetState(); if (!state.CallFlags.HasFlag(method.RequiredCallFlags)) throw new InvalidOperationException($"Cannot call this method with the flag {state.CallFlags}."); - // In the unit of datoshi, 1 datoshi = 1e-8 GAS - engine.AddFee(method.CpuFee * engine.ExecFeeFactor + method.StorageFee * engine.StoragePrice); + // Check native-whitelist + if (!engine.IsHardforkEnabled(Hardfork.HF_Faun) || + !Policy.IsWhitelistFeeContract(engine.SnapshotCache, Hash, method.Name, method.Parameters.Length, out var fixedFee)) + { + // In the unit of datoshi, 1 datoshi = 1e-8 GAS + engine.AddFee(method.CpuFee * engine.ExecFeeFactor + method.StorageFee * engine.StoragePrice); + } List parameters = new(); if (method.NeedApplicationEngine) parameters.Add(engine); if (method.NeedSnapshot) parameters.Add(engine.SnapshotCache); diff --git a/src/Neo/SmartContract/Native/PolicyContract.cs b/src/Neo/SmartContract/Native/PolicyContract.cs index 3a1c8fa3a5..750dedd523 100644 --- a/src/Neo/SmartContract/Native/PolicyContract.cs +++ b/src/Neo/SmartContract/Native/PolicyContract.cs @@ -11,10 +11,13 @@ #pragma warning disable IDE0051 +using Neo.Extensions; using Neo.Network.P2P.Payloads; using Neo.Persistence; using Neo.SmartContract.Iterators; using System; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; using System.Numerics; namespace Neo.SmartContract.Native @@ -83,6 +86,7 @@ public sealed class PolicyContract : NativeContract public const uint MaxMaxTraceableBlocks = 2102400; private const byte Prefix_BlockedAccount = 15; + private const byte Prefix_WhitelistedFeeContracts = 16; private const byte Prefix_FeePerByte = 10; private const byte Prefix_ExecFeeFactor = 18; private const byte Prefix_StoragePrice = 19; @@ -103,10 +107,18 @@ public sealed class PolicyContract : NativeContract /// private const string MillisecondsPerBlockChangedEventName = "MillisecondsPerBlockChanged"; + private const string WhitelistChangedEventName = "WhitelistChanged"; + [ContractEvent(Hardfork.HF_Echidna, 0, name: MillisecondsPerBlockChangedEventName, "old", ContractParameterType.Integer, "new", ContractParameterType.Integer )] + [ContractEvent(Hardfork.HF_Faun, 1, name: WhitelistChangedEventName, + "contract", ContractParameterType.Hash160, + "method", ContractParameterType.String, + "argCount", ContractParameterType.Integer, + "fee", ContractParameterType.Any + )] internal PolicyContract() : base() { _feePerByte = CreateStorageKey(Prefix_FeePerByte); @@ -258,6 +270,113 @@ public bool IsBlocked(IReadOnlyStore snapshot, UInt160 account) return snapshot.Contains(CreateStorageKey(Prefix_BlockedAccount, account)); } + internal bool IsWhitelistFeeContract(DataCache snapshot, UInt160 contractHash, string method, int argCount, [NotNullWhen(true)] out long? fixedFee) + { + // Check contract existence + + var currentContract = ContractManagement.GetContract(snapshot, contractHash); + + if (currentContract != null) + { + // Check state existence + + var item = snapshot.TryGet(CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash, method, argCount)); + + if (item != null) + { + fixedFee = (long)(BigInteger)item; + return true; + } + } + + fixedFee = null; + return false; + } + + /// + /// Remove whitelisted Fee contracts + /// + /// The execution engine. + /// The contract to set the whitelist + /// Method + /// Argument count + [ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + private void RemoveWhitelistFeeContract(ApplicationEngine engine, UInt160 contractHash, string method, int argCount) + { + if (!CheckCommittee(engine)) throw new InvalidOperationException("Invalid committee signature"); + + var key = CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash, method, argCount); + + if (!engine.SnapshotCache.Contains(key)) throw new InvalidOperationException("Whitelist not found"); + + engine.SnapshotCache.Delete(key); + + // Emit event + engine.SendNotification(Hash, WhitelistChangedEventName, + [new VM.Types.ByteString(contractHash.ToArray()), new VM.Types.ByteString(method.ToStrictUtf8Bytes()), + new VM.Types.Integer(argCount), VM.Types.StackItem.Null]); + } + + internal int CleanWhitelist(ApplicationEngine engine, UInt160 contractHash) + { + var count = 0; + var searchKey = CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash); + + foreach ((var key, _) in engine.SnapshotCache.Find(searchKey, SeekDirection.Forward)) + { + engine.SnapshotCache.Delete(key); + count++; + + // Emit event recovering the values from the Key + + var keyData = key.ToArray().AsSpan(); + (var method, var argCount) = StorageKey.ReadMethodAndArgCount(key.ToArray().AsSpan()); + + engine.SendNotification(Hash, WhitelistChangedEventName, + [new VM.Types.ByteString(contractHash.ToArray()), new VM.Types.ByteString(method.ToStrictUtf8Bytes()), + new VM.Types.Integer(argCount), VM.Types.StackItem.Null]); + } + + return count; + } + + /// + /// Set whitelisted Fee contracts + /// + /// The execution engine. + /// The contract to set the whitelist + /// Method + /// Argument count + /// Fixed execution fee + [ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + internal void SetWhitelistFeeContract(ApplicationEngine engine, UInt160 contractHash, string method, int argCount, long fixedFee) + { + ArgumentOutOfRangeException.ThrowIfNegative(fixedFee, nameof(fixedFee)); + + if (!CheckCommittee(engine)) throw new InvalidOperationException("Invalid committee signature"); + + var key = CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash, method, argCount); + + // Validate methods + var contract = ContractManagement.GetContract(engine.SnapshotCache, contractHash) + ?? throw new InvalidOperationException("Is not a valid contract"); + + if (contract.Manifest.Abi.GetMethod(method, argCount) is null) + throw new InvalidOperationException($"{method} with {argCount} args is not a valid method of {contractHash}"); + + // Set + var entry = engine.SnapshotCache + .GetAndChange(key, () => new StorageItem(fixedFee)); + + entry.Set(fixedFee); + + // Emit event + + engine.SendNotification(Hash, WhitelistChangedEventName, + [new VM.Types.ByteString(contractHash.ToArray()), new VM.Types.ByteString(method.ToStrictUtf8Bytes()), + new VM.Types.Integer(argCount), new VM.Types.Integer(fixedFee)]); + } + /// /// Sets the block generation time in milliseconds. /// @@ -438,5 +557,16 @@ private StorageIterator GetBlockedAccounts(DataCache snapshot) .GetEnumerator(); return new StorageIterator(enumerator, 1, options); } + + [ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + internal StorageIterator GetWhitelistFeeContracts(DataCache snapshot) + { + const FindOptions options = FindOptions.RemovePrefix | FindOptions.KeysOnly; + var enumerator = snapshot + .Find(CreateStorageKey(Prefix_WhitelistedFeeContracts), SeekDirection.Forward) + .GetEnumerator(); + + return new StorageIterator(enumerator, 1, options); + } } } diff --git a/src/Neo/SmartContract/StorageKey.cs b/src/Neo/SmartContract/StorageKey.cs index 5f870558e3..07bafd23be 100644 --- a/src/Neo/SmartContract/StorageKey.cs +++ b/src/Neo/SmartContract/StorageKey.cs @@ -181,6 +181,37 @@ public static StorageKey Create(int id, byte prefix, UInt256 hash, UInt160 signe return new(id, data); } + /// + /// Create StorageKey + /// + /// The id of the contract. + /// The prefix of the key. + /// Hash + /// Method Name + /// Big Endian key. + /// The class + public static StorageKey Create(int id, byte prefix, UInt160 hash, string methodName, int bigEndian) + { + const int HashAndInt = UInt160Length + sizeof(int); + + var methodData = methodName.ToStrictUtf8Bytes(); + var data = new byte[HashAndInt + methodData.Length]; + + FillHeader(data, id, prefix); + hash.Serialize(data.AsSpan(PrefixLength..)); + BinaryPrimitives.WriteInt32BigEndian(data.AsSpan(UInt160Length..), bigEndian); + Array.Copy(methodData, 0, data, HashAndInt, methodData.Length); + + return new(id, data); + } + + internal static (string methodName, int bigEndian) ReadMethodAndArgCount(ReadOnlySpan keyData) + { + var argCount = BinaryPrimitives.ReadInt32BigEndian(keyData.Slice(UInt160Length, 4)); + var method = keyData[(UInt160Length + 4)..]; + return (method.ToStrictUtf8String(), argCount); + } + /// /// Create StorageKey /// diff --git a/tests/Neo.UnitTests/Ledger/UT_StorageKey.cs b/tests/Neo.UnitTests/Ledger/UT_StorageKey.cs index 03a9bb447b..eb7ef67a68 100644 --- a/tests/Neo.UnitTests/Ledger/UT_StorageKey.cs +++ b/tests/Neo.UnitTests/Ledger/UT_StorageKey.cs @@ -14,6 +14,7 @@ using Neo.Extensions; using Neo.SmartContract; using System; +using System.Text; namespace Neo.UnitTests.Ledger { @@ -76,6 +77,23 @@ public void SameTest() UInt256.Parse("0x761a9bb72ca2a63984db0cc43f943a2a25e464f62d1a91114c2b6fbbfd24b51d"), UInt160.Parse("2d3b96ae1bcc5a585e075e3b81920210dec16302")).ToArray()); + // UInt160+String+Int + key = new KeyBuilder(1, 2); + key.Add(UInt160.Parse("2d3b96ae1bcc5a585e075e3b81920210dec16302")); + key.AddBigEndian((int)3); // arg count + key.Add(Encoding.UTF8.GetBytes("hello world")); + + CollectionAssert.AreEqual(key.ToArray(), StorageKey.Create(1, 2, + UInt160.Parse("2d3b96ae1bcc5a585e075e3b81920210dec16302"), "hello world", 3).ToArray()); + + // Recover method and arg count + + var keyB = new StorageKey(key.ToArray()).ToArray().AsSpan(); + (var method, var argCount) = StorageKey.ReadMethodAndArgCount(keyB); + + Assert.AreEqual(3, argCount); + Assert.AreEqual("hello world", method); + // ISerializable key = new KeyBuilder(1, 2); key.Add(ECCurve.Secp256r1.G); diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs index 03f58a8dbd..0fa96485ca 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs @@ -48,7 +48,7 @@ public void TestSetup() {"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}}"""}, - {"PolicyContract", """{"id":-7,"updatecounter":0,"hash":"0xcc5e4edd9f5f8dba8bb65734541df7a1c081c67b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":2208257578},"manifest":{"name":"PolicyContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"blockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":0,"safe":false},{"name":"getAttributeFee","parameters":[{"name":"attributeType","type":"Integer"}],"returntype":"Integer","offset":7,"safe":true},{"name":"getBlockedAccounts","parameters":[],"returntype":"InteropInterface","offset":14,"safe":true},{"name":"getExecFeeFactor","parameters":[],"returntype":"Integer","offset":21,"safe":true},{"name":"getFeePerByte","parameters":[],"returntype":"Integer","offset":28,"safe":true},{"name":"getMaxTraceableBlocks","parameters":[],"returntype":"Integer","offset":35,"safe":true},{"name":"getMaxValidUntilBlockIncrement","parameters":[],"returntype":"Integer","offset":42,"safe":true},{"name":"getMillisecondsPerBlock","parameters":[],"returntype":"Integer","offset":49,"safe":true},{"name":"getStoragePrice","parameters":[],"returntype":"Integer","offset":56,"safe":true},{"name":"isBlocked","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":63,"safe":true},{"name":"setAttributeFee","parameters":[{"name":"attributeType","type":"Integer"},{"name":"value","type":"Integer"}],"returntype":"Void","offset":70,"safe":false},{"name":"setExecFeeFactor","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":77,"safe":false},{"name":"setFeePerByte","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":84,"safe":false},{"name":"setMaxTraceableBlocks","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"setMaxValidUntilBlockIncrement","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":98,"safe":false},{"name":"setMillisecondsPerBlock","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":105,"safe":false},{"name":"setStoragePrice","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":112,"safe":false},{"name":"unblockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":119,"safe":false}],"events":[{"name":"MillisecondsPerBlockChanged","parameters":[{"name":"old","type":"Integer"},{"name":"new","type":"Integer"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, + {"PolicyContract", """{"id":-7,"updatecounter":0,"hash":"0xcc5e4edd9f5f8dba8bb65734541df7a1c081c67b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":1991619121},"manifest":{"name":"PolicyContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"blockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":0,"safe":false},{"name":"getAttributeFee","parameters":[{"name":"attributeType","type":"Integer"}],"returntype":"Integer","offset":7,"safe":true},{"name":"getBlockedAccounts","parameters":[],"returntype":"InteropInterface","offset":14,"safe":true},{"name":"getExecFeeFactor","parameters":[],"returntype":"Integer","offset":21,"safe":true},{"name":"getFeePerByte","parameters":[],"returntype":"Integer","offset":28,"safe":true},{"name":"getMaxTraceableBlocks","parameters":[],"returntype":"Integer","offset":35,"safe":true},{"name":"getMaxValidUntilBlockIncrement","parameters":[],"returntype":"Integer","offset":42,"safe":true},{"name":"getMillisecondsPerBlock","parameters":[],"returntype":"Integer","offset":49,"safe":true},{"name":"getStoragePrice","parameters":[],"returntype":"Integer","offset":56,"safe":true},{"name":"getWhitelistFeeContracts","parameters":[],"returntype":"InteropInterface","offset":63,"safe":true},{"name":"isBlocked","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":70,"safe":true},{"name":"removeWhitelistFeeContract","parameters":[{"name":"contractHash","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"}],"returntype":"Void","offset":77,"safe":false},{"name":"setAttributeFee","parameters":[{"name":"attributeType","type":"Integer"},{"name":"value","type":"Integer"}],"returntype":"Void","offset":84,"safe":false},{"name":"setExecFeeFactor","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"setFeePerByte","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":98,"safe":false},{"name":"setMaxTraceableBlocks","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":105,"safe":false},{"name":"setMaxValidUntilBlockIncrement","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":112,"safe":false},{"name":"setMillisecondsPerBlock","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":119,"safe":false},{"name":"setStoragePrice","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":126,"safe":false},{"name":"setWhitelistFeeContract","parameters":[{"name":"contractHash","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"},{"name":"fixedFee","type":"Integer"}],"returntype":"Void","offset":133,"safe":false},{"name":"unblockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":140,"safe":false}],"events":[{"name":"MillisecondsPerBlockChanged","parameters":[{"name":"old","type":"Integer"},{"name":"new","type":"Integer"}]},{"name":"WhitelistChanged","parameters":[{"name":"contract","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"},{"name":"fee","type":"Any"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, {"RoleManagement", """{"id":-8,"updatecounter":0,"hash":"0x49cf4e5378ffcd4dec034fd98a174c5491e395e2","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0A=","checksum":983638438},"manifest":{"name":"RoleManagement","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"designateAsRole","parameters":[{"name":"role","type":"Integer"},{"name":"nodes","type":"Array"}],"returntype":"Void","offset":0,"safe":false},{"name":"getDesignatedByRole","parameters":[{"name":"role","type":"Integer"},{"name":"index","type":"Integer"}],"returntype":"Array","offset":7,"safe":true}],"events":[{"name":"Designation","parameters":[{"name":"Role","type":"Integer"},{"name":"BlockIndex","type":"Integer"},{"name":"Old","type":"Array"},{"name":"New","type":"Array"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, {"OracleContract", """{"id":-9,"updatecounter":0,"hash":"0xfe924b7cfe89ddd271abaf7210a80a7e11178758","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2663858513},"manifest":{"name":"OracleContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"finish","parameters":[],"returntype":"Void","offset":0,"safe":false},{"name":"getPrice","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"request","parameters":[{"name":"url","type":"String"},{"name":"filter","type":"String"},{"name":"callback","type":"String"},{"name":"userData","type":"Any"},{"name":"gasForResponse","type":"Integer"}],"returntype":"Void","offset":14,"safe":false},{"name":"setPrice","parameters":[{"name":"price","type":"Integer"}],"returntype":"Void","offset":21,"safe":false},{"name":"verify","parameters":[],"returntype":"Boolean","offset":28,"safe":true}],"events":[{"name":"OracleRequest","parameters":[{"name":"Id","type":"Integer"},{"name":"RequestContract","type":"Hash160"},{"name":"Url","type":"String"},{"name":"Filter","type":"String"}]},{"name":"OracleResponse","parameters":[{"name":"Id","type":"Integer"},{"name":"OriginalTx","type":"Hash256"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, {"Notary", """{"id":-10,"updatecounter":0,"hash":"0xc1e14f19c3e60d0b9244d06dd7ba9b113135ec3b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1110259869},"manifest":{"name":"Notary","groups":[],"features":{},"supportedstandards":["NEP-27"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"expirationOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":7,"safe":true},{"name":"getMaxNotValidBeforeDelta","parameters":[],"returntype":"Integer","offset":14,"safe":true},{"name":"lockDepositUntil","parameters":[{"name":"account","type":"Hash160"},{"name":"till","type":"Integer"}],"returntype":"Boolean","offset":21,"safe":false},{"name":"onNEP17Payment","parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","offset":28,"safe":false},{"name":"setMaxNotValidBeforeDelta","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":35,"safe":false},{"name":"verify","parameters":[{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":42,"safe":true},{"name":"withdraw","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"}],"returntype":"Boolean","offset":49,"safe":false}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs index 07dd41df5c..00b4bcd9ed 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs @@ -10,11 +10,13 @@ // modifications are permitted. using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Cryptography; using Neo.Extensions; using Neo.Network.P2P.Payloads; using Neo.Persistence; using Neo.SmartContract; using Neo.SmartContract.Iterators; +using Neo.SmartContract.Manifest; using Neo.SmartContract.Native; using Neo.UnitTests.Extensions; using Neo.VM; @@ -22,6 +24,7 @@ using System; using System.Linq; using System.Numerics; +using System.Reflection; using Boolean = Neo.VM.Types.Boolean; namespace Neo.UnitTests.SmartContract.Native @@ -626,5 +629,172 @@ public void TestListBlockedAccounts() Assert.IsTrue(iter.Next()); Assert.AreEqual(new UInt160(iter.Value(new ReferenceCounter()).GetSpan()), UInt160.Zero); } + + [TestMethod] + public void TestWhiteListFee() + { + // Create script + + var snapshotCache = _snapshotCache.CloneCache(); + + byte[] script; + using (var sb = new ScriptBuilder()) + { + sb.EmitDynamicCall(NativeContract.NEO.Hash, "balanceOf", NativeContract.NEO.GetCommitteeAddress(_snapshotCache.CloneCache())); + script = sb.ToArray(); + } + + var engine = CreateEngineWithCommitteeSigner(snapshotCache, script); + + // Not whitelisted + + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.AreEqual(0, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(2028330, engine.FeeConsumed); + Assert.AreEqual(0, NativeContract.Policy.CleanWhitelist(engine, NativeContract.NEO.Hash)); + Assert.IsEmpty(engine.Notifications); + + // Whitelist + + engine = CreateEngineWithCommitteeSigner(snapshotCache, script); + + NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, "balanceOf", 1, 0); + engine.SnapshotCache.Commit(); + + // Whitelisted + + Assert.HasCount(1, engine.Notifications); // Whitelist changed + Assert.AreEqual(VMState.HALT, engine.Execute()); + Assert.AreEqual(0, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(1045290, engine.FeeConsumed); + + // Clean white list + + engine.SnapshotCache.Commit(); + engine = CreateEngineWithCommitteeSigner(snapshotCache, script); + + Assert.AreEqual(1, NativeContract.Policy.CleanWhitelist(engine, NativeContract.NEO.Hash)); + Assert.HasCount(1, engine.Notifications); // Whitelist deleted + } + + [TestMethod] + public void TestSetWhiteListFeeContractNegativeFixedFee() + { + var snapshotCache = _snapshotCache.CloneCache(); + var engine = CreateEngineWithCommitteeSigner(snapshotCache); + + // Register a dummy contract + UInt160 contractHash; + using (var sb = new ScriptBuilder()) + { + sb.Emit(OpCode.RET); + var script = sb.ToArray(); + contractHash = script.ToScriptHash(); + snapshotCache.DeleteContract(contractHash); + var manifest = TestUtils.CreateManifest("dummy", ContractParameterType.Any); + manifest.Abi.Methods = [ + new ContractMethodDescriptor + { + Name = "foo", + Parameters = [], + ReturnType = ContractParameterType.Any, + Offset = 0, + Safe = false + } + ]; + + var contract = TestUtils.GetContract(script, manifest); + snapshotCache.AddContract(contractHash, contract); + } + + // Invoke SetWhiteListFeeContract with fixedFee negative + + Assert.Throws(() => NativeContract.Policy.SetWhitelistFeeContract(engine, contractHash, "foo", 1, -1L)); + } + + [TestMethod] + public void TestSetWhiteListFeeContractWhenContractNotFound() + { + var snapshotCache = _snapshotCache.CloneCache(); + var engine = CreateEngineWithCommitteeSigner(snapshotCache); + var randomHash = new UInt160(Crypto.Hash160([1, 2, 3]).ToArray()); + Assert.ThrowsExactly(() => NativeContract.Policy.SetWhitelistFeeContract(engine, randomHash, "transfer", 3, 10)); + } + + [TestMethod] + public void TestSetWhiteListFeeContractWhenContractNotInAbi() + { + var snapshotCache = _snapshotCache.CloneCache(); + var engine = CreateEngineWithCommitteeSigner(snapshotCache); + Assert.ThrowsExactly(() => NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, "noexists", 0, 10)); + } + + [TestMethod] + public void TestSetWhiteListFeeContractWhenArgCountMismatch() + { + var snapshotCache = _snapshotCache.CloneCache(); + var engine = CreateEngineWithCommitteeSigner(snapshotCache); + // transfer exists with 4 args + Assert.ThrowsExactly(() => NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, "transfer", 0, 10)); + } + + [TestMethod] + public void TestSetWhiteListFeeContractWhenNotCommittee() + { + var snapshotCache = _snapshotCache.CloneCache(); + var tx = new Transaction + { + Version = 0, + Nonce = 1, + Signers = [new() { Account = UInt160.Zero, Scopes = WitnessScope.Global }], + Attributes = [], + Witnesses = [new Witness { }], + Script = new byte[1], + NetworkFee = 0, + SystemFee = 0, + ValidUntilBlock = 0 + }; + + using var engine = ApplicationEngine.Create(TriggerType.Application, tx, snapshotCache, settings: TestProtocolSettings.Default); + Assert.ThrowsExactly(() => NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, "transfer", 4, 10)); + } + + [TestMethod] + public void TestSetWhiteListFeeContractSetContract() + { + var snapshotCache = _snapshotCache.CloneCache(); + var engine = CreateEngineWithCommitteeSigner(snapshotCache); + NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, "transfer", 4, 123_456); + + Assert.IsTrue(NativeContract.Policy.IsWhitelistFeeContract(engine.SnapshotCache, NativeContract.NEO.Hash, "transfer", 4, out var fixedFee)); + Assert.AreEqual(123_456, fixedFee); + } + + private static ApplicationEngine CreateEngineWithCommitteeSigner(DataCache snapshotCache, byte[] script = null) + { + // Get committe public keys and calculate m + var committee = NativeContract.NEO.GetCommittee(snapshotCache); + var m = (committee.Length / 2) + 1; + var committeeContract = Contract.CreateMultiSigContract(m, committee); + + // Create Tx needed for CheckWitness / CheckCommittee + var tx = new Transaction + { + Version = 0, + Nonce = 1, + Signers = [new() { Account = committeeContract.ScriptHash, Scopes = WitnessScope.Global }], + Attributes = [], + Witnesses = [new Witness { InvocationScript = new byte[1], VerificationScript = committeeContract.Script }], + Script = script ?? [(byte)OpCode.NOP], + NetworkFee = 0, + SystemFee = 0, + ValidUntilBlock = 0 + }; + + var engine = ApplicationEngine.Create(TriggerType.Application, tx, snapshotCache, settings: TestProtocolSettings.Default); + engine.LoadScript(tx.Script); + + return engine; + } } }