Skip to content

vm: add dynamic opcode pricing - #4087

Open
Turalchik wants to merge 1 commit into
masterfrom
dynamic-opcode-pricing
Open

vm: add dynamic opcode pricing#4087
Turalchik wants to merge 1 commit into
masterfrom
dynamic-opcode-pricing

Conversation

@Turalchik

@Turalchik Turalchik commented Nov 18, 2025

Copy link
Copy Markdown
Contributor

Close #4043.

After the introduction of the new pricing, the mainnet accepts blocks up to the block with the index 295097, because there are not enough funds to execute the transaction in block 295098:

wrong dump file or settings mismatch: failed to add block 295098: onPersist failed: VM has failed: at instruction 0 (SYSCALL): insufficient funds

Mainnet states match.

go run ./scripts/compare-states/compare-states.go http://127.0.0.1:10332 http://127.0.0.1:10334
at 0: 58a5157b7e99eeabf631291f1747ec8eb12ab89461cda888492b17a301de81e8 vs 58a5157b7e99eeabf631291f1747ec8eb12ab89461cda888492b17a301de81e8
at 295097: ed6df187f75e1f68225b412495856e5d45bb1377c35202eaa65c16cb4fe31f07 vs ed6df187f75e1f68225b412495856e5d45bb1377c35202eaa65c16cb4fe31f07

Mainnet gas consumed match.

go run ./scripts/compare-gasconsumed/compare-gasconsumed.go http://127.0.0.1:10332 http://127.0.0.1:10334 0 295097
compare-gasconsumed: all checks passed

Testnet gas consumed match.

go run ./scripts/compare-gasconsumed/compare-gasconsumed.go http://127.0.0.1:10332 http://127.0.0.1:20332 0 601084
compare-gasconsumed: all checks passed

Privnet with https://github.com/nspcc-dev/neo-bench states match.

go run ./scripts/compare-states/compare-states.go http://127.0.0.1:30335 http://127.0.0.1:30333 --ignore-height 
at 0: 64b81230de46a763f167b8b4b0d0ee3383e5b162d2d93cf66bf7d815c584a5c1 vs 64b81230de46a763f167b8b4b0d0ee3383e5b162d2d93cf66bf7d815c584a5c1
at 68: ca76fffbd5138e4d2ba198ab74b0d145525bbd6f60cf8502828cf341fd363ba6 vs ca76fffbd5138e4d2ba198ab74b0d145525bbd6f60cf8502828cf341fd363ba6

Privnet with https://github.com/nspcc-dev/neo-bench gas consumed match.

go run ./scripts/compare-gasconsumed/compare-gasconsumed.go http://127.0.0.1:30335 http://127.0.0.1:30333 0 68
compare-gasconsumed: all checks passed

Comparison statistics for the old model on the mainnet:

go run ./scripts/compare-gasconsumed/compare-gasconsumed.go -r http://127.0.0.1:10332 http://127.0.0.1:20334 0 295097
ratio analysis (A/B):
  transactions: 33085
  average: 1.290861
  min: 0.105444
  max: 77.982933
  stddev: 1.515816

Comparison statistics for the old model on the mainnet:

go run ./scripts/compare-gasconsumed/compare-gasconsumed.go -r http://127.0.0.1:10332 http://127.0.0.1:20332 0 601084
ratio analysis (A/B):
  transactions: 632506
  average: 1.010010
  min: 0.530678
  max: 109.903513
  stddev: 0.171589

Related issues with gas consumption consistency: #4288, #4291, #4294, #4295

@Turalchik
Turalchik marked this pull request as draft November 18, 2025 14:29
@Turalchik
Turalchik force-pushed the dynamic-opcode-pricing branch from b84940d to bf06cda Compare November 27, 2025 19:13
@codecov

codecov Bot commented Nov 27, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.54723% with 72 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.43%. Comparing base (48a0ae4) to head (886b761).
⚠️ Report is 16 commits behind head on master.

Files with missing lines Patch % Lines
pkg/core/fee/opcode.go 18.18% 72 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4087      +/-   ##
==========================================
- Coverage   83.56%   83.43%   -0.13%     
==========================================
  Files         359      359              
  Lines       44158    44425     +267     
==========================================
+ Hits        36901    37067     +166     
- Misses       5438     5541     +103     
+ Partials     1819     1817       -2     

☔ 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.

@Turalchik
Turalchik force-pushed the dynamic-opcode-pricing branch 22 times, most recently from bb928b3 to 43b4467 Compare December 4, 2025 10:30
@Turalchik
Turalchik force-pushed the dynamic-opcode-pricing branch from 43b4467 to 995606b Compare December 5, 2025 17:24
@Turalchik
Turalchik force-pushed the dynamic-opcode-pricing branch 2 times, most recently from 174df2d to 0315022 Compare December 16, 2025 16:11
@Turalchik
Turalchik force-pushed the dynamic-opcode-pricing branch 10 times, most recently from 0de3721 to 5196698 Compare December 22, 2025 07:50

@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.

@Turalchik, conflicts.

The first part of review, will submit more later.

Comment thread pkg/vm/vm.go
Comment thread pkg/vm/bench_test.go
Comment thread pkg/vm/opcodebench_test.go Outdated
Comment thread pkg/vm/opcodebench_test.go Outdated
Comment thread pkg/vm/opcodebench_test.go Outdated
Comment thread pkg/vm/opcodebench_test.go Outdated
Comment thread pkg/vm/opcodebench_test.go Outdated
Comment thread pkg/vm/opcodebench_test.go Outdated
Comment thread pkg/vm/opcodebench_test.go Outdated
Comment thread pkg/vm/opcodebench_test.go Outdated
Comment thread pkg/core/interop/runtime/ext_test.go Outdated
Comment thread pkg/core/native/native_test/management_test.go Outdated
Comment thread pkg/core/native/native_test/management_test.go Outdated
Comment thread pkg/core/native/policy_test.go Outdated
Comment thread pkg/services/rpcsrv/server_test.go Outdated
Comment thread go.mod Outdated
Comment thread pkg/core/fee/opcode.go Outdated
Comment thread pkg/core/fee/opcode.go Outdated
Comment thread pkg/core/fee/opcode.go Outdated
@AnnaShaleva

Copy link
Copy Markdown
Member

Conflicts.

Comment thread pkg/core/interop/gas_price.go Outdated
Comment thread pkg/vm/vm.go Outdated
Comment thread pkg/vm/vm.go Outdated
Comment thread pkg/core/interop/gas_price.go Outdated
Comment thread pkg/core/interop/gas_price.go Outdated
Comment thread pkg/vm/vm.go Outdated
Comment thread pkg/vm/vm.go Outdated
Comment thread pkg/vm/vm.go Outdated
Comment thread go.mod Outdated
Comment thread pkg/core/fee/opcode.go Outdated

func OpcodeDynamic(base int64, op opcode.Opcode, args *vm.PriceArgs) int64 {
if dynamicCoefficients[op] == nil {
fmt.Println(op, args)

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.

Shouldn't be here.

Comment thread pkg/core/fee/opcode.go Outdated
if dynamicCoefficients[op] == nil {
fmt.Println(op, args)
}
return base * max(1, dynamicCoefficients[op](args)/factor)

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.

Shouldn't be like that. Calculate the overall price of all opcodes in the script and then divide the resulting sum by the factor.

Comment thread pkg/core/fee/opcode.go Outdated
Comment thread pkg/core/fee/opcode.go Outdated
Comment thread pkg/core/fee/opcode.go Outdated
}

func AppendGas(args *vm.PriceArgs) int64 {
return dotInts(appendW, []int64{int64(args.Refs), int64(args.NClonedItems), 1})

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.

Additional slice allocation on every opcode doesn't bring us anything good. Remove dotInts, use direct multiplication.

Comment thread pkg/vm/vm.go Outdated
for range n {
key := v.estack.popNoRef()
val := v.estack.popNoRef().value
numComparisons += m.Len()

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.

Is it m.Len() or the number of iterations over cycle? m.Len() may be different from the number of iterations in case if map has duplicated entries.

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.

I do not know how we can find out the number of comparisons if Add cannot return it #4202 (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.

It's just a question. OK, I think we're fine with m.Len() here.

Comment thread pkg/vm/vm.go Outdated
Comment on lines +1403 to +1480
v.estack.PushItem(t.Value().([]stackitem.MapElement)[index].Value.Dup())
m := t.Value().([]stackitem.MapElement)
item = m[i].Value.Dup()

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.

No need to declare m, useless variable.

Comment thread pkg/vm/vm.go Outdated
Comment on lines +1572 to +1576
r := v.refs
var (
item = v.estack.Pop()
n int
)

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.

Move r under var declaration.

Comment thread pkg/vm/vm.go Outdated

func (v *VM) cpValues(src iter.Seq[stackitem.Item], n int, isReferenced bool) []stackitem.Item {
arr := make([]stackitem.Item, 0, n)
func (v *VM) cpValues(src iter.Seq[stackitem.Item], n int, isReferenced bool) ([]stackitem.Item, int) {

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.

Doc for int return is needed.

Comment thread pkg/vm/vm.go
Comment thread pkg/core/fee/calculate.go
if scparser.IsSignatureContract(script) {
size += 67 + io.GetVarSize(script)
netFee += Opcode(base, opcode.PUSHDATA1, opcode.PUSHDATA1) + base*ECDSAVerifyPrice
netFee += Opcode(base, opcode.PUSHDATA1, opcode.PUSHDATA1)/factor + base*ECDSAVerifyPrice

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.

Two problems here:

fee.Opcode() behaviour change

The behaviour of Opcode() shouldn't be changed because it's an exported API that may be used by some external users.

I see two possible solutions:

I
  1. Divide the result of fee.Opcode by factor internally, inside fee.Opcode itself (it will return Datoshi then).
  2. Use new OpcodeV0 function as VM's PriceGetter for pre-Gorgon (it will return Datoshi*factor, but with old coefficients).
  3. Use new OpcodeV1 function as VM's PriceGetter for post-Gorgon (also will return `Datoshi*factor, but with new coefficients).
II
  1. Add an extra factor argument to Opcode. Use pre-Gorgon coefficients if factor is 0 and use post-Gorgon coefficients otherwise.

@roman-khimov, do you have other suggestions? Which one do you prefer?

fee.Calculate() becomes Gorgon-dependent.

The behaviour of fee.Calculate (and other similar and dependent APIs) becomes Gorgon-dependent.

I can suggest introducing fee.CalculateV0 (using pre-Gorgon coefficients and returning Datoshi*factor) and fee.CalculateV1 (using post-Gorgon coefficients, returning Datoshi*factor). Dependent functions will have to switch between two implementations by themselves based on Gorgon. It's also important to keep the original behaviour of fee.Calculate unchanged.

@roman-khimov also need your opinion on that.

Comment thread pkg/core/fee/opcode.go Outdated
const factor = 1000

// Opcode price weights used in dynamic fee formulas.
// All values are multiplied by vm.OpcodePriceMultiplier.

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.

Suggested change
// All values are multiplied by vm.OpcodePriceMultiplier.
// All values are multiplied by [vm.OpcodePriceMultiplier].

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.

Use it for every link in doc.

Comment thread pkg/core/fee/opcode.go
Comment thread pkg/core/fee/opcode.go Outdated
)

// OpcodeV1 returns opcode pricing used since Gorgon hardfork,
// supporting dynamic (input-dependent) costs.

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.

s/(input-dependent)/(parameter-dependent)

Comment thread pkg/core/fee/opcode.go Outdated
}
)

// OpcodeV1 returns opcode pricing used since Gorgon hardfork,

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.

returns opcode pricing

... in the unit of Datoshi*[vm.OpcodePriceMultiplier]

Comment thread pkg/core/native/invocation_test.go Outdated
opcode.PUSH4, // amount of args
opcode.PACK, // pack args
)
) / vm.OpcodePriceMultiplier

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 OK for the old version of Opcode to divide by vm.OpcodePriceMultiplier in the middle of the script calculation. However, it won't be OK for OpcodeV1; the division should happen after the final price is known and the final value should be rounded up to cover non-integer remnant.

Comment thread pkg/vm/stackitem/item.go Outdated
// Array fields are still copied by reference.
func (i *Struct) Clone() (*Struct, error) {
// Clone returns a Struct with all Struct fields copied by the value
// and number of cloned items. Array fields are still copied by reference.

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.

s/items/fields

... including the struct itself.

Comment thread pkg/vm/vm.go Outdated

// PriceArgs contains opcode-specific parameters used to calculate dynamic price.
type PriceArgs struct {
// Typ specifies type of stack item involved in opcode.

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.

s/stack item/[stackitem.Item]

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.

s/involved in opcode/which in most of the cases serves as an operand of the given opcode.

Comment thread pkg/vm/vm.go Outdated
}

// PriceArgs contains opcode-specific parameters used to calculate dynamic price.
type PriceArgs struct {

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.

s/PriceArgs/OpcodePriceArgs or something like that?

Just because vm.PriceArgs is too wide definition if referred from an external user package.

Comment thread pkg/vm/vm.go Outdated
Comment on lines +87 to +89
// Size is size of compound type, buffer or bytearray length,
// number slots or stack elements.
Size int

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.

Suggested change
// Size is size of compound type, buffer or bytearray length,
// number slots or stack elements.
Size int
// Size denotes one of the following:
// - the number of elements in the compound type (i.e. the length of Array or Struct, the number of key-value pairs in Map);
// - the length of Buffer or ByteArray;
// - the number of VM slot cells or stack elements involved into opcode handling.
Size int

@Turalchik we need to be specific and precise. Imagine external users using this parameter to calculate something.

And may be it's worth to rename Size to Length?

Comment thread pkg/core/fee/opcode.go Outdated
return dst
}

// OpcodeDynamic returns dynamic opcode prices used since Gorgon hardfork.

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.

Suggested change
// OpcodeDynamic returns dynamic opcode prices used since Gorgon hardfork.
// opcodeDynamic returns dynamic opcode prices used since Gorgon hardfork.

Comment thread pkg/vm/vm.go Outdated
var priceArgs *OpcodePriceArgs
defer func(canGetPrice bool) {
if canGetPrice {
v.gasConsumed += v.getPrice(op, parameter, priceArgs)

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.

For cases when panic occurs in the middle of opcode handling, priceArgs argument is not set yet. It means all parameters are set to 0, so the result will be equal to the free term. Is it expected behaviour?Related to https://github.com/neo-project/neo-vm/pull/571/changes#r3160408408.

Close #4043.

Signed-off-by: Tural Devrishev <tural@nspcc.ru>
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.

Dynamic opcode pricing

2 participants