diff --git a/docs/DynamicGas.md b/docs/DynamicGas.md
new file mode 100644
index 0000000000..d72cba7a05
--- /dev/null
+++ b/docs/DynamicGas.md
@@ -0,0 +1,86 @@
+# Dynamic Gas Pricing
+
+This release replaces the legacy fixed fee schedule with a runtime-aware
+resource model that charges contracts for the CPU, transient memory, and
+persistent storage they actually consume. The change affects every opcode,
+syscall, and native contract entrypoint.
+
+## Execution Model
+
+- **ResourceCost** – A new struct that tracks `cpuUnits`, `memoryBytes`, and
+ `storageBytes`. `ApplicationEngine` now exposes helpers (`ChargeCpu`,
+ `ChargeMemory`, `ChargeStorage`, and `AddResourceCost`) which translate
+ resource usage into datoshi using three policy-controlled multipliers:
+ `ExecFeeFactor`, `MemoryFeeFactor`, and `StoragePrice`.
+- **Memory Fee Factor** – Managed alongside the existing execution and storage
+ multipliers by the native `PolicyContract`. The defaults are available even
+ on historical state, and governance can tune the factor at runtime through
+ the existing policy RPC surface (`PolicyAPI.GetMemoryFeeFactorAsync`).
+- **Diagnostic Support** – Per-opcode resource contributions flow through the
+ existing `IDiagnostic` hook chain, enabling custom instrumentation and
+ trace tooling.
+
+## Opcode Accounting
+
+- `PreExecuteInstruction` now charges the static cost through `ChargeCpu` and
+ layers a dynamic adjustment via `CalculateDynamicOpcodeCost`. The estimator
+ inspects the evaluation stack to account for operand byte length, element
+ counts, and container sizes when executing string, buffer, and collection
+ opcodes (`PUSHDATA*`, `CAT`, `SUBSTR`, `LEFT/RIGHT`, `PACK`, `NEWARRAY*`,
+ `NEWBUFFER`, `MEMCPY`, etc.).
+- Container mutations (`APPEND`, `SETITEM`, `REVERSEITEMS`, map projections)
+ add CPU and memory proportional to the element footprint using a small
+ heuristic (`AverageElementOverhead`).
+- Iterator materialisation (`System.Iterator.Value`) bills the value size while
+ preserving immutability on the evaluation stack.
+- Numeric, bitwise, and shift opcodes scale with operand size: the estimator
+ caps total cost to avoid runaway multiplications while still reflecting the
+ expense of big integer arithmetic, modular exponentiation, or wide bitfield
+ operations.
+
+## Syscalls & Native Contracts
+
+- Interop descriptors accept optional dynamic calculators. The engine captures
+ raw stack arguments before conversion and evaluates any additional resource
+ cost prior to dispatching the handler.
+- Storage operations meter key lookups, value loads, iterator creation, and
+ writes: `System.Storage.Put`/`Delete` now charge CPU per-byte and convert the
+ existing storage delta logic to `ChargeStorage`; reads apply CPU + memory
+ proportional to the returned value size.
+- Storage iterators charge per entry during enumeration, aligning the cost of
+ `Iterator.Next` with the key/value data retrieved from the underlying store.
+- Runtime facilities (`System.Runtime.Log/Notify/LoadScript/CheckWitness`) are
+ instrumented to charge for serialized payloads, script size, and validation
+ inputs. Notifications charge per encoded byte at the point of serialization.
+- Native contracts leverage the new helpers. Method invocation now maps the
+ descriptor metadata (`CpuFee`, `StorageFee`) into resource costs without
+ manual datoshi arithmetic. Contract deployment/update charges are expressed
+ in terms of storage bytes plus a make-whole component to honour the minimum
+ deployment fee.
+
+## Policy & Tooling Updates
+
+- `PolicyContract` persists the `MemoryFeeFactor` alongside existing policy
+ knobs. Setter/getter entrypoints and RPC bindings mirror the execution and
+ storage controls.
+- `PolicyAPI` adds `GetMemoryFeeFactorAsync`, and unit/integration tests cover
+ the new RPC method.
+- Documentation and CLI examples now reference the memory fee factor and
+ describe the conversion pipeline for all three resource classes.
+
+## Migration Guidance
+
+1. **Contract Authors** – Expect gas consumption to scale with payload size.
+ Re-run critical flows on testnet and ensure off-chain estimators use the
+ updated policy API before mainnet activation. Storage writes remain priced
+ per byte; transient allocations and large notifications are now metered.
+2. **Node Operators** – Review policy multipliers (`exec`, `memory`,
+ `storage`) and adjust to reflect infrastructure costs. The defaults match
+ previous behaviour for small payloads and maintain deterministic execution.
+3. **Tooling Vendors** – Update SDKs and wallets to query
+ `GetMemoryFeeFactorAsync` when modelling gas, and surface richer diagnostics
+ where available.
+
+Activation should be coordinated through a dedicated hardfork flag so legacy
+state (which lacks the memory fee slot) absorbs the default without manual
+migration.
diff --git a/src/Neo/SmartContract/ApplicationEngine.Contract.cs b/src/Neo/SmartContract/ApplicationEngine.Contract.cs
index 0ec783cc08..762b07a881 100644
--- a/src/Neo/SmartContract/ApplicationEngine.Contract.cs
+++ b/src/Neo/SmartContract/ApplicationEngine.Contract.cs
@@ -124,7 +124,7 @@ internal protected UInt160 CreateStandardAccount(ECPoint pubKey)
long fee = IsHardforkEnabled(Hardfork.HF_Aspidochelone)
? CheckSigPrice
: 1 << 8;
- AddFee(fee * ExecFeeFactor);
+ ChargeCpu(fee);
return Contract.CreateSignatureRedeemScript(pubKey).ToScriptHash();
}
@@ -141,7 +141,7 @@ internal protected UInt160 CreateMultisigAccount(int m, ECPoint[] pubKeys)
long fee = IsHardforkEnabled(Hardfork.HF_Aspidochelone)
? CheckSigPrice * pubKeys.Length
: 1 << 8;
- AddFee(fee * ExecFeeFactor);
+ ChargeCpu(fee);
return Contract.CreateMultiSigRedeemScript(m, pubKeys).ToScriptHash();
}
diff --git a/src/Neo/SmartContract/ApplicationEngine.Crypto.cs b/src/Neo/SmartContract/ApplicationEngine.Crypto.cs
index a190cfdd0d..d6b2a1fbdb 100644
--- a/src/Neo/SmartContract/ApplicationEngine.Crypto.cs
+++ b/src/Neo/SmartContract/ApplicationEngine.Crypto.cs
@@ -69,7 +69,7 @@ protected internal bool CheckMultisig(byte[][] pubkeys, byte[][] signatures)
if (n == 0) throw new ArgumentException("pubkeys array cannot be empty.");
if (m == 0) throw new ArgumentException("signatures array cannot be empty.");
if (m > n) throw new ArgumentException($"signatures count ({m}) cannot be greater than pubkeys count ({n}).");
- AddFee(CheckSigPrice * n * ExecFeeFactor);
+ ChargeCpu(CheckSigPrice * n);
try
{
for (int i = 0, j = 0; i < m && j < n;)
diff --git a/src/Neo/SmartContract/ApplicationEngine.Fees.cs b/src/Neo/SmartContract/ApplicationEngine.Fees.cs
new file mode 100644
index 0000000000..58484cf416
--- /dev/null
+++ b/src/Neo/SmartContract/ApplicationEngine.Fees.cs
@@ -0,0 +1,94 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// ApplicationEngine.Fees.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System;
+
+namespace Neo.SmartContract
+{
+ public readonly struct ResourceCost
+ {
+ public long CpuUnits { get; }
+ public long MemoryBytes { get; }
+ public long StorageBytes { get; }
+
+ public static ResourceCost Zero => default;
+
+ public ResourceCost(long cpuUnits, long memoryBytes, long storageBytes)
+ {
+ if (cpuUnits < 0) throw new ArgumentOutOfRangeException(nameof(cpuUnits));
+ if (memoryBytes < 0) throw new ArgumentOutOfRangeException(nameof(memoryBytes));
+ if (storageBytes < 0) throw new ArgumentOutOfRangeException(nameof(storageBytes));
+
+ CpuUnits = cpuUnits;
+ MemoryBytes = memoryBytes;
+ StorageBytes = storageBytes;
+ }
+
+ public ResourceCost Add(ResourceCost other)
+ {
+ checked
+ {
+ return new ResourceCost(
+ CpuUnits + other.CpuUnits,
+ MemoryBytes + other.MemoryBytes,
+ StorageBytes + other.StorageBytes);
+ }
+ }
+
+ public bool IsZero => CpuUnits == 0 && MemoryBytes == 0 && StorageBytes == 0;
+
+ public static ResourceCost FromCpu(long cpuUnits) => new(cpuUnits, 0, 0);
+
+ public static ResourceCost FromMemory(long memoryBytes) => new(0, memoryBytes, 0);
+
+ public static ResourceCost FromStorage(long storageBytes) => new(0, 0, storageBytes);
+ }
+
+ partial class ApplicationEngine
+ {
+ protected internal void AddResourceCost(ResourceCost resourceCost)
+ {
+ if (resourceCost.IsZero)
+ return;
+
+ checked
+ {
+ long datoshi = resourceCost.CpuUnits * ExecFeeFactor
+ + resourceCost.MemoryBytes * MemoryFeeFactor
+ + resourceCost.StorageBytes * StoragePrice;
+ if (datoshi != 0)
+ AddFee(datoshi);
+ }
+ }
+
+ protected internal void ChargeCpu(long cpuUnits)
+ {
+ if (cpuUnits < 0) throw new ArgumentOutOfRangeException(nameof(cpuUnits));
+ if (cpuUnits == 0) return;
+ AddResourceCost(ResourceCost.FromCpu(cpuUnits));
+ }
+
+ protected internal void ChargeMemory(long memoryBytes)
+ {
+ if (memoryBytes < 0) throw new ArgumentOutOfRangeException(nameof(memoryBytes));
+ if (memoryBytes == 0) return;
+ AddResourceCost(ResourceCost.FromMemory(memoryBytes));
+ }
+
+ protected internal void ChargeStorage(long storageBytes)
+ {
+ if (storageBytes < 0) throw new ArgumentOutOfRangeException(nameof(storageBytes));
+ if (storageBytes == 0) return;
+ AddResourceCost(ResourceCost.FromStorage(storageBytes));
+ }
+ }
+}
+
diff --git a/src/Neo/SmartContract/ApplicationEngine.Iterator.cs b/src/Neo/SmartContract/ApplicationEngine.Iterator.cs
index faa5d0cd4f..631deb6aaf 100644
--- a/src/Neo/SmartContract/ApplicationEngine.Iterator.cs
+++ b/src/Neo/SmartContract/ApplicationEngine.Iterator.cs
@@ -47,7 +47,14 @@ internal protected static bool IteratorNext(IIterator iterator)
/// The element in the collection at the current position of the iterator.
internal protected StackItem IteratorValue(IIterator iterator)
{
- return iterator.Value(ReferenceCounter);
+ StackItem value = iterator.Value(ReferenceCounter);
+ long approximateSize = GetApproximateItemSize(value);
+ if (approximateSize > 0)
+ {
+ ChargeCpu(approximateSize);
+ ChargeMemory(approximateSize);
+ }
+ return value;
}
}
}
diff --git a/src/Neo/SmartContract/ApplicationEngine.Runtime.cs b/src/Neo/SmartContract/ApplicationEngine.Runtime.cs
index bbdef2e7d4..20324b4faf 100644
--- a/src/Neo/SmartContract/ApplicationEngine.Runtime.cs
+++ b/src/Neo/SmartContract/ApplicationEngine.Runtime.cs
@@ -217,6 +217,12 @@ protected internal void RuntimeLoadScript(byte[] script, CallFlags callFlags, Ar
if ((callFlags & ~CallFlags.All) != 0)
throw new ArgumentOutOfRangeException(nameof(callFlags), $"Invalid call flags: {callFlags}");
+ if (script is not null && script.Length > 0)
+ {
+ ChargeCpu(script.Length);
+ ChargeMemory(script.Length);
+ }
+
ExecutionContextState state = CurrentContext.GetState();
ExecutionContext context = LoadScript(new Script(script, true), configureState: p =>
{
@@ -237,6 +243,8 @@ protected internal void RuntimeLoadScript(byte[] script, CallFlags callFlags, Ar
/// if the account has witnessed the current transaction; otherwise, .
protected internal bool CheckWitness(byte[] hashOrPubkey)
{
+ if (hashOrPubkey is not null && hashOrPubkey.Length > 0)
+ ChargeCpu(hashOrPubkey.Length);
UInt160 hash = hashOrPubkey.Length switch
{
20 => new UInt160(hashOrPubkey),
@@ -322,7 +330,7 @@ protected internal BigInteger GetRandom()
buffer = nonceData = Cryptography.Helper.Murmur128(nonceData, ProtocolSettings.Network);
price = 1 << 4;
}
- AddFee(price * ExecFeeFactor);
+ ChargeCpu(price);
return new BigInteger(buffer, isUnsigned: true);
}
@@ -335,6 +343,11 @@ protected internal void RuntimeLog(byte[] state)
{
if (state.Length > MaxNotificationSize)
throw new ArgumentException($"Notification size {state.Length} exceeds maximum allowed size of {MaxNotificationSize} bytes", nameof(state));
+ if (state.Length > 0)
+ {
+ ChargeCpu(state.Length);
+ ChargeMemory(state.Length);
+ }
try
{
string message = state.ToStrictUtf8String();
@@ -361,6 +374,8 @@ protected internal void RuntimeNotify(byte[] eventName, Array state)
}
if (eventName.Length > MaxEventName)
throw new ArgumentException($"Event name size {eventName.Length} exceeds maximum allowed size of {MaxEventName} bytes", nameof(eventName));
+ if (eventName.Length > 0)
+ ChargeCpu(eventName.Length);
string name = eventName.ToStrictUtf8String();
ContractState contract = CurrentContext.GetState().Contract;
@@ -380,6 +395,11 @@ protected internal void RuntimeNotify(byte[] eventName, Array state)
using MemoryStream ms = new(MaxNotificationSize);
using BinaryWriter writer = new(ms, Utility.StrictUTF8, true);
BinarySerializer.Serialize(writer, state, MaxNotificationSize, Limits.MaxStackSize);
+ if (ms.Position > 0)
+ {
+ ChargeCpu((int)ms.Position);
+ ChargeMemory((int)ms.Position);
+ }
SendNotification(CurrentScriptHash, name, state);
}
@@ -389,9 +409,16 @@ protected internal void RuntimeNotifyV1(byte[] eventName, Array state)
throw new ArgumentException($"Event name size {eventName.Length} exceeds maximum allowed size of {MaxEventName} bytes", nameof(eventName));
if (CurrentContext.GetState().Contract is null)
throw new InvalidOperationException("Notifications are not allowed in dynamic scripts.");
+ if (eventName.Length > 0)
+ ChargeCpu(eventName.Length);
using MemoryStream ms = new(MaxNotificationSize);
using BinaryWriter writer = new(ms, Utility.StrictUTF8, true);
BinarySerializer.Serialize(writer, state, MaxNotificationSize, Limits.MaxStackSize);
+ if (ms.Position > 0)
+ {
+ ChargeCpu((int)ms.Position);
+ ChargeMemory((int)ms.Position);
+ }
SendNotification(CurrentScriptHash, eventName.ToStrictUtf8String(), state);
}
diff --git a/src/Neo/SmartContract/ApplicationEngine.Storage.cs b/src/Neo/SmartContract/ApplicationEngine.Storage.cs
index d04f5b8a8f..edfa64dee8 100644
--- a/src/Neo/SmartContract/ApplicationEngine.Storage.cs
+++ b/src/Neo/SmartContract/ApplicationEngine.Storage.cs
@@ -152,11 +152,24 @@ protected internal static StorageContext AsReadOnly(StorageContext context)
/// The value of the entry. Or if the entry doesn't exist.
protected internal ReadOnlyMemory? Get(StorageContext context, byte[] key)
{
- return SnapshotCache.TryGet(new StorageKey
+ if (key is not null && key.Length > 0)
+ ChargeCpu(key.Length);
+
+ var storageItem = SnapshotCache.TryGet(new StorageKey
{
Id = context.Id,
Key = key
- })?.Value;
+ });
+ if (storageItem is null)
+ return null;
+
+ var value = storageItem.Value;
+ if (!value.IsEmpty)
+ {
+ ChargeCpu(value.Length);
+ ChargeMemory(value.Length);
+ }
+ return value;
}
///
@@ -201,9 +214,12 @@ protected internal IIterator Find(StorageContext context, byte[] prefix, FindOpt
if ((options.HasFlag(FindOptions.PickField0) || options.HasFlag(FindOptions.PickField1)) && !options.HasFlag(FindOptions.DeserializeValues))
throw new ArgumentException("PickField0 or PickField1 requires DeserializeValues", nameof(options));
+ if (prefix.Length > 0)
+ ChargeCpu(prefix.Length);
+
var prefixKey = StorageKey.CreateSearchPrefix(context.Id, prefix);
var direction = options.HasFlag(FindOptions.Backwards) ? SeekDirection.Backward : SeekDirection.Forward;
- return new StorageIterator(SnapshotCache.Find(prefixKey, direction).GetEnumerator(), prefix.Length, options);
+ return new StorageIterator(SnapshotCache.Find(prefixKey, direction).GetEnumerator(), prefix.Length, options, this);
}
///
@@ -256,7 +272,8 @@ protected internal void Put(StorageContext context, byte[] key, byte[] value)
else
newDataSize = (item.Value.Length - 1) / 4 + 1 + value.Length - item.Value.Length;
}
- AddFee(newDataSize * StoragePrice);
+ ChargeCpu(key.Length + value.Length);
+ ChargeStorage(newDataSize);
item.Value = value;
}
@@ -281,6 +298,8 @@ protected internal void PutLocal(byte[] key, byte[] value)
protected internal void Delete(StorageContext context, byte[] key)
{
if (context.IsReadOnly) throw new ArgumentException("StorageContext is read-only", nameof(context));
+ if (key is not null && key.Length > 0)
+ ChargeCpu(key.Length);
SnapshotCache.Delete(new StorageKey
{
Id = context.Id,
diff --git a/src/Neo/SmartContract/ApplicationEngine.cs b/src/Neo/SmartContract/ApplicationEngine.cs
index 6bbb909d22..b10c3dbb27 100644
--- a/src/Neo/SmartContract/ApplicationEngine.cs
+++ b/src/Neo/SmartContract/ApplicationEngine.cs
@@ -74,6 +74,7 @@ public partial class ApplicationEngine : ExecutionEngine
private readonly Dictionary invocationCounter = new();
private readonly Dictionary contractTasks = new();
internal readonly uint ExecFeeFactor;
+ internal readonly uint MemoryFeeFactor;
// In the unit of datoshi, 1 datoshi = 1e-8 GAS
internal readonly uint StoragePrice;
private byte[] nonceData;
@@ -211,11 +212,13 @@ protected ApplicationEngine(
if (snapshotCache is null || persistingBlock?.Index == 0)
{
ExecFeeFactor = PolicyContract.DefaultExecFeeFactor;
+ MemoryFeeFactor = PolicyContract.DefaultMemoryFeeFactor;
StoragePrice = PolicyContract.DefaultStoragePrice;
}
else
{
ExecFeeFactor = NativeContract.Policy.GetExecFeeFactor(snapshotCache);
+ MemoryFeeFactor = NativeContract.Policy.GetMemoryFeeFactor(snapshotCache);
StoragePrice = NativeContract.Policy.GetStoragePrice(snapshotCache);
}
@@ -657,11 +660,24 @@ internal protected void ValidateCallFlags(CallFlags requiredCallFlags)
protected virtual void OnSysCall(InteropDescriptor descriptor)
{
ValidateCallFlags(descriptor.RequiredCallFlags);
- AddFee(descriptor.FixedPrice * ExecFeeFactor);
+ if (descriptor.FixedPrice > 0)
+ ChargeCpu(descriptor.FixedPrice);
- object[] parameters = new object[descriptor.Parameters.Count];
- for (int i = 0; i < parameters.Length; i++)
- parameters[i] = Convert(Pop(), descriptor.Parameters[i]);
+ int parameterCount = descriptor.Parameters.Count;
+ StackItem[] rawArguments = parameterCount == 0 ? Array.Empty() : new StackItem[parameterCount];
+ for (int i = 0; i < parameterCount; i++)
+ rawArguments[i] = Pop();
+
+ if (descriptor.DynamicCostCalculator is not null)
+ {
+ var extraCost = descriptor.DynamicCostCalculator(this, rawArguments);
+ if (!extraCost.IsZero)
+ AddResourceCost(extraCost);
+ }
+
+ object[] parameters = new object[parameterCount];
+ for (int i = 0; i < parameterCount; i++)
+ parameters[i] = Convert(rawArguments[i], descriptor.Parameters[i]);
object returnValue = descriptor.Handler.Invoke(this, parameters);
if (descriptor.Handler.ReturnType != typeof(void))
@@ -671,7 +687,13 @@ protected virtual void OnSysCall(InteropDescriptor descriptor)
protected override void PreExecuteInstruction(Instruction instruction)
{
Diagnostic?.PreExecuteInstruction(instruction);
- AddFee(ExecFeeFactor * OpCodePriceTable[(byte)instruction.OpCode]);
+ long baseCpuUnits = OpCodePriceTable[(byte)instruction.OpCode];
+ if (baseCpuUnits > 0)
+ ChargeCpu(baseCpuUnits);
+
+ var dynamicCost = CalculateDynamicOpcodeCost(instruction);
+ if (!dynamicCost.IsZero)
+ AddResourceCost(dynamicCost);
}
protected override void PostExecuteInstruction(Instruction instruction)
@@ -680,6 +702,495 @@ protected override void PostExecuteInstruction(Instruction instruction)
Diagnostic?.PostExecuteInstruction(instruction);
}
+ private const long MaxDynamicCpuUnits = 1_000_000_000; // Guardrail to avoid overflow while still covering large payloads
+
+ private ResourceCost CalculateDynamicOpcodeCost(Instruction instruction)
+ {
+ if (CurrentContext is null)
+ return ResourceCost.Zero;
+
+ var stack = CurrentContext.EvaluationStack;
+ try
+ {
+ switch (instruction.OpCode)
+ {
+ case OpCode.PUSHDATA1:
+ case OpCode.PUSHDATA2:
+ case OpCode.PUSHDATA4:
+ return new ResourceCost(instruction.Operand.Length, instruction.Operand.Length, 0);
+
+ case OpCode opCode when IsInlinePushBytesInstruction(opCode, instruction.Operand.Length):
+ return EstimatePushBytesCost(instruction);
+
+ case OpCode.CAT:
+ return EstimateConcatCost(stack, 2);
+
+ case OpCode.SUBSTR:
+ return EstimateSubstrCost(stack);
+
+ case OpCode.LEFT:
+ return EstimateLeftRightCost(stack, isLeft: true);
+
+ case OpCode.RIGHT:
+ return EstimateLeftRightCost(stack, isLeft: false);
+
+ case OpCode.MEMCPY:
+ return EstimateMemcpyCost(stack);
+
+ case OpCode.NEWBUFFER:
+ return EstimateNewBufferCost(stack);
+
+ case OpCode.PACK:
+ case OpCode.PACKMAP:
+ case OpCode.PACKSTRUCT:
+ return EstimatePackCost(stack);
+
+ case OpCode.NEWARRAY:
+ case OpCode.NEWARRAY_T:
+ case OpCode.NEWSTRUCT:
+ return EstimateNewCollectionCost(stack);
+
+ case OpCode.APPEND:
+ case OpCode.SETITEM:
+ return EstimateSetItemCost(stack);
+
+ case OpCode.PICKITEM:
+ return EstimatePickItemCost(stack);
+
+ case OpCode.REVERSEITEMS:
+ return EstimateReverseItemsCost(stack);
+
+ case OpCode.VALUES:
+ case OpCode.KEYS:
+ return EstimateKeyValueProjectionCost(stack, instruction.OpCode);
+
+ case OpCode.ADD:
+ case OpCode.SUB:
+ case OpCode.MAX:
+ case OpCode.MIN:
+ return EstimateBinaryNumericCost(stack, NumericOperationKind.Linear);
+
+ case OpCode.MUL:
+ case OpCode.DIV:
+ case OpCode.MOD:
+ case OpCode.MODMUL:
+ return EstimateBinaryNumericCost(stack, NumericOperationKind.Multiplicative);
+
+ case OpCode.POW:
+ case OpCode.MODPOW:
+ return EstimateBinaryNumericCost(stack, NumericOperationKind.Exponential);
+
+ case OpCode.SQRT:
+ case OpCode.ABS:
+ case OpCode.NEGATE:
+ case OpCode.INC:
+ case OpCode.DEC:
+ case OpCode.SIGN:
+ return EstimateUnaryNumericCost(stack);
+
+ case OpCode.AND:
+ case OpCode.OR:
+ case OpCode.XOR:
+ case OpCode.INVERT:
+ case OpCode.BOOLAND:
+ case OpCode.BOOLOR:
+ return EstimateBitOperationCost(stack, instruction.OpCode);
+
+ case OpCode.SHL:
+ case OpCode.SHR:
+ return EstimateShiftOperationCost(stack);
+
+ case OpCode.EQUAL:
+ case OpCode.NOTEQUAL:
+ case OpCode.NUMEQUAL:
+ case OpCode.NUMNOTEQUAL:
+ case OpCode.LT:
+ case OpCode.LE:
+ case OpCode.GT:
+ case OpCode.GE:
+ case OpCode.WITHIN:
+ return EstimateComparisonCost(stack);
+ }
+ }
+ catch (Exception)
+ {
+ // Ignore estimation errors; actual opcode execution will handle invalid states
+ }
+
+ return ResourceCost.Zero;
+ }
+
+ private static bool IsInlinePushBytesInstruction(OpCode opCode, int operandLength)
+ {
+ byte value = (byte)opCode;
+ return value is >= 0x01 and <= 0x4B && operandLength == value;
+ }
+
+ private static ResourceCost EstimatePushBytesCost(Instruction instruction)
+ {
+ int length = instruction.Operand.Length;
+ if (length <= 0)
+ return ResourceCost.Zero;
+
+ return new ResourceCost(ClampDynamicCpu(length), length, 0);
+ }
+
+ private static ResourceCost EstimateConcatCost(EvaluationStack stack, int itemCount)
+ {
+ if (stack.Count < itemCount)
+ return ResourceCost.Zero;
+
+ long totalInput = 0;
+ for (int i = 0; i < itemCount; i++)
+ totalInput += GetSpanLength(stack.Peek(i));
+
+ if (totalInput == 0)
+ return ResourceCost.Zero;
+
+ // Copying inputs plus creating the resulting buffer
+ return new ResourceCost(ClampDynamicCpu(totalInput * 2), totalInput, 0);
+ }
+
+ private static ResourceCost EstimateSubstrCost(EvaluationStack stack)
+ {
+ if (stack.Count < 3)
+ return ResourceCost.Zero;
+
+ var lengthItem = stack.Peek(0);
+ var startItem = stack.Peek(1);
+ var valueItem = stack.Peek(2);
+
+ if (!TryGetInt(lengthItem, out long length) || length <= 0)
+ return ResourceCost.Zero;
+
+ long available = GetSpanLength(valueItem);
+ long effective = Math.Min(Math.Max(length, 0), available);
+ if (effective <= 0)
+ return ResourceCost.Zero;
+
+ return new ResourceCost(ClampDynamicCpu(effective * 2), effective, 0);
+ }
+
+ private static ResourceCost EstimateLeftRightCost(EvaluationStack stack, bool isLeft)
+ {
+ if (stack.Count < 2)
+ return ResourceCost.Zero;
+
+ var lengthItem = stack.Peek(0);
+ var valueItem = stack.Peek(1);
+
+ if (!TryGetInt(lengthItem, out long length) || length <= 0)
+ return ResourceCost.Zero;
+
+ long available = GetSpanLength(valueItem);
+ long effective = Math.Min(Math.Max(length, 0), available);
+ if (effective <= 0)
+ return ResourceCost.Zero;
+
+ // Similar to substring; copying subset and allocating new bytes
+ return new ResourceCost(ClampDynamicCpu(effective * 2), effective, 0);
+ }
+
+ private static ResourceCost EstimateMemcpyCost(EvaluationStack stack)
+ {
+ if (stack.Count < 3)
+ return ResourceCost.Zero;
+
+ var countItem = stack.Peek(0);
+ if (!TryGetInt(countItem, out long length) || length <= 0)
+ return ResourceCost.Zero;
+
+ return new ResourceCost(ClampDynamicCpu(length * 3), 0, 0);
+ }
+
+ private static ResourceCost EstimateNewBufferCost(EvaluationStack stack)
+ {
+ if (stack.Count < 1)
+ return ResourceCost.Zero;
+
+ if (!TryGetInt(stack.Peek(0), out long size) || size <= 0)
+ return ResourceCost.Zero;
+
+ return new ResourceCost(ClampDynamicCpu(size), size, 0);
+ }
+
+ private static ResourceCost EstimatePackCost(EvaluationStack stack)
+ {
+ if (stack.Count < 1)
+ return ResourceCost.Zero;
+
+ if (!TryGetInt(stack.Peek(0), out long count) || count <= 0)
+ return ResourceCost.Zero;
+
+ long cpu = ClampDynamicCpu(count * 2);
+ long memory = count * AverageElementOverhead;
+ return new ResourceCost(cpu, memory, 0);
+ }
+
+ private static ResourceCost EstimateNewCollectionCost(EvaluationStack stack)
+ {
+ if (stack.Count < 1)
+ return ResourceCost.Zero;
+
+ if (!TryGetInt(stack.Peek(0), out long count) || count <= 0)
+ return ResourceCost.Zero;
+
+ long cpu = ClampDynamicCpu(count);
+ long memory = count * AverageElementOverhead;
+ return new ResourceCost(cpu, memory, 0);
+ }
+
+ private static ResourceCost EstimateSetItemCost(EvaluationStack stack)
+ {
+ if (stack.Count < 3)
+ return ResourceCost.Zero;
+
+ var container = stack.Peek(2);
+ var value = stack.Peek(0);
+ long valueSize = GetApproximateItemSize(value);
+ long cpu = ClampDynamicCpu(valueSize + AverageElementOverhead);
+ long memory = container.Type switch
+ {
+ StackItemType.Map => valueSize,
+ _ => 0
+ };
+ return new ResourceCost(Math.Max(cpu, 0), Math.Max(memory, 0), 0);
+ }
+
+ private static ResourceCost EstimatePickItemCost(EvaluationStack stack)
+ {
+ if (stack.Count < 2)
+ return ResourceCost.Zero;
+
+ var container = stack.Peek(1);
+ long elementSize = GetApproximateItemSize(container);
+ if (elementSize <= 0)
+ elementSize = AverageElementOverhead;
+ return new ResourceCost(elementSize, 0, 0);
+ }
+
+ private static ResourceCost EstimateReverseItemsCost(EvaluationStack stack)
+ {
+ if (stack.Count < 1)
+ return ResourceCost.Zero;
+
+ var arrayItem = stack.Peek(0);
+ if (arrayItem is VMArray array)
+ {
+ long count = array.Count;
+ return new ResourceCost(ClampDynamicCpu(count * 2), 0, 0);
+ }
+ return ResourceCost.Zero;
+ }
+
+ private static ResourceCost EstimateKeyValueProjectionCost(EvaluationStack stack, OpCode opCode)
+ {
+ if (stack.Count < 1)
+ return ResourceCost.Zero;
+
+ if (stack.Peek(0) is not Map map)
+ return ResourceCost.Zero;
+
+ long count = map.Count;
+ long cpu = ClampDynamicCpu(count * 2);
+ long memory = count * AverageElementOverhead;
+ return new ResourceCost(cpu, memory, 0);
+ }
+
+ private static bool TryGetInt(StackItem item, out long value)
+ {
+ try
+ {
+ value = (long)item.GetInteger();
+ return true;
+ }
+ catch
+ {
+ value = 0;
+ return false;
+ }
+ }
+
+ private const int AverageElementOverhead = 16;
+
+ private static long GetSpanLength(StackItem item)
+ {
+ try
+ {
+ return item.Type switch
+ {
+ StackItemType.ByteString or StackItemType.Buffer => item.GetSpan().Length,
+ _ => 0
+ };
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+
+ private static long GetApproximateItemSize(StackItem item, int depth = 0)
+ {
+ if (item is null)
+ return 0;
+
+ if (depth > 4)
+ return AverageElementOverhead;
+
+ try
+ {
+ return item.Type switch
+ {
+ StackItemType.ByteString or StackItemType.Buffer => item.GetSpan().Length,
+ StackItemType.Array or StackItemType.Struct => item is VMArray array ? SumArrayFootprint(array, depth + 1) : AverageElementOverhead,
+ StackItemType.Map => item is Map map ? SumMapFootprint(map, depth + 1) : AverageElementOverhead,
+ StackItemType.Integer => item.GetSpan().Length,
+ _ => AverageElementOverhead
+ };
+ }
+ catch
+ {
+ return AverageElementOverhead;
+ }
+ }
+
+ private static long SumArrayFootprint(VMArray array, int depth)
+ {
+ long total = AverageElementOverhead;
+ foreach (var element in array)
+ {
+ total = AddWithCap(total, GetApproximateItemSize(element, depth));
+ }
+ return total;
+ }
+
+ private static long SumMapFootprint(Map map, int depth)
+ {
+ long total = AverageElementOverhead;
+ foreach (var (key, value) in map)
+ {
+ total = AddWithCap(total, GetApproximateItemSize(key, depth));
+ total = AddWithCap(total, GetApproximateItemSize(value, depth));
+ }
+ return total;
+ }
+
+ private static ResourceCost EstimateBinaryNumericCost(EvaluationStack stack, NumericOperationKind kind)
+ {
+ if (stack.Count < 2)
+ return ResourceCost.Zero;
+
+ long left = GetNumericOperandSize(stack.Peek(0));
+ long right = GetNumericOperandSize(stack.Peek(1));
+
+ long cpu = kind switch
+ {
+ NumericOperationKind.Linear => Math.Max(left, right),
+ NumericOperationKind.Multiplicative => MultiplyWithCap(left, right),
+ NumericOperationKind.Exponential =>
+ MultiplyWithCap(left, MultiplyWithCap(Math.Max(right, 1), Math.Max(right, 1))),
+ _ => 0
+ };
+
+ return cpu > 0 ? ResourceCost.FromCpu(ClampDynamicCpu(cpu)) : ResourceCost.Zero;
+ }
+
+ private static ResourceCost EstimateUnaryNumericCost(EvaluationStack stack)
+ {
+ if (stack.Count < 1)
+ return ResourceCost.Zero;
+
+ long size = GetNumericOperandSize(stack.Peek(0));
+ return size > 0 ? ResourceCost.FromCpu(ClampDynamicCpu(size)) : ResourceCost.Zero;
+ }
+
+ private static ResourceCost EstimateBitOperationCost(EvaluationStack stack, OpCode opCode)
+ {
+ if (stack.Count < 1)
+ return ResourceCost.Zero;
+
+ long operandSize = GetSpanLength(stack.Peek(0));
+ if (opCode is OpCode.AND or OpCode.OR or OpCode.XOR)
+ {
+ if (stack.Count < 2)
+ return ResourceCost.Zero;
+ operandSize = Math.Max(GetSpanLength(stack.Peek(0)), GetSpanLength(stack.Peek(1)));
+ }
+ return operandSize > 0 ? ResourceCost.FromCpu(ClampDynamicCpu(operandSize)) : ResourceCost.Zero;
+ }
+
+ private static ResourceCost EstimateShiftOperationCost(EvaluationStack stack)
+ {
+ if (stack.Count < 2)
+ return ResourceCost.Zero;
+
+ long valueSize = GetNumericOperandSize(stack.Peek(0));
+ long shift = Math.Max(0, GetNumericOperandSize(stack.Peek(1)));
+ long cpu = ClampDynamicCpu(MultiplyWithCap(valueSize, Math.Max(1, shift)));
+ return cpu > 0 ? ResourceCost.FromCpu(cpu) : ResourceCost.Zero;
+ }
+
+ private static ResourceCost EstimateComparisonCost(EvaluationStack stack)
+ {
+ if (stack.Count < 2)
+ return ResourceCost.Zero;
+
+ long left = GetApproximateItemSize(stack.Peek(0));
+ long right = GetApproximateItemSize(stack.Peek(1));
+ long cpu = ClampDynamicCpu(Math.Max(left, right));
+ return cpu > 0 ? ResourceCost.FromCpu(cpu) : ResourceCost.Zero;
+ }
+
+ private static long GetNumericOperandSize(StackItem item)
+ {
+ try
+ {
+ return item.Type switch
+ {
+ StackItemType.Integer => Math.Max(1, item.GetSpan().Length),
+ StackItemType.ByteString or StackItemType.Buffer => Math.Max(1, item.GetSpan().Length),
+ _ => AverageElementOverhead
+ };
+ }
+ catch
+ {
+ return AverageElementOverhead;
+ }
+ }
+
+ private static long ClampDynamicCpu(long cpu)
+ {
+ if (cpu <= 0)
+ return 0;
+ return Math.Min(cpu, MaxDynamicCpuUnits);
+ }
+
+ private static long MultiplyWithCap(long left, long right)
+ {
+ if (left <= 0 || right <= 0)
+ return 0;
+ if (left > MaxDynamicCpuUnits)
+ return MaxDynamicCpuUnits;
+ if (right > MaxDynamicCpuUnits)
+ return MaxDynamicCpuUnits;
+ if (left > long.MaxValue / right)
+ return MaxDynamicCpuUnits;
+ return Math.Min(left * right, MaxDynamicCpuUnits);
+ }
+
+ private static long AddWithCap(long left, long right)
+ {
+ long result = left + right;
+ return result < left ? MaxDynamicCpuUnits : Math.Min(result, MaxDynamicCpuUnits);
+ }
+
+ private enum NumericOperationKind
+ {
+ Linear,
+ Multiplicative,
+ Exponential
+ }
+
private static Block CreateDummyBlock(IReadOnlyStore snapshot, ProtocolSettings settings)
{
UInt256 hash = NativeContract.Ledger.CurrentHash(snapshot);
@@ -700,7 +1211,7 @@ private static Block CreateDummyBlock(IReadOnlyStore snapshot, ProtocolSettings
};
}
- protected static InteropDescriptor Register(string name, string handler, long fixedPrice, CallFlags requiredCallFlags, Hardfork? hardfork = null)
+ protected static InteropDescriptor Register(string name, string handler, long fixedPrice, CallFlags requiredCallFlags, Hardfork? hardfork = null, Func dynamicCost = null)
{
var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
var method = typeof(ApplicationEngine).GetMethod(handler, flags)
@@ -711,7 +1222,8 @@ protected static InteropDescriptor Register(string name, string handler, long fi
Handler = method,
Hardfork = hardfork,
FixedPrice = fixedPrice,
- RequiredCallFlags = requiredCallFlags
+ RequiredCallFlags = requiredCallFlags,
+ DynamicCostCalculator = dynamicCost
};
services ??= [];
services.Add(descriptor.Hash, descriptor);
diff --git a/src/Neo/SmartContract/InteropDescriptor.cs b/src/Neo/SmartContract/InteropDescriptor.cs
index 172dcfda12..47ddc315d7 100644
--- a/src/Neo/SmartContract/InteropDescriptor.cs
+++ b/src/Neo/SmartContract/InteropDescriptor.cs
@@ -10,6 +10,8 @@
// modifications are permitted.
using Neo.Cryptography;
+using Neo.VM.Types;
+using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
@@ -58,6 +60,12 @@ public uint Hash
///
public long FixedPrice { get; init; }
+ ///
+ /// An optional delegate used to compute dynamic resource cost for the interoperable service.
+ /// Receives the engine and the raw stack arguments popped for the call (top-first order).
+ ///
+ public Func DynamicCostCalculator { get; init; }
+
///
/// Required Hardfork to be active.
///
diff --git a/src/Neo/SmartContract/Iterators/StorageIterator.cs b/src/Neo/SmartContract/Iterators/StorageIterator.cs
index e2e69e7337..92efa915e5 100644
--- a/src/Neo/SmartContract/Iterators/StorageIterator.cs
+++ b/src/Neo/SmartContract/Iterators/StorageIterator.cs
@@ -22,12 +22,14 @@ internal class StorageIterator : IIterator
private readonly IEnumerator<(StorageKey Key, StorageItem Value)> enumerator;
private readonly int prefixLength;
private readonly FindOptions options;
+ private readonly ApplicationEngine engine;
- public StorageIterator(IEnumerator<(StorageKey, StorageItem)> enumerator, int prefixLength, FindOptions options)
+ public StorageIterator(IEnumerator<(StorageKey, StorageItem)> enumerator, int prefixLength, FindOptions options, ApplicationEngine engine = null)
{
this.enumerator = enumerator;
this.prefixLength = prefixLength;
this.options = options;
+ this.engine = engine;
}
public void Dispose()
@@ -37,7 +39,16 @@ public void Dispose()
public bool Next()
{
- return enumerator.MoveNext();
+ bool moved = enumerator.MoveNext();
+ if (moved && engine is not null)
+ {
+ ReadOnlyMemory key = enumerator.Current.Key.Key;
+ ReadOnlyMemory value = enumerator.Current.Value.Value;
+ int cpu = key.Length + value.Length;
+ if (cpu > 0)
+ engine.ChargeCpu(cpu);
+ }
+ return moved;
}
public StackItem Value(IReferenceCounter referenceCounter)
diff --git a/src/Neo/SmartContract/Native/ContractManagement.cs b/src/Neo/SmartContract/Native/ContractManagement.cs
index 7ea9c8cdb0..1eab9f95ca 100644
--- a/src/Neo/SmartContract/Native/ContractManagement.cs
+++ b/src/Neo/SmartContract/Native/ContractManagement.cs
@@ -240,10 +240,13 @@ private async ContractTask Deploy(ApplicationEngine engine, byte[
if (manifest.Length == 0)
throw new ArgumentException($"Manifest length cannot be zero.");
- engine.AddFee(Math.Max(
- engine.StoragePrice * (nefFile.Length + manifest.Length),
- GetMinimumDeploymentFee(engine.SnapshotCache)
- ));
+ long storageBytes = nefFile.Length + manifest.Length;
+ long storageCost = storageBytes * engine.StoragePrice;
+ long minimumFee = GetMinimumDeploymentFee(engine.SnapshotCache);
+ if (storageBytes > 0)
+ engine.ChargeStorage(storageBytes);
+ if (minimumFee > storageCost)
+ engine.AddFee(minimumFee - storageCost);
NefFile nef = nefFile.AsSerializable();
ContractManifest parsedManifest = ContractManifest.Parse(manifest);
@@ -294,7 +297,9 @@ private ContractTask Update(ApplicationEngine engine, byte[] nefFile, byte[] man
if (nefFile is null && manifest is null)
throw new ArgumentException("NEF file and manifest cannot both be null.");
- engine.AddFee(engine.StoragePrice * ((nefFile?.Length ?? 0) + (manifest?.Length ?? 0)));
+ long updateBytes = (nefFile?.Length ?? 0) + (manifest?.Length ?? 0);
+ if (updateBytes > 0)
+ engine.ChargeStorage(updateBytes);
var contractState = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_Contract, engine.CallingScriptHash))
?? throw new InvalidOperationException($"Updating Contract Does Not Exist: {engine.CallingScriptHash}");
diff --git a/src/Neo/SmartContract/Native/NativeContract.cs b/src/Neo/SmartContract/Native/NativeContract.cs
index 4a22c91b53..418410b87f 100644
--- a/src/Neo/SmartContract/Native/NativeContract.cs
+++ b/src/Neo/SmartContract/Native/NativeContract.cs
@@ -428,7 +428,7 @@ internal async void Invoke(ApplicationEngine engine, byte version)
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);
+ engine.AddResourceCost(new ResourceCost(method.CpuFee, 0, method.StorageFee));
List
public const uint DefaultStoragePrice = 100000;
+ ///
+ /// The default memory fee factor used for charging transient memory allocations.
+ ///
+ public const uint DefaultMemoryFeeFactor = 30;
+
///
/// The default network fee per byte of transactions.
/// In the unit of datoshi, 1 datoshi = 1e-8 GAS
@@ -65,6 +70,11 @@ public sealed class PolicyContract : NativeContract
///
public const uint MaxStoragePrice = 10000000;
+ ///
+ /// The maximum memory fee factor that the committee can set.
+ ///
+ public const uint MaxMemoryFeeFactor = 1000;
+
///
/// The maximum block generation time that the committee can set in milliseconds.
///
@@ -86,6 +96,7 @@ public sealed class PolicyContract : NativeContract
private const byte Prefix_FeePerByte = 10;
private const byte Prefix_ExecFeeFactor = 18;
private const byte Prefix_StoragePrice = 19;
+ private const byte Prefix_MemoryFeeFactor = 24;
private const byte Prefix_AttributeFee = 20;
private const byte Prefix_MillisecondsPerBlock = 21;
private const byte Prefix_MaxValidUntilBlockIncrement = 22;
@@ -94,6 +105,7 @@ public sealed class PolicyContract : NativeContract
private readonly StorageKey _feePerByte;
private readonly StorageKey _execFeeFactor;
private readonly StorageKey _storagePrice;
+ private readonly StorageKey _memoryFeeFactor;
private readonly StorageKey _millisecondsPerBlock;
private readonly StorageKey _maxValidUntilBlockIncrement;
private readonly StorageKey _maxTraceableBlocks;
@@ -112,6 +124,7 @@ internal PolicyContract() : base()
_feePerByte = CreateStorageKey(Prefix_FeePerByte);
_execFeeFactor = CreateStorageKey(Prefix_ExecFeeFactor);
_storagePrice = CreateStorageKey(Prefix_StoragePrice);
+ _memoryFeeFactor = CreateStorageKey(Prefix_MemoryFeeFactor);
_millisecondsPerBlock = CreateStorageKey(Prefix_MillisecondsPerBlock);
_maxValidUntilBlockIncrement = CreateStorageKey(Prefix_MaxValidUntilBlockIncrement);
_maxTraceableBlocks = CreateStorageKey(Prefix_MaxTraceableBlocks);
@@ -124,6 +137,7 @@ internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfor
engine.SnapshotCache.Add(_feePerByte, new StorageItem(DefaultFeePerByte));
engine.SnapshotCache.Add(_execFeeFactor, new StorageItem(DefaultExecFeeFactor));
engine.SnapshotCache.Add(_storagePrice, new StorageItem(DefaultStoragePrice));
+ engine.SnapshotCache.Add(_memoryFeeFactor, new StorageItem(DefaultMemoryFeeFactor));
}
if (hardfork == Hardfork.HF_Echidna)
{
@@ -168,6 +182,19 @@ public uint GetStoragePrice(IReadOnlyStore snapshot)
return (uint)(BigInteger)snapshot[_storagePrice];
}
+ ///
+ /// Gets the memory fee factor.
+ ///
+ /// The snapshot used to read data.
+ /// The memory fee factor.
+ [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)]
+ public uint GetMemoryFeeFactor(IReadOnlyStore snapshot)
+ {
+ return snapshot.TryGet(_memoryFeeFactor, out var item)
+ ? (uint)(BigInteger)item
+ : DefaultMemoryFeeFactor;
+ }
+
///
/// Gets the block generation time in milliseconds.
///
@@ -357,6 +384,15 @@ private void SetStoragePrice(ApplicationEngine engine, uint value)
engine.SnapshotCache.GetAndChange(_storagePrice).Set(value);
}
+ [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)]
+ private void SetMemoryFeeFactor(ApplicationEngine engine, uint value)
+ {
+ if (value == 0 || value > MaxMemoryFeeFactor)
+ throw new ArgumentOutOfRangeException(nameof(value), $"MemoryFeeFactor must be between [1, {MaxMemoryFeeFactor}], got {value}");
+ if (!CheckCommittee(engine)) throw new InvalidOperationException();
+ engine.SnapshotCache.GetAndChange(_memoryFeeFactor, () => new StorageItem(DefaultMemoryFeeFactor)).Set(value);
+ }
+
[ContractMethod(Hardfork.HF_Echidna, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)]
private void SetMaxValidUntilBlockIncrement(ApplicationEngine engine, uint value)
{
diff --git a/src/RpcClient/PolicyAPI.cs b/src/RpcClient/PolicyAPI.cs
index e175feff8a..3a4e673327 100644
--- a/src/RpcClient/PolicyAPI.cs
+++ b/src/RpcClient/PolicyAPI.cs
@@ -48,6 +48,16 @@ public async Task GetStoragePriceAsync()
return (uint)result.Stack.Single().GetInteger();
}
+ ///
+ /// Get Memory Fee Factor
+ ///
+ ///
+ public async Task GetMemoryFeeFactorAsync()
+ {
+ var result = await TestInvokeAsync(scriptHash, "getMemoryFeeFactor").ConfigureAwait(false);
+ return (uint)result.Stack.Single().GetInteger();
+ }
+
///
/// Get Network Fee Per Byte
///
diff --git a/tests/Neo.RpcClient.Tests/UT_PolicyAPI.cs b/tests/Neo.RpcClient.Tests/UT_PolicyAPI.cs
index bf5d6de6ec..0d76e2f76e 100644
--- a/tests/Neo.RpcClient.Tests/UT_PolicyAPI.cs
+++ b/tests/Neo.RpcClient.Tests/UT_PolicyAPI.cs
@@ -57,6 +57,16 @@ public async Task TestGetStoragePrice()
Assert.AreEqual(100000u, result);
}
+ [TestMethod]
+ public async Task TestGetMemoryFeeFactor()
+ {
+ byte[] testScript = NativeContract.Policy.Hash.MakeScript("getMemoryFeeFactor");
+ UT_TransactionManager.MockInvokeScript(rpcClientMock, testScript, new ContractParameter { Type = ContractParameterType.Integer, Value = new BigInteger(30) });
+
+ var result = await policyAPI.GetMemoryFeeFactorAsync();
+ Assert.AreEqual(30u, result);
+ }
+
[TestMethod]
public async Task TestGetFeePerByte()
{