Skip to content

ExecutionEngine: collect parameters affecting opcode price - #571

Merged
shargon merged 56 commits into
neo-project:masterfrom
Turalchik:dynamic-opcode-pricing
Aug 2, 2026
Merged

shargon merged 56 commits into
neo-project:masterfrom
Turalchik:dynamic-opcode-pricing

Conversation

@Turalchik

@Turalchik Turalchik commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

This PR adds collection of parameters in the OpcodePriceArgs struct (length, reference count changes, etc.) that affect opcode costs for dynamic pricing. The pricing is calculated in the related PR neo-project/neo#4536.

A partial port of nspcc-dev/neo-go#4087. Ref. nspcc-dev/neo-go#4043.

Signed-off-by: Tural Devrishev <tural@nspcc.ru>
@codecov

codecov Bot commented Apr 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.44444% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.49%. Comparing base (e5fc3da) to head (68a9eaa).

Files with missing lines Patch % Lines
src/Neo.VM/JumpTable/JumpTable.Compound.cs 77.14% 23 Missing and 1 partial ⚠️
src/Neo.VM/JumpTable/JumpTable.Control.cs 88.23% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #571      +/-   ##
==========================================
- Coverage   85.55%   85.49%   -0.06%     
==========================================
  Files          41       42       +1     
  Lines        2720     2896     +176     
  Branches      345      349       +4     
==========================================
+ Hits         2327     2476     +149     
- Misses        303      330      +27     
  Partials       90       90              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@AnnaShaleva AnnaShaleva left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will submit detailed handlers review later.

Comment thread src/Neo.VM/OpCodePriceArgs.cs Outdated
Comment thread src/Neo.VM/ExecutionEngine.cs Outdated
Comment thread src/Neo.VM/ExecutionEngine.cs Outdated
Comment thread src/Neo.VM/Types/Struct.cs Outdated
Comment thread src/Neo.VM/OpCodePriceArgs.cs Outdated
@AnnaShaleva AnnaShaleva changed the title collect parameters that affect price of opcode ExecutionEngine: collect parameters affecting opcode price Apr 17, 2026
Tural Devrishev added 4 commits April 20, 2026 11:49
Signed-off-by: Tural Devrishev <tural@nspcc.ru>
Signed-off-by: Tural Devrishev <tural@nspcc.ru>
Signed-off-by: Tural Devrishev <tural@nspcc.ru>
@Turalchik
Turalchik requested a review from AnnaShaleva April 20, 2026 09:59

@AnnaShaleva AnnaShaleva left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will check it one more time together with neo-project/neo#4536.

Comment thread src/Neo.VM/JumpTable/JumpTable.Compound.cs Outdated
Comment thread src/Neo.VM/JumpTable/JumpTable.Compound.cs Outdated
Comment thread src/Neo.VM/JumpTable/JumpTable.Compound.cs Outdated
Comment thread src/Neo.VM/JumpTable/JumpTable.Push.cs Outdated
Comment thread src/Neo.VM/JumpTable/JumpTable.Types.cs
Comment thread src/Neo.VM/OpCodePriceParams.cs Outdated
Comment thread src/Neo.VM/JumpTable/JumpTable.Splice.cs
Tural Devrishev added 3 commits April 21, 2026 11:41
Signed-off-by: Tural Devrishev <tural@nspcc.ru>
Signed-off-by: Tural Devrishev <tural@nspcc.ru>
Signed-off-by: Tural Devrishev <tural@nspcc.ru>
Comment thread src/Neo.VM/OpCodePriceParams.cs Outdated

