Skip to content

JIT Costing: The Definitive PR#876

Open
mwaddip wants to merge 25 commits into
ergoplatform:developfrom
mwaddip:jit-costing-final
Open

JIT Costing: The Definitive PR#876
mwaddip wants to merge 25 commits into
ergoplatform:developfrom
mwaddip:jit-costing-final

Conversation

@mwaddip
Copy link
Copy Markdown

@mwaddip mwaddip commented Jun 3, 2026

Closes #193.

Ports JIT costing from sigmastate-interpreter (direct-tree-eval): each IR node
accumulates its Scala cost into the interpreter Context during evaluation, the
per-transaction cost limit is enforced cumulatively across inputs, and per-element
equality costs (DataValueComparer) and SigmaByteWriter serialize costs are
charged. A cost_parity test replays mainnet blocks 700000–700060 and asserts every
transaction's computed JIT cost equals the node's recorded cost (78 vectors).

Supersedes #854. Same logical change, re-sliced from #854's tangled history into
24 dependency-ordered, individually-green commits — each builds and passes the
ergotree-interpreter / ergotree-ir suites plus cargo clippy --all-features --all-targets -D warnings. One conformance fix is folded in: SigmaProp == SigmaProp
now charges per Scala equalSigmaBoolean (ProveDlog 174 / ProveDHTuple 690) instead of
the flat catch-all 3.

Relationship to #858 (lazy constant resolution)

Independent PR, overlapping in reduce_to_crypto / ConstPlaceholder handling — whichever
merges first, the other takes a mechanical rebase. This PR resolves segregated
ConstPlaceholder via ConstantPlaceholder::resolved (populated by
ErgoTree::proposition_for_cost_eval) and charges it JitCost(1); #858 introduces
Context.constants lazy resolution. Reconciliation either way: the ConstPlaceholder
arm ends up charging 1 JitCost and resolving via ctx.constants, dropping this
PR's resolved-field machinery as redundant. Worked reconciliation on the
ergo-node-integration branch (9094f693); full rebase recipe pinned on #858.

mwaddip and others added 24 commits June 3, 2026 22:29
Introduce the JIT cost accumulator and limit on the interpreter Context (jit_cost / jit_cost_limit fields; add_jit_cost / add_per_item_jit_cost / jit_cost_value / reset_jit_cost; CostLimitExceeded), wire the supporting cost_accum and error plumbing, and initialise the new fields in every Context constructor (arbitrary + wallet::signing::make_context). Remove the dead pre-costing costs.rs stub (zero consumers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge each node's Scala base cost (FixedCost) at eval time: the per-Expr arms (Const=5, Global=5, Context=1), the ~40 single-cost eval ops, GlobalVars (Height=26, Self/Outputs/Inputs/GroupGenerator=10, MinerPubKey=20), method_call, and ConcreteCollection=20.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge each builtin-type method's Scala FixedCost in its EVAL_FN: Context (dataInputs/headers/preHeader/lastBlockUtxoRootHash 15, selfBoxIndex/minerPubKey 20, getVarFromInput 10), Box (value 8, getReg 50, tokens 15), Header/PreHeader fields (10 each; Header.checkPow 700), GroupElement, AvlTree (15 each; updateOperations 45, updateDigest 40), and Global generic methods (groupGenerator/xor/fromBigEndianBytes 10, some/none 5, deserialize per-item 100/32/32).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge each BinOp's Scala per-kind cost and route equality through DataValueComparer (per-element comparison costs). Includes the folded SigmaProp equality conformance fix: SigmaProp == SigmaProp now charges per Scala equalSigmaBoolean (ProveDlog 174 / ProveDHTuple 690) instead of the flat catch-all 3, matching the JVM equalDataValues on a rare type-valid path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cost reduce_to_crypto: accumulate JIT cost as a delta from the caller's accumulator (cumulative across inputs, enabling per-tx limit enforcement), preserve ConstPlaceholders for cost eval via ErgoTree::proposition_for_cost_eval (+ Constant.resolved / Expr::resolve_placeholders), short-circuit trivial SigmaProp-constant trees (P2PK) at Scala's EvalSigmaPropConstant flat 50, and propagate the accumulated cost through the verifier (crypto_cost).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enforce the per-transaction JIT cost limit cumulatively across all transaction inputs: derive the cost limit from chain parameters, share one Context accumulator across the per-input reduce_to_crypto calls so their costs sum, and fail verification when the cumulative cost exceeds the limit. Threaded through tx_context (validate), parameters, ergo_transaction, and the ergo-lib-wasm binding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge Coll.indexOf its Scala cost: PerItemCost(20,10,2) over the iterations actually performed, plus the element-type equality cost per comparison via DataValueComparer::eq_with_cost. A bare == previously left both uncharged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce eval_lambda_1arg, the shared collection-HOF lambda helper that charges ADD_TO_ENV_COST=5 per binding, and route flatMap through it (charged once per input element). Also charge flatMap's output-length PerItemCost(60,10,8) over the flattened result, matching Scala flatMap_eval.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge ADD_TO_ENV_COST per value binding in Apply (FuncValue invocation) and BlockValue (val definitions), matching Scala's per-binding AddToEnvironment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Route the collection HOFs map/filter/fold/exists/forall through eval_lambda_1arg (ADD_TO_ENV charged per element) and charge their Scala per-iteration costs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Many/reverse/startsWith/endsWith/get)

Charge the Scala PerItemCost for the remaining Coll methods: zip, indices, patch, updated, updateMany, reverse, startsWith, endsWith, get.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge SubstConstants its Scala cost scaled by the number of template constants substituted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge Coll.slice its Scala cost over the requested (to - from) range.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge SigmaProp.propBytes its Scala cost scaled by the SigmaBoolean proposition node count (adds SigmaBoolean::size()).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge And and Blake2b256 their per-collection-element Scala costs, and charge boolean-constant collections N*5 per element to match the Exprs form. Adds empty-collection regression tests (and_empty, calc_blake2b256_empty, bool_constants_coll).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge the Scala FixedCost for the v6.0 numeric and UnsignedBigInt methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge Global.encodeNbits=25 / decodeNbits=50 (FixedCost) and powHit's PowHitCostKind formula 500 + (k+1) * (totalLen/128 + 1) * 7, with regression tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a SigmaByteWriter serialize-cost accumulator (enable_serialize_cost_tracking / serialize_cost / add_put_byte_cost / add_put_numeric_cost / add_put_chunk_cost) and charge the direct DataSerializer types: Boolean/Byte = PutByte, Short/Int/Long and all length prefixes = PutNumeric, String/Coll bytes = PutChunk, Opt tag = PutByte. Wire Global.serialize to meter the writer puts (StartWriterCost 10 + tracked put costs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge the serialize put-cost for GroupElement (PutChunk over GROUP_SIZE), SigmaBoolean/SigmaProp, and BigInt256/UnsignedBigInt256 sigma_serialize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge the serialize put-cost for ErgoBox: the register type-code prefix (TypeCode::sigma_serialize PutByte), creationHeight, value, and TxId, matching Scala's box serialization metering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge the AvlTree serialize put-cost (digest + flags + key/value-length fields) during Global.serialize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge the PutByte cost for the SType fast-path combined type-code arms (Coll/Nested/Option/Tuple-pair over primitives), with a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Charge the Header serialize put-cost by mirroring the JVM put sequence (HeaderWithoutPow + AutolykosSolution; Header lives in ergo-chain-types and can't reach the cost sink), reaching the blessed v6 Global.serialize[Header] = 333. Adds the cross-type serialize_charges_writer_costkinds regression test covering byte/numeric/coll (c17), GroupElement/UnsignedBigInt (c18), AvlTree (c20) and Header.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the ergo-lib cost_parity integration test replaying mainnet blocks 700000-700060 and asserting each transaction's computed JIT cost matches the node's recorded cost (78 vectors), pinning the full costing model to consensus. Includes the headers / transactions / tx_costs test vectors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scala NumericCastCostKind bills both SBigInt and SUnsignedBigInt at
JitCost(30) (others 10); Upcast/Downcast were billing UnsignedBigInt the
else-branch 10. Mirror the oracle so the JIT cost of a numeric cast to
UnsignedBigInt matches the JVM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

JIT costing

1 participant