Skip to content

vm: add dynamic opcode pricing#4087

Open
Turalchik wants to merge 1 commit intomasterfrom
dynamic-opcode-pricing
Open

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

Conversation

@Turalchik
Copy link
Copy Markdown
Contributor

Close #4043.

@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
Copy link
Copy Markdown

codecov Bot commented Nov 27, 2025

Codecov Report

❌ Patch coverage is 68.24818% with 87 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.44%. Comparing base (6c2e5a6) to head (8deae27).

Files with missing lines Patch % Lines
pkg/core/fee/opcode.go 7.22% 77 Missing ⚠️
pkg/core/interop/context.go 33.33% 2 Missing and 2 partials ⚠️
pkg/vm/vm.go 97.74% 2 Missing and 2 partials ⚠️
pkg/core/interop/gas_price.go 33.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4087      +/-   ##
==========================================
- Coverage   83.51%   83.44%   -0.07%     
==========================================
  Files         359      359              
  Lines       43910    44130     +220     
==========================================
+ Hits        36670    36824     +154     
- Misses       5437     5497      +60     
- Partials     1803     1809       +6     

☔ View full report in Codecov by Sentry.
📢 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 17 times, most recently from 0de3721 to 5196698 Compare December 22, 2025 07:50
Copy link
Copy Markdown
Member

@AnnaShaleva AnnaShaleva left a comment

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

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 on lines +708 to +711
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.

Can't do this directly. We need to preserve the old behaviour for pre-Gorgon and introduce new behaviour starting from Gorgon.

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?

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