Skip to content

fix: variable-length Context.headers; lastBlockUtxoRoot as its own context input#889

Open
mwaddip wants to merge 2 commits into
ergoplatform:developfrom
mwaddip:fix/context-variable-headers
Open

fix: variable-length Context.headers; lastBlockUtxoRoot as its own context input#889
mwaddip wants to merge 2 commits into
ergoplatform:developfrom
mwaddip:fix/context-variable-headers

Conversation

@mwaddip

@mwaddip mwaddip commented Jun 7, 2026

Copy link
Copy Markdown

JVM ErgoLikeContext.headers is variable-length — empty is legal, and at height h ≤ 10 headerChainBack yields h−1 headers — with lastBlockUtxoRoot as its own context input (CContext returns it directly, never headers(0)). sigma-rust pinned [Header; 10] and derived the root from headers[0]: a node bridging the fixed type must pad, so CONTEXT.headers.size / high-index reads in a chain's first blocks diverge from the JVM — a silent value fork (exposure: testnet from-genesis validation).

Fix: Context.headersBoundedVec<Header, 0, 10> + standalone Context.last_block_utxo_root; SDK headers → BoundedVec<Header, 1, 10> (signing always has a chain tip); bindings relax ==101..=10. Regression tests drive JVM-blessed vectors against the truly-empty context.

mwaddip and others added 2 commits June 7, 2026 14:37
The JVM models lastBlockUtxoRoot as its own ErgoLikeContext input and
CContext.LastBlockUtxoRootHash returns it directly; deriving it from
headers(0).stateRoot only coincides when headers are non-empty (the JVM
requires digest agreement then, ErgoLikeContext.scala:85) and has no
value for an empty header chain. Add Context.last_block_utxo_root and
return it from the eval; make_context derives it from the newest header,
which is exactly the JVM-required relationship.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The JVM models ErgoLikeContext.headers as a variable-length Coll[Header]:
empty is explicitly legal, and at block height h <= 10 the window that
headerChainBack gathers stops at genesis, so scripts see h - 1 headers.
sigma-rust's fixed [Header; 10] cannot represent that; a node bridging it
must pad, and a script reading CONTEXT.headers.size or a high index in
the first blocks of a chain gets a different answer than the JVM — a
silent value fork (practical exposure: testnet from-genesis validation).

Context.headers becomes ContextHeaders (BoundedVec 0..=10), and the SDK
ErgoStateContext.headers becomes BoundedVec 1..=10 — signing always has
a chain tip, and make_context still derives the root from the newest
header. The wasm/python/C constructors relax accordingly: every existing
caller passing 10 headers keeps working; passing fewer becomes possible.

Regression tests drive the two JVM-blessed Context.properties vectors
(empty headers -> empty Coll[Header]; LastBlockUtxoRootHash on an
empty-headers context -> the standalone root field).

Co-Authored-By: Claude Opus 4.8 <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.

1 participant