@AnnaShaleva AnnaShaleva left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need to update parameters construction for Map-related opcodes (ref. nspcc-dev/neo-go#4222); other than that the PR looks legit, let's wait for the update.

Comment thread src/Neo.VM/Types/Struct.cs Outdated
Comment thread src/Neo.VM/JumpTable/JumpTable.Slot.cs Outdated
Tural Devrishev added 3 commits April 28, 2026 21:47
Signed-off-by: Tural Devrishev <tural@nspcc.ru>
Signed-off-by: Tural Devrishev <tural@nspcc.ru>
Signed-off-by: Tural Devrishev <tural@nspcc.ru>
Comment thread src/Neo.VM/OpCodePriceParams.cs Outdated
/// Typ specifies type of <see cref="StackItemType"/> which in most of the cases serves
/// as an operand of the given opcode.
/// </summary>
public StackItemType Type { get; internal set; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this is not initialized every time? Is this required?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's required for some of opcodes. For other opcodes price doesn't depend on the operand type.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you give me an example please?

@AnnaShaleva AnnaShaleva Apr 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. CONVERT requires Type since it works differently for Array/Struct and ByteStrings:
https://github.com/Turalchik/neo/blob/f0699631792accf4cdd717073091199f0bd650a3/src/Neo/SmartContract/Fee.cs#L357

PICKITEM requires Type argument since its price depends on whether the operand is a compound type or not:
https://github.com/Turalchik/neo/blob/f0699631792accf4cdd717073091199f0bd650a3/src/Neo/SmartContract/Fee.cs#L368

And APPEND doesn't actually depend on the operand type. RefsDelta and NClonedItems arguments cover possible price fluctuations for different operand types:
https://github.com/Turalchik/neo/blob/f0699631792accf4cdd717073091199f0bd650a3/src/Neo/SmartContract/Fee.cs#L353

@AnnaShaleva AnnaShaleva Apr 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, different combinations of Type, RefsDelta, Length and NClonedItems are used for every opcode. The set of required parameters is defined separately for every opcode via opcode handler investigation and via benchmarks of edge cases in nspcc-dev/neo-go#4218.

@AnnaShaleva
AnnaShaleva self-requested a review April 29, 2026 09:14
Comment thread src/Neo.VM/JumpTable/JumpTable.Compound.cs
Comment thread src/Neo.VM/JumpTable/JumpTable.Compound.cs
Comment thread src/Neo.VM/JumpTable/JumpTable.Stack.cs Outdated
Comment thread src/Neo.VM/JumpTable/JumpTable.Types.cs Outdated
Comment thread src/Neo.VM/JumpTable/JumpTable.Types.cs Outdated
Signed-off-by: Tural Devrishev <tural@nspcc.ru>
Comment thread src/Neo.VM/JumpTable/JumpTable.Compound.cs
{
engine.ReferenceCounter.RemoveStackReference(value);
throw new CatchableException($"The index of {nameof(VMArray)} is out of range, {index}/[0, {array.Count}).");
var r4 = engine.ReferenceCounter.Count;

@shargon shargon Jul 23, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be good if we add some uts for this catcheable exceptions (json)
This can be done in a different PR

var r2 = engine.ReferenceCounter.Count;
engine.Push(x.ConvertTo(toType));
var (type, length) = (StackItemType.Any, 0);
if (fromType == StackItemType.Array && toType == StackItemType.Struct || fromType == StackItemType.Struct && toType == StackItemType.Array)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From array to array or from struct to struct?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When converting Array to Struct

public override StackItem ConvertTo(StackItemType type)
{
if (Type == StackItemType.Array && type == StackItemType.Struct)
return new Struct(new List<StackItem>(InnerList));
return base.ConvertTo(type);
}
or Struct to Array
public override StackItem ConvertTo(StackItemType type)
{
if (type == StackItemType.Array)
return new Array(new List<StackItem>(InnerList));
return base.ConvertTo(type);
}
copying occurs, while converting Array to Array (or Struct to Struct) is a no-op.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but type will be Any in RunStats?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, Type will be equal to StackItemType.Array

type = StackItemType.Array;
runStats.CollectType(type);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check my UT (693ed61)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, Type is equal to StackItemType.Any in cases where the length of the array, structure, byte array, or other parameters do not affect the opcode operation time. When an Array is converted to an Array (or something like that), the memory is not copied, respectively, the instruction executes in constant time relative to the size of the array. The price of CONVERT depends on the size of the data structure only when the Type is equal to StackItemType.Array or StackItemType.ByteString https://github.com/neo-project/neo/blob/7c1e64512984ebdd39c0d26df25abb5670af7760/src/Neo/SmartContract/ApplicationEngine.OpCodePricesV1.cs#L151-L157

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think they should pay as if was converted, these optimizations should be done in compiler side. We can be faster, but they call convert

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Price should reflect real resources spent and any imbalance is a problem. We're not spending any real resources on same-type conversions, these operations are fast, price should just reflect that instead of trying to punish anyone. Why anyone is doing that is a separate question, it's the same as NOP price --- we don't care why people use NOP (theoretically they shouldn't), we just charge some tiny amount of GAS for it and move the instruction pointer to the next (more interesting or not) instruction.

Comment thread src/Neo.VM/JumpTable/JumpTable.Control.cs
@cschuchardt88

Copy link
Copy Markdown
Member

Review summary

This PR is a solid foundation for dynamic opcode pricing: it introduces RunStats, threads it through the jump table, and reworks catchable faults to go through ExecuteThrow with ref-delta accounting (neo-go-aligned). Pricing itself correctly lives in neo-project/neo#4536.

I think this is mergeable after a few verification items, not a redesign.

Must verify before merge

  1. CatchableException migration is complete
    ExecuteNext no longer catches CatchableException. All former throw new CatchableException(...) paths in handlers must use ExecuteThrow (the compound/map/buffer cases in the diff look correct). Please confirm with a full-branch grep that nothing still throws CatchableException expecting the old outer catch. A regression test for a former catchable path would help.

  2. Breaking API surface
    This breaks:

    • JumpTable.DelAction / all handlers
    • PostExecuteInstruction
    • ExecuteThrow
    • Map.Remove
    • Struct.Clone
      Please call this out explicitly for Neo.VM consumers and land in lockstep with neo#4536.

Suggestions (non-blocking if correct)

  1. More RunStats unit tests
    ASSERTMSG UTF-8 length tests are excellent. Please add similar captures for at least: map remove (index + refs), struct clone (NClonedItems), append/newarray length, and one fault path that uses ExecuteThrow + RefsDelta.

  2. Struct.Clone clone count
    Prefer an explicit counter in the clone loop over nClonedItems = MaxStackSize - count for readability and future-proofing.

  3. Map.Remove ordered index
    Add a small test: insert A,B,C → remove B → out index == 1 and remaining order A,C (so pricing uses the ordered index neo-go expects).

Nits

  1. Document that each RunStats field is set once per instruction (later Collect* overwrites).
  2. Document that PostExecuteInstruction may receive a null instruction on the synthetic RET path.

Existing discussion

  • Agree with @AnnaShaleva: no need to special-case RunStats population for uncatchable FAULT.

@Turalchik
Turalchik requested a review from shargon July 23, 2026 10:28
shargon
shargon previously approved these changes Jul 23, 2026

@shargon shargon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As talked with @roman-khimov , my ut must be updated, but the code looks good to me

@vncoelho vncoelho left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm
There is no much test for now

Comment thread src/Neo.VM/RunStats.cs
RefsDelta = refsDelta;
}

/// <summary>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe some virtual method here for additional info to this stats, that we can in the future trigger for specific case and serve as a flag for price calculation.
Not sure yet.

Comment thread src/Neo.VM/RunStats.cs
/// <summary>
/// Contains opcode-specific parameters used to calculate dynamic price.
/// </summary>
public struct RunStats

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest another name such as OpCodeRunStats, OpcodeStackStats, OpcodePriceStats

ExecutionContext context = CurrentContext!;
Instruction? currentInstruction = context.CurrentInstruction;
Instruction instruction = currentInstruction ?? Instruction.RET;
RunStats runStats = default;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RunStats can be a field of ExecutionEngine?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was discussed here #571 (comment).

@Turalchik
Turalchik requested review from Jim8y, Wi1l-B0t and ajara87 July 23, 2026 16:14
Wi1l-B0t
Wi1l-B0t previously approved these changes Jul 23, 2026

@Wi1l-B0t Wi1l-B0t left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@ajara87

ajara87 commented Jul 23, 2026

Copy link
Copy Markdown
Member

UT fails

@AnnaShaleva AnnaShaleva left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, will approve after unit-test fix.

Signed-off-by: Tural Devrishev <tural@nspcc.ru>
@Turalchik
Turalchik dismissed stale reviews from Wi1l-B0t and shargon via 68a9eaa July 28, 2026 10:42
@shargon
shargon merged commit 3665b50 into neo-project:master Aug 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants