Skip to content

Commit

Permalink
Decouple calculate fee from Wallet (#3147)
Browse files Browse the repository at this point in the history
* Decouple calculate fee from Wallet

* clean iff

* Convert into extension

* Update src/Neo/Wallets/Helper.cs
  • Loading branch information
shargon committed Feb 23, 2024
1 parent ffa9064 commit d8af2c6
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 88 deletions.
90 changes: 90 additions & 0 deletions src/Neo/Wallets/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
using Neo.IO;
using Neo.Network.P2P;
using Neo.Network.P2P.Payloads;
using Neo.Persistence;
using Neo.SmartContract;
using Neo.SmartContract.Native;
using Neo.VM;
using System;
using static Neo.SmartContract.Helper;

namespace Neo.Wallets
{
Expand Down Expand Up @@ -72,5 +77,90 @@ internal static byte[] XOR(byte[] x, byte[] y)
r[i] = (byte)(x[i] ^ y[i]);
return r;
}

/// <summary>
/// Calculates the network fee for the specified transaction.
/// </summary>
/// <param name="tx">The transaction to calculate.</param>
/// <param name="snapshot">The snapshot used to read data.</param>
/// <param name="settings">Thr protocol settings to use.</param>
/// <param name="accountScript">Function to retrive the script's account from a hash.</param>
/// <param name="maxExecutionCost">The maximum cost that can be spent when a contract is executed.</param>
/// <returns>The network fee of the transaction.</returns>
public static long CalculateNetworkFee(this Transaction tx, DataCache snapshot, ProtocolSettings settings, Func<UInt160, byte[]> accountScript, long maxExecutionCost = ApplicationEngine.TestModeGas)
{
UInt160[] hashes = tx.GetScriptHashesForVerifying(snapshot);

// base size for transaction: includes const_header + signers + attributes + script + hashes
int size = Transaction.HeaderSize + tx.Signers.GetVarSize() + tx.Attributes.GetVarSize() + tx.Script.GetVarSize() + IO.Helper.GetVarSize(hashes.Length), index = -1;
uint exec_fee_factor = NativeContract.Policy.GetExecFeeFactor(snapshot);
long networkFee = 0;
foreach (UInt160 hash in hashes)
{
index++;
byte[] witnessScript = accountScript(hash);
byte[] invocationScript = null;

if (tx.Witnesses != null && witnessScript is null)
{
// Try to find the script in the witnesses
Witness witness = tx.Witnesses[index];
witnessScript = witness?.VerificationScript.ToArray();

if (witnessScript is null || witnessScript.Length == 0)
{
// Then it's a contract-based witness, so try to get the corresponding invocation script for it
invocationScript = witness?.InvocationScript.ToArray();
}
}

if (witnessScript is null || witnessScript.Length == 0)
{
var contract = NativeContract.ContractManagement.GetContract(snapshot, hash);
if (contract is null)
throw new ArgumentException($"The smart contract or address {hash} is not found");
var md = contract.Manifest.Abi.GetMethod("verify", -1);
if (md is null)
throw new ArgumentException($"The smart contract {contract.Hash} haven't got verify method");
if (md.ReturnType != ContractParameterType.Boolean)
throw new ArgumentException("The verify method doesn't return boolean value.");
if (md.Parameters.Length > 0 && invocationScript is null)
throw new ArgumentException("The verify method requires parameters that need to be passed via the witness' invocation script.");

// Empty verification and non-empty invocation scripts
var invSize = invocationScript?.GetVarSize() ?? Array.Empty<byte>().GetVarSize();
size += Array.Empty<byte>().GetVarSize() + invSize;

// Check verify cost
using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshot.CreateSnapshot(), settings: settings, gas: maxExecutionCost);
engine.LoadContract(contract, md, CallFlags.ReadOnly);
if (invocationScript != null) engine.LoadScript(invocationScript, configureState: p => p.CallFlags = CallFlags.None);
if (engine.Execute() == VMState.FAULT) throw new ArgumentException($"Smart contract {contract.Hash} verification fault.");
if (!engine.ResultStack.Pop().GetBoolean()) throw new ArgumentException($"Smart contract {contract.Hash} returns false.");

maxExecutionCost -= engine.GasConsumed;
if (maxExecutionCost <= 0) throw new InvalidOperationException("Insufficient GAS.");
networkFee += engine.GasConsumed;
}
else if (IsSignatureContract(witnessScript))
{
size += 67 + witnessScript.GetVarSize();
networkFee += exec_fee_factor * SignatureContractCost();
}
else if (IsMultiSigContract(witnessScript, out int m, out int n))
{
int size_inv = 66 * m;
size += IO.Helper.GetVarSize(size_inv) + size_inv + witnessScript.GetVarSize();
networkFee += exec_fee_factor * MultiSignatureContractCost(m, n);
}
// We can support more contract types in the future.
}
networkFee += size * NativeContract.Policy.GetFeePerByte(snapshot);
foreach (TransactionAttribute attr in tx.Attributes)
{
networkFee += attr.CalculateNetworkFee(snapshot, tx);
}
return networkFee;
}
}
}
89 changes: 1 addition & 88 deletions src/Neo/Wallets/Wallet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -578,99 +578,12 @@ private Transaction MakeTransaction(DataCache snapshot, ReadOnlyMemory<byte> scr
tx.SystemFee = engine.GasConsumed;
}

tx.NetworkFee = CalculateNetworkFee(snapshot, tx, maxGas);
tx.NetworkFee = tx.CalculateNetworkFee(snapshot, ProtocolSettings, (a) => GetAccount(a)?.Contract?.Script, maxGas);
if (value >= tx.SystemFee + tx.NetworkFee) return tx;
}
throw new InvalidOperationException("Insufficient GAS");
}

/// <summary>
/// Calculates the network fee for the specified transaction.
/// </summary>
/// <param name="snapshot">The snapshot used to read data.</param>
/// <param name="tx">The transaction to calculate.</param>
/// <param name="maxExecutionCost">The maximum cost that can be spent when a contract is executed.</param>
/// <returns>The network fee of the transaction.</returns>
public long CalculateNetworkFee(DataCache snapshot, Transaction tx, long maxExecutionCost = ApplicationEngine.TestModeGas)
{
UInt160[] hashes = tx.GetScriptHashesForVerifying(snapshot);

// base size for transaction: includes const_header + signers + attributes + script + hashes
int size = Transaction.HeaderSize + tx.Signers.GetVarSize() + tx.Attributes.GetVarSize() + tx.Script.GetVarSize() + IO.Helper.GetVarSize(hashes.Length);
uint exec_fee_factor = NativeContract.Policy.GetExecFeeFactor(snapshot);
long networkFee = 0;
int index = -1;
foreach (UInt160 hash in hashes)
{
index++;
byte[] witness_script = GetAccount(hash)?.Contract?.Script;
byte[] invocationScript = null;

if (tx.Witnesses != null)
{
if (witness_script is null)
{
// Try to find the script in the witnesses
Witness witness = tx.Witnesses[index];
witness_script = witness?.VerificationScript.ToArray();

if (witness_script is null || witness_script.Length == 0)
{
// Then it's a contract-based witness, so try to get the corresponding invocation script for it
invocationScript = witness?.InvocationScript.ToArray();
}
}
}

if (witness_script is null || witness_script.Length == 0)
{
var contract = NativeContract.ContractManagement.GetContract(snapshot, hash);
if (contract is null)
throw new ArgumentException($"The smart contract or address {hash} is not found");
var md = contract.Manifest.Abi.GetMethod("verify", -1);
if (md is null)
throw new ArgumentException($"The smart contract {contract.Hash} haven't got verify method");
if (md.ReturnType != ContractParameterType.Boolean)
throw new ArgumentException("The verify method doesn't return boolean value.");
if (md.Parameters.Length > 0 && invocationScript is null)
throw new ArgumentException("The verify method requires parameters that need to be passed via the witness' invocation script.");

// Empty verification and non-empty invocation scripts
var invSize = invocationScript?.GetVarSize() ?? Array.Empty<byte>().GetVarSize();
size += Array.Empty<byte>().GetVarSize() + invSize;

// Check verify cost
using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshot.CreateSnapshot(), settings: ProtocolSettings, gas: maxExecutionCost);
engine.LoadContract(contract, md, CallFlags.ReadOnly);
if (invocationScript != null) engine.LoadScript(invocationScript, configureState: p => p.CallFlags = CallFlags.None);
if (engine.Execute() == VMState.FAULT) throw new ArgumentException($"Smart contract {contract.Hash} verification fault.");
if (!engine.ResultStack.Pop().GetBoolean()) throw new ArgumentException($"Smart contract {contract.Hash} returns false.");

maxExecutionCost -= engine.GasConsumed;
if (maxExecutionCost <= 0) throw new InvalidOperationException("Insufficient GAS.");
networkFee += engine.GasConsumed;
}
else if (IsSignatureContract(witness_script))
{
size += 67 + witness_script.GetVarSize();
networkFee += exec_fee_factor * SignatureContractCost();
}
else if (IsMultiSigContract(witness_script, out int m, out int n))
{
int size_inv = 66 * m;
size += IO.Helper.GetVarSize(size_inv) + size_inv + witness_script.GetVarSize();
networkFee += exec_fee_factor * MultiSignatureContractCost(m, n);
}
// We can support more contract types in the future.
}
networkFee += size * NativeContract.Policy.GetFeePerByte(snapshot);
foreach (TransactionAttribute attr in tx.Attributes)
{
networkFee += attr.CalculateNetworkFee(snapshot, tx);
}
return networkFee;
}

/// <summary>
/// Signs the <see cref="IVerifiable"/> in the specified <see cref="ContractParametersContext"/> with the wallet.
/// </summary>
Expand Down

0 comments on commit d8af2c6

Please sign in to comment.