diff --git a/docs/native-contracts-api.md b/docs/native-contracts-api.md new file mode 100644 index 0000000000..470d92a730 --- /dev/null +++ b/docs/native-contracts-api.md @@ -0,0 +1,209 @@ +# Native Contracts API +Native contracts are the contracts that are implemented in the Neo blockchain, +and native contract APIsare the methods that are provided by the native contracts. + +When calling a native contract method by transaction script, there are several tips and notes: +1. A part of native contract methods require CallFlags. If no such CallFlags is provided, the call will be failed. +2. Some native contract methods are only allowed to be called before or after a certain hardfork. +3. A native contract method may have different behaviors in different hardforks. + +## Table of Contents + +1. [ContractManagement](#contractmanagement) +2. [StdLib](#stdlib) +3. [CryptoLib](#cryptolib) +4. [LedgerContract](#ledgercontract) +5. [NeoToken](#neotoken) +6. [GasToken](#gastoken) +7. [PolicyContract](#policycontract) +8. [RoleManagement](#rolemanagement) +9. [OracleContract](#oraclecontract) +10. [Notary](#notary) + +## ContractManagement + +| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork | +|--------|---------|------------|--------------|---------|-------------|------------|----------| +| getMinimumDeploymentFee | Gets the minimum deployment fee for deploying a contract. | -- | Int64 | 1<<15 | 0 | ReadStates | -- | +| setMinimumDeploymentFee | Sets the minimum deployment fee for deploying a contract. Only committee members can call this method. | BigInteger(*value*) | Void | 1<<15 | 0 | States | -- | +| getContract | Gets the deployed contract with the specified hash. | UInt160(*hash*) | ContractState | 1<<15 | 0 | ReadStates | -- | +| isContract | Check if exists the deployed contract with the specified hash. | UInt160(*hash*) | Boolean | 1<<14 | 0 | ReadStates | HF_Echidna | +| getContractById | Maps specified ID to deployed contract. | Int32(*id*) | ContractState | 1<<15 | 0 | ReadStates | -- | +| getContractHashes | Gets hashes of all non native deployed contracts. | -- | IIterator | 1<<15 | 0 | ReadStates | -- | +| hasMethod | Check if a method exists in a contract. | UInt160(*hash*), String(*method*), Int32(*pcount*) | Boolean | 1<<15 | 0 | ReadStates | -- | +| deploy | Deploys a contract. It needs to pay the deployment fee and storage fee. | Byte[](*nefFile*), Byte[](*manifest*) | ContractState | 0 | 0 | States,AllowNotify | -- | +| deploy | Deploys a contract. It needs to pay the deployment fee and storage fee. | Byte[](*nefFile*), Byte[](*manifest*), StackItem(*data*) | ContractState | 0 | 0 | States,AllowNotify | -- | +| update | Updates a contract. It needs to pay the storage fee. | Byte[](*nefFile*), Byte[](*manifest*) | Void | 0 | 0 | States,AllowNotify | -- | +| update | Updates a contract. It needs to pay the storage fee. | Byte[](*nefFile*), Byte[](*manifest*), StackItem(*data*) | Void | 0 | 0 | States,AllowNotify | -- | +| destroy | Destroys a contract. | -- | Void | 1<<15 | 0 | States,AllowNotify | -- | + + +## StdLib + +| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork | +|--------|---------|------------|--------------|---------|-------------|------------|----------| +| serialize | -- | StackItem(*item*) | Byte[] | 1<<12 | 0 | -- | -- | +| deserialize | -- | Byte[](*data*) | StackItem | 1<<14 | 0 | -- | -- | +| jsonSerialize | -- | StackItem(*item*) | Byte[] | 1<<12 | 0 | -- | -- | +| jsonDeserialize | -- | Byte[](*json*) | StackItem | 1<<14 | 0 | -- | -- | +| itoa | Converts an integer to a String. | BigInteger(*value*) | String | 1<<12 | 0 | -- | -- | +| itoa | Converts an integer to a String. | BigInteger(*value*), Int32(*base*) | String | 1<<12 | 0 | -- | -- | +| atoi | Converts a String to an integer. | String(*value*) | BigInteger | 1<<6 | 0 | -- | -- | +| atoi | Converts a String to an integer. | String(*value*), Int32(*base*) | BigInteger | 1<<6 | 0 | -- | -- | +| base64Encode | Encodes a byte array into a base64 String. | Byte[](*data*) | String | 1<<5 | 0 | -- | -- | +| base64Decode | Decodes a byte array from a base64 String. | String(*s*) | Byte[] | 1<<5 | 0 | -- | -- | +| base64UrlEncode | Encodes a byte array into a base64Url string. | String(*data*) | String | 1<<5 | 0 | -- | HF_Echidna | +| base64UrlDecode | Decodes a byte array from a base64Url string. | String(*s*) | String | 1<<5 | 0 | -- | HF_Echidna | +| base58Encode | Encodes a byte array into a base58 String. | Byte[](*data*) | String | 1<<13 | 0 | -- | -- | +| base58Decode | Decodes a byte array from a base58 String. | String(*s*) | Byte[] | 1<<10 | 0 | -- | -- | +| base58CheckEncode | Converts a byte array to its equivalent String representation that is encoded with base-58 digits. The encoded String contains the checksum of the binary data. | Byte[](*data*) | String | 1<<16 | 0 | -- | -- | +| base58CheckDecode | Converts the specified String, which encodes binary data as base-58 digits, to an equivalent byte array. The encoded String contains the checksum of the binary data. | String(*s*) | Byte[] | 1<<16 | 0 | -- | -- | +| hexEncode | -- | Byte[](*bytes*) | String | 1<<5 | 0 | -- | HF_Faun | +| hexDecode | -- | String(*str*) | Byte[] | 1<<5 | 0 | -- | HF_Faun | +| memoryCompare | -- | Byte[](*str1*), Byte[](*str2*) | Int32 | 1<<5 | 0 | -- | -- | +| memorySearch | -- | Byte[](*mem*), Byte[](*value*) | Int32 | 1<<6 | 0 | -- | -- | +| memorySearch | -- | Byte[](*mem*), Byte[](*value*), Int32(*start*) | Int32 | 1<<6 | 0 | -- | -- | +| memorySearch | -- | Byte[](*mem*), Byte[](*value*), Int32(*start*), Boolean(*backward*) | Int32 | 1<<6 | 0 | -- | -- | +| stringSplit | -- | String(*str*), String(*separator*) | String[] | 1<<8 | 0 | -- | -- | +| stringSplit | -- | String(*str*), String(*separator*), Boolean(*removeEmptyEntries*) | String[] | 1<<8 | 0 | -- | -- | +| strLen | -- | String(*str*) | Int32 | 1<<8 | 0 | -- | -- | + + +## CryptoLib + +| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork | +|--------|---------|------------|--------------|---------|-------------|------------|----------| +| bls12381Serialize | Serialize a bls12381 point. | InteropInterface(*g*) | Byte[] | 1<<19 | 0 | -- | -- | +| bls12381Deserialize | Deserialize a bls12381 point. | Byte[](*data*) | InteropInterface | 1<<19 | 0 | -- | -- | +| bls12381Equal | Determines whether the specified points are equal. | InteropInterface(*x*), InteropInterface(*y*) | Boolean | 1<<5 | 0 | -- | -- | +| bls12381Add | Add operation of two points. | InteropInterface(*x*), InteropInterface(*y*) | InteropInterface | 1<<19 | 0 | -- | -- | +| bls12381Mul | Mul operation of gt point and multiplier | InteropInterface(*x*), Byte[](*mul*), Boolean(*neg*) | InteropInterface | 1<<21 | 0 | -- | -- | +| bls12381Pairing | Pairing operation of g1 and g2 | InteropInterface(*g1*), InteropInterface(*g2*) | InteropInterface | 1<<23 | 0 | -- | -- | +| recoverSecp256K1 | Recovers the public key from a secp256k1 signature in a single byte array format. | Byte[](*messageHash*), Byte[](*signature*) | Byte[] | 1<<15 | 0 | -- | HF_Echidna | +| ripemd160 | Computes the hash value for the specified byte array using the ripemd160 algorithm. | Byte[](*data*) | Byte[] | 1<<15 | 0 | -- | -- | +| sha256 | Computes the hash value for the specified byte array using the sha256 algorithm. | Byte[](*data*) | Byte[] | 1<<15 | 0 | -- | -- | +| murmur32 | Computes the hash value for the specified byte array using the murmur32 algorithm. | Byte[](*data*), UInt32(*seed*) | Byte[] | 1<<13 | 0 | -- | -- | +| keccak256 | Computes the hash value for the specified byte array using the keccak256 algorithm. | Byte[](*data*) | Byte[] | 1<<15 | 0 | -- | HF_Cockatrice | +| verifyWithECDsa | Verifies that a digital signature is appropriate for the provided key and message using the ECDSA algorithm. | Byte[](*message*), Byte[](*pubkey*), Byte[](*signature*), NamedCurveHash(*curveHash*) | Boolean | 1<<15 | 0 | -- | HF_Cockatrice | +| verifyWithECDsa | -- | Byte[](*message*), Byte[](*pubkey*), Byte[](*signature*), NamedCurveHash(*curve*) | Boolean | 1<<15 | 0 | -- | Deprecated in HF_Cockatrice | +| verifyWithEd25519 | Verifies that a digital signature is appropriate for the provided key and message using the Ed25519 algorithm. | Byte[](*message*), Byte[](*pubkey*), Byte[](*signature*) | Boolean | 1<<15 | 0 | -- | HF_Echidna | + + +## LedgerContract + +| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork | +|--------|---------|------------|--------------|---------|-------------|------------|----------| +| currentHash | Gets the hash of the current block. | -- | UInt256 | 1<<15 | 0 | ReadStates | -- | +| currentIndex | Gets the index of the current block. | -- | UInt32 | 1<<15 | 0 | ReadStates | -- | +| getBlock | -- | Byte[](*indexOrHash*) | TrimmedBlock | 1<<15 | 0 | ReadStates | -- | +| getTransaction | -- | UInt256(*hash*) | Transaction | 1<<15 | 0 | ReadStates | -- | +| getTransactionSigners | -- | UInt256(*hash*) | Signer[] | 1<<15 | 0 | ReadStates | -- | +| getTransactionVMState | -- | UInt256(*hash*) | VMState | 1<<15 | 0 | ReadStates | -- | +| getTransactionHeight | -- | UInt256(*hash*) | Int32 | 1<<15 | 0 | ReadStates | -- | +| getTransactionFromBlock | -- | Byte[](*blockIndexOrHash*), Int32(*txIndex*) | Transaction | 1<<16 | 0 | ReadStates | -- | + + +## NeoToken + +| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork | +|--------|---------|------------|--------------|---------|-------------|------------|----------| +| totalSupply | -- | -- | BigInteger | 1<<15 | 0 | ReadStates | -- | +| setGasPerBlock | Sets the amount of GAS generated in each block. Only committee members can call this method. | BigInteger(*gasPerBlock*) | Void | 1<<15 | 0 | States | -- | +| getGasPerBlock | Gets the amount of GAS generated in each block. | -- | BigInteger | 1<<15 | 0 | ReadStates | -- | +| setRegisterPrice | Sets the fees to be paid to register as a candidate. Only committee members can call this method. | Int64(*registerPrice*) | Void | 1<<15 | 0 | States | -- | +| getRegisterPrice | Gets the fees to be paid to register as a candidate. | -- | Int64 | 1<<15 | 0 | ReadStates | -- | +| unclaimedGas | Get the amount of unclaimed GAS in the specified account. | UInt160(*account*), UInt32(*end*) | BigInteger | 1<<17 | 0 | ReadStates | -- | +| onNEP17Payment | Handles the payment of GAS. | UInt160(*from*), BigInteger(*amount*), StackItem(*data*) | Void | 0 | 0 | States,AllowNotify | HF_Echidna | +| registerCandidate | Registers a candidate. | ECPoint(*pubkey*) | Boolean | 0 | 0 | States | Deprecated in HF_Echidna | +| registerCandidate | Registers a candidate. | ECPoint(*pubkey*) | Boolean | 0 | 0 | States,AllowNotify | HF_Echidna | +| unregisterCandidate | Unregisters a candidate. | ECPoint(*pubkey*) | Boolean | 1<<16 | 0 | States | Deprecated in HF_Echidna | +| unregisterCandidate | Unregisters a candidate. | ECPoint(*pubkey*) | Boolean | 1<<16 | 0 | States,AllowNotify | HF_Echidna | +| vote | Votes for a candidate. | UInt160(*account*), ECPoint(*voteTo*) | Boolean | 1<<16 | 0 | States | Deprecated in HF_Echidna | +| vote | Votes for a candidate. | UInt160(*account*), ECPoint(*voteTo*) | Boolean | 1<<16 | 0 | States,AllowNotify | HF_Echidna | +| getCandidates | Gets the first 256 registered candidates. | -- | ValueTuple`2[] | 1<<22 | 0 | ReadStates | -- | +| getAllCandidates | Gets the registered candidates iterator. | -- | IIterator | 1<<22 | 0 | ReadStates | -- | +| getCandidateVote | Gets votes from specific candidate. | ECPoint(*pubKey*) | BigInteger | 1<<15 | 0 | ReadStates | -- | +| getCommittee | Gets all the members of the committee. | -- | ECPoint[] | 1<<16 | 0 | ReadStates | -- | +| getAccountState | Get account state. | UInt160(*account*) | NeoAccountState | 1<<15 | 0 | ReadStates | -- | +| getCommitteeAddress | Gets the address of the committee. | -- | UInt160 | 1<<16 | 0 | ReadStates | HF_Cockatrice | +| getNextBlockValidators | Gets the validators of the next block. | -- | ECPoint[] | 1<<16 | 0 | ReadStates | -- | +| balanceOf | Gets the balance of the specified account. | UInt160(*account*) | BigInteger | 1<<15 | 0 | ReadStates | -- | +| transfer | -- | UInt160(*from*), UInt160(*to*), BigInteger(*amount*), StackItem(*data*) | Boolean | 1<<17 | 50 | All | -- | +| symbol | -- | -- | String | 0 | 0 | -- | -- | +| decimals | -- | -- | Byte | 0 | 0 | -- | -- | + + +## GasToken + +| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork | +|--------|---------|------------|--------------|---------|-------------|------------|----------| +| totalSupply | Gets the total supply of the token. | -- | BigInteger | 1<<15 | 0 | ReadStates | -- | +| balanceOf | Gets the balance of the specified account. | UInt160(*account*) | BigInteger | 1<<15 | 0 | ReadStates | -- | +| transfer | -- | UInt160(*from*), UInt160(*to*), BigInteger(*amount*), StackItem(*data*) | Boolean | 1<<17 | 50 | All | -- | +| symbol | -- | -- | String | 0 | 0 | -- | -- | +| decimals | -- | -- | Byte | 0 | 0 | -- | -- | + + +## PolicyContract + +| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork | +|--------|---------|------------|--------------|---------|-------------|------------|----------| +| getFeePerByte | Gets the network fee per transaction byte. | -- | Int64 | 1<<15 | 0 | ReadStates | -- | +| getExecFeeFactor | Gets the execution fee factor. This is a multiplier that can be adjusted by the committee to adjust the system fees for transactions. | -- | UInt32 | 1<<15 | 0 | ReadStates | -- | +| getExecPicoFeeFactor | Gets the execution fee factor. This is a multiplier that can be adjusted by the committee to adjust the system fees for transactions. | -- | BigInteger | 1<<15 | 0 | ReadStates | HF_Faun | +| getStoragePrice | Gets the storage price. | -- | UInt32 | 1<<15 | 0 | ReadStates | -- | +| getMillisecondsPerBlock | Gets the block generation time in milliseconds. | -- | UInt32 | 1<<15 | 0 | ReadStates | HF_Echidna | +| getMaxValidUntilBlockIncrement | Gets the upper increment size of blockchain height (in blocks) exceeding that a transaction should fail validation. | -- | UInt32 | 1<<15 | 0 | ReadStates | HF_Echidna | +| getMaxTraceableBlocks | Gets the length of the chain accessible to smart contracts. | -- | UInt32 | 1<<15 | 0 | ReadStates | HF_Echidna | +| getAttributeFee | Gets the fee for attribute before Echidna hardfork. NotaryAssisted attribute type not supported. | Byte(*attributeType*) | UInt32 | 1<<15 | 0 | ReadStates | Deprecated in HF_Echidna | +| getAttributeFee | Gets the fee for attribute after Echidna hardfork. NotaryAssisted attribute type supported. | Byte(*attributeType*) | UInt32 | 1<<15 | 0 | ReadStates | HF_Echidna | +| isBlocked | Determines whether the specified account is blocked. | UInt160(*account*) | Boolean | 1<<15 | 0 | ReadStates | -- | +| removeWhitelistFeeContract | Remove whitelisted Fee contracts | UInt160(*contractHash*), String(*method*), Int32(*argCount*) | Void | 1<<15 | 0 | States,AllowNotify | HF_Faun | +| setWhitelistFeeContract | Set whitelisted Fee contracts | UInt160(*contractHash*), String(*method*), Int32(*argCount*), Int64(*fixedFee*) | Void | 1<<15 | 0 | States,AllowNotify | HF_Faun | +| setMillisecondsPerBlock | Sets the block generation time in milliseconds. | UInt32(*value*) | Void | 1<<15 | 0 | States,AllowNotify | HF_Echidna | +| setAttributeFee | Sets the fee for attribute before Echidna hardfork. NotaryAssisted attribute type not supported. | Byte(*attributeType*), UInt32(*value*) | Void | 1<<15 | 0 | States | Deprecated in HF_Echidna | +| setAttributeFee | Sets the fee for attribute after Echidna hardfork. NotaryAssisted attribute type supported. | Byte(*attributeType*), UInt32(*value*) | Void | 1<<15 | 0 | States | HF_Echidna | +| setFeePerByte | -- | Int64(*value*) | Void | 1<<15 | 0 | States | -- | +| setExecFeeFactor | -- | UInt64(*value*) | Void | 1<<15 | 0 | States | -- | +| setStoragePrice | -- | UInt32(*value*) | Void | 1<<15 | 0 | States | -- | +| setMaxValidUntilBlockIncrement | -- | UInt32(*value*) | Void | 1<<15 | 0 | States | HF_Echidna | +| setMaxTraceableBlocks | Sets the length of the chain accessible to smart contracts. | UInt32(*value*) | Void | 1<<15 | 0 | States | HF_Echidna | +| blockAccount | -- | UInt160(*account*) | Boolean | 1<<15 | 0 | States | -- | +| unblockAccount | -- | UInt160(*account*) | Boolean | 1<<15 | 0 | States | -- | +| getBlockedAccounts | -- | -- | StorageIterator | 1<<15 | 0 | ReadStates | HF_Faun | +| getWhitelistFeeContracts | -- | -- | StorageIterator | 1<<15 | 0 | ReadStates | HF_Faun | + + +## RoleManagement + +| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork | +|--------|---------|------------|--------------|---------|-------------|------------|----------| +| getDesignatedByRole | Gets the list of nodes for the specified role. | Role(*role*), UInt32(*index*) | ECPoint[] | 1<<15 | 0 | ReadStates | -- | +| designateAsRole | -- | Role(*role*), ECPoint[](*nodes*) | Void | 1<<15 | 0 | States,AllowNotify | -- | + + +## OracleContract + +| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork | +|--------|---------|------------|--------------|---------|-------------|------------|----------| +| setPrice | Sets the price for an Oracle request. Only committee members can call this method. | Int64(*price*) | Void | 1<<15 | 0 | States | -- | +| getPrice | Gets the price for an Oracle request. | -- | Int64 | 1<<15 | 0 | ReadStates | -- | +| finish | Finishes an Oracle response. | -- | Void | 0 | 0 | All | -- | +| request | -- | String(*url*), String(*filter*), String(*callback*), StackItem(*userData*), Int64(*gasForResponse*) | Void | 0 | 0 | States,AllowNotify | -- | +| verify | -- | -- | Boolean | 1<<15 | 0 | -- | -- | + + +## Notary + +| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork | +|--------|---------|------------|--------------|---------|-------------|------------|----------| +| verify | Verify checks whether the transaction is signed by one of the notaries and ensures whether deposited amount of GAS is enough to pay the actual sender's fee. | Byte[](*signature*) | Boolean | 1<<15 | 0 | ReadStates | -- | +| onNEP17Payment | OnNEP17Payment is a callback that accepts GAS transfer as Notary deposit. It also sets the deposit's lock height after which deposit can be withdrawn. | UInt160(*from*), BigInteger(*amount*), StackItem(*data*) | Void | 1<<15 | 0 | States | -- | +| lockDepositUntil | Lock asset until the specified height is unlocked. | UInt160(*account*), UInt32(*till*) | Boolean | 1<<15 | 0 | States | -- | +| expirationOf | ExpirationOf returns deposit lock height for specified address. | UInt160(*account*) | UInt32 | 1<<15 | 0 | ReadStates | -- | +| balanceOf | BalanceOf returns deposited GAS amount for specified address. | UInt160(*account*) | BigInteger | 1<<15 | 0 | ReadStates | -- | +| withdraw | Withdraw sends all deposited GAS for "from" address to "to" address. If "to" address is not specified, then "from" will be used as a sender. | UInt160(*from*), UInt160(*to*) | Boolean | 1<<15 | 0 | All | -- | +| getMaxNotValidBeforeDelta | GetMaxNotValidBeforeDelta is Notary contract method and returns the maximum NotValidBefore delta. | -- | UInt32 | 1<<15 | 0 | ReadStates | -- | +| setMaxNotValidBeforeDelta | SetMaxNotValidBeforeDelta is Notary contract method and sets the maximum NotValidBefore delta. | UInt32(*value*) | Void | 1<<15 | 0 | States | -- | + + diff --git a/src/Neo/SmartContract/ApplicationEngine.cs b/src/Neo/SmartContract/ApplicationEngine.cs index 2d66654fee..12df4dc6e8 100644 --- a/src/Neo/SmartContract/ApplicationEngine.cs +++ b/src/Neo/SmartContract/ApplicationEngine.cs @@ -328,6 +328,14 @@ protected static void OnSysCall(ExecutionEngine engine, Instruction instruction) /// The amount of GAS, in the unit of picoGAS, 1 picoGAS = 1e-12 GAS, to be added. protected internal void AddFee(BigInteger picoGas) { + // Check whitelist + + if (CurrentContext?.GetState()?.WhiteListed == true) + { + // The execution is whitelisted + return; + } + _feeConsumed = _feeConsumed + picoGas; if (_feeConsumed > _feeAmount) throw new InvalidOperationException("Insufficient GAS."); @@ -368,12 +376,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 * ApplicationEngine.FeeFactor); + 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 f4c2ccd03f..a8b9430a0b 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 7453ceb947..cf78d2c083 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,10 +435,15 @@ 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 picoGAS, 1 picoGAS = 1e-12 GAS - engine.AddFee( - (method.CpuFee * engine.ExecFeePicoFactor) + - (method.StorageFee * engine.StoragePrice * ApplicationEngine.FeeFactor)); + // 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 picoGAS, 1 picoGAS = 1e-12 GAS + engine.AddFee( + (method.CpuFee * engine.ExecFeePicoFactor) + + (method.StorageFee * engine.StoragePrice * ApplicationEngine.FeeFactor)); + } 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 11f5c3e7de..4d4a38b22b 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 = "WhitelistFeeChanged"; + [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); @@ -290,6 +302,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. /// @@ -473,5 +592,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 a811505502..c6d36a04e6 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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":65467259},"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":"getExecPicoFeeFactor","parameters":[],"returntype":"Integer","offset":28,"safe":true},{"name":"getFeePerByte","parameters":[],"returntype":"Integer","offset":35,"safe":true},{"name":"getMaxTraceableBlocks","parameters":[],"returntype":"Integer","offset":42,"safe":true},{"name":"getMaxValidUntilBlockIncrement","parameters":[],"returntype":"Integer","offset":49,"safe":true},{"name":"getMillisecondsPerBlock","parameters":[],"returntype":"Integer","offset":56,"safe":true},{"name":"getStoragePrice","parameters":[],"returntype":"Integer","offset":63,"safe":true},{"name":"isBlocked","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":70,"safe":true},{"name":"setAttributeFee","parameters":[{"name":"attributeType","type":"Integer"},{"name":"value","type":"Integer"}],"returntype":"Void","offset":77,"safe":false},{"name":"setExecFeeFactor","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":84,"safe":false},{"name":"setFeePerByte","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"setMaxTraceableBlocks","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":98,"safe":false},{"name":"setMaxValidUntilBlockIncrement","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":105,"safe":false},{"name":"setMillisecondsPerBlock","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":112,"safe":false},{"name":"setStoragePrice","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":119,"safe":false},{"name":"unblockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":126,"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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":751055395},"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":"getExecPicoFeeFactor","parameters":[],"returntype":"Integer","offset":28,"safe":true},{"name":"getFeePerByte","parameters":[],"returntype":"Integer","offset":35,"safe":true},{"name":"getMaxTraceableBlocks","parameters":[],"returntype":"Integer","offset":42,"safe":true},{"name":"getMaxValidUntilBlockIncrement","parameters":[],"returntype":"Integer","offset":49,"safe":true},{"name":"getMillisecondsPerBlock","parameters":[],"returntype":"Integer","offset":56,"safe":true},{"name":"getStoragePrice","parameters":[],"returntype":"Integer","offset":63,"safe":true},{"name":"getWhitelistFeeContracts","parameters":[],"returntype":"InteropInterface","offset":70,"safe":true},{"name":"isBlocked","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":77,"safe":true},{"name":"removeWhitelistFeeContract","parameters":[{"name":"contractHash","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"}],"returntype":"Void","offset":84,"safe":false},{"name":"setAttributeFee","parameters":[{"name":"attributeType","type":"Integer"},{"name":"value","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"setExecFeeFactor","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":98,"safe":false},{"name":"setFeePerByte","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":105,"safe":false},{"name":"setMaxTraceableBlocks","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":112,"safe":false},{"name":"setMaxValidUntilBlockIncrement","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":119,"safe":false},{"name":"setMillisecondsPerBlock","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":126,"safe":false},{"name":"setStoragePrice","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":133,"safe":false},{"name":"setWhitelistFeeContract","parameters":[{"name":"contractHash","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"},{"name":"fixedFee","type":"Integer"}],"returntype":"Void","offset":140,"safe":false},{"name":"unblockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":147,"safe":false}],"events":[{"name":"MillisecondsPerBlockChanged","parameters":[{"name":"old","type":"Integer"},{"name":"new","type":"Integer"}]},{"name":"WhitelistFeeChanged","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 ba0471e1a1..d9f234fd03 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; + } } }