Conversation
`NipopowProof::has_valid_connections` was too strict compared to JVM:
it required every adjacent prefix entry to connect to its IMMEDIATE
sorted-by-height neighbour via interlink or parent_id. JVM's
`NipopowProof.scala` (`hasValidConnections`, lines ~128-148) instead
allows each `next` entry to connect to ANY of the preceding
`useLastEpochs + 3` entries (a window of up to 11 with the mainnet
default of 8).
The tolerance is load-bearing for real-world proofs: JVM-built proofs
include continuous-mode difficulty-recalculation headers and
naturally-skipped entries from sparse-superlevel walks. Those entries
do not directly connect to the immediately previous sorted entry but
do connect to a nearby earlier neighbour. The Rust verifier had been
silently rejecting such proofs since `2b69b16a` (the original
`is_better_than` impl), but it went unnoticed until sigma-rust was
deployed as a verifier against live JVM peers.
Changes:
- Add `use_last_epochs: u32` to `NipopowAlgos` (Rust analog of JVM
`chainSettings.useLastEpochs`). Default `8`, matching mainnet and
testnet `application.conf`. Manual `Default` impl preserves the
default-derive call sites in `ergo-chain-generation` and the
proptest `Arbitrary` impl.
- Rewrite `has_valid_connections` to walk a `[max(0, idx - lookback), idx)`
window for each prefix entry, exactly matching the Scala range
semantics. Suffix-tail check is unchanged.
- Add three regression tests:
1. `accepts_skipped_prefix_entry` — synthesizes a chain whose
middle prefix entry skips its immediate predecessor but connects
to a 2-back interlink; tolerant verifier must accept.
2. `rejects_too_far_skip` — same shape, but with `use_last_epochs`
squeezed to `0` so the only valid backward link sits outside the
lookback window; verifier must still reject. Proves the fix is
not a blanket accept-all.
3. `rejects_broken_suffix_tail` — sanity check that the
suffix-side parent_id chain is still strictly enforced.
Adds two tests to ergo-nipopow/src/nipopow_algos.rs covering NipopowAlgos::pack_interlinks: - pack_interlinks_keys_are_first_occurrence_positions: asserts that the ExtensionKV key[1] for each duplicate-run encodes the input-vector index of the run's first element (JVM Ergo encoding), not a sequential distinct-group counter. Test currently FAILS against the pre-fix pack_interlinks which emits sequential ix_distinct_block_ids. - pack_interlinks_empty_returns_empty: pins the no-panic behavior on empty input. Verified empirically against mainnet block 1784124 nipopow proof (captured 2026-05-13): the JVM-compat key encoding makes 11/11 leaf hashes in interlinksProof.indices match against an external TS port. The pre-fix sequential encoding matches only 2/11. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ition Replaces the sequential `ix_distinct_block_ids` counter with the input-vector index of each duplicate-run's first element. Matches JVM Ergo's `NipopowAlgos.packInterlinks` (ergoplatform/ergo, `src/main/scala/org/ergoplatform/modifiers/history/popow/NipopowAlgos.scala`) which encodes ExtensionKV keys as `[INTERLINK_VECTOR_PREFIX, position_of_first_occurrence_in_interlinks_vector]`. ## The bug `unpack_interlinks` filters by `key[0] == INTERLINK_VECTOR_PREFIX` only and never reads `key[1]`, so sigma-rust round-trips its own buggy output internally. The divergence is observable only at the Merkle-leaf hash level: a kv-leaf's hash depends on the full kv-bytes including `key[1]`, so a sigma-rust-packed leaf hashes differently from a JVM-packed leaf. Concrete impact: - `PoPowHeader::check_interlinks_proof` FAILS on real mainnet proofs because the expected Merkle root it computes (via the buggy pack) doesn't match the root the proof's walk-up reaches (built from JVM-packed leaves). - `NipopowProof::is_better_than` rejects all real mainnet proofs as "invalid" because it calls `is_valid()` → `has_valid_proofs()` → `check_interlinks_proof`, which fails for any block with at least one duplicate-run starting past position 1 (≈ every mainnet block). ## Validation Verified against mainnet block 1784124 nipopow proof (captured 2026-05-13 via ergo-node-rust): with this fix, all 11 leaf hashes in the proof's interlinksProof.indices match the JVM-generated leaves exactly. Pre-fix matches only 2 (positions 0 and 1 happen to align because distinct_ix=0,1 also = first_pos=0,1 for the first two runs). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sigma-state restricts tuples to exactly 2 elements: `Tuple.eval` rejects `items.length != 2` with "Invalid tuple" (values.scala: "in v5.0 version we support only tuples of 2 elements to be equivalent with v4.x"). The check is unconditional (no version gate); sigma-state 6.0.3 is JIT-only and evaluates every height through it. sigma-rust models tuples as flat N-ary (`TupleItems = BoundedVec<2,255>`) and its `Tuple` eval had no arity check, so it accepted arity>=3 tuples the JVM rejects — a consensus accept/reject divergence: sigma-rust would accept a spend the JVM rejects (crafter-constructible). Surfaced by the santa eval fixture `tuple_triple_bool_byte_short` (tree 0086030101020703a413): JVM rejects "Invalid tuple"; sigma-rust accepted Tuple[true, 7, 1234]. Mirror the JVM: reject `items.len() != 2` at eval. No mainnet block can carry an arity>=3 tuple (the JVM rejects them), so this cannot regress historical sync. Regression test added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
powHit computes the Autolykos-2 PoW hit value (a big integer), so its
result type is UnsignedBigInt — matching Scala SGlobalMethods.powHit and
the interpreter's POW_HIT_EVAL_FN, which already yields
Value::UnsignedBigInt. The descriptor mis-declared SBoolean, so
coll.map(x => Global.powHit(..)).exists((u: UnsignedBigInt) => ..) failed
parse-time type-checking ("Invalid condition tpe"), wedging a full node
off testnet at canonical block 28,474.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TransactionContext::validate summed input and output box values with plain u64 sums, so an overflowing aggregate panicked in debug builds and wrapped in release builds before BoxValue::new could reject it — the input check misfired and the output sum had no check at all. The reference implementation sums with Math.addExact over longs (ErgoTransaction.validateStateful), trapping every addition. Sum with i64 try_fold/checked_add — the same idiom validate_stateless already uses — returning InputSumOverflow/OutputSumOverflow. This also drops the spurious lower-bound check the BoxValue::new construction applied to the aggregate (the reference bounds boxes, not the sum). Closes #881. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion The JVM reads the Option DATA tag through getOption (sigma Extensions.getOption): `if (tag != 0) Some(getValue) else None`. Our sigma-ser get_option mapped only tag 1 to Some and silently returned None on any other nonzero tag WITHOUT consuming the value bytes, desyncing the stream — a v3 tree carrying an SOption constant with DATA tag 0x02 failed to parse where the JVM evaluates it to Some(5). Fix the shared reader helper to mirror the convention; put_option still writes 0/1, so the write side is unchanged. The only other caller (ergo-p2p peer-spec parsing) aligns with the same scorex reader convention. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ContextExtension keys are a signed Byte JVM-side. ErgoLikeContext .toSigmaContext builds the contextVars as new Array(maxKey+1) indexed by the signed key (ErgoLikeContext.scala:140-146), so a self-box extension carrying a key >= 0x80 (signed-negative) crashes context construction (NegativeArraySizeException / ArrayIndexOutOfBoundsException) BEFORE any bytecode runs — the spend never validates. The crash is on the key's PRESENCE, independent of whether the script reads it. sigma-rust stores extension keys as unsigned u8 and resolves them lazily at GetVar, so it would otherwise ACCEPT a >= 0x80 key where the JVM rejects — a consensus fork on the attacker-supplied spending extension (the node feeds the spending input's extension into ctx.extension before reduce_to_crypto). Reject any self-extension key outside 0..=127 at the reduction boundary, mirroring the construction-time crash as a clean rejection. This is the top-level self extension only; the per-input getVarFromInput path masks the id with & 0xff by design and is untouched, and a GetVar of a >= 0x80 id with the key absent still resolves to None. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… gate
The JVM `HeaderWithoutPow` reads `version` as a signed `Byte` and gates the
`unparsedBytes` region on `version > 1`. sigma-rust compared `version` as
`u8`, so a version byte 0x80 (-128 signed) was seen as 128 > 1: the parser
consumed `unparsedBytes`, shifting the AutolykosSolution parse and yielding a
different `minerPk` than the JVM (which skips the region) -- a deserialization
fork vs sigma-state 6.0.3.
Compare the signed interpretation `(version as i8) > 1` at the parse and
serialize gates. Versions 1/2/3 are positive, so signed == unsigned; only
version >= 0x80 changes, matching the JVM. The two `version > 1` sites in the
arbitrary proptest generator are left unchanged: it emits only versions {1,2},
and one is a v2-field nulling gate (`!= 1`), not an unparsedBytes gate. Adds a
regression test on the 0x80/0x7f witness headers (minerPk infinity vs real).
fix: Option DATA parser treats any nonzero tag as Some, per JVM getOption
fix: read Header version as a signed Byte at the unparsedBytes gate (JVM parity)
Reject self ContextExtension keys >= 0x80 (JVM context-construction parity)
fix: reject non-pair tuples at eval to match sigma-state consensus
fix(ergotree-ir): Global.powHit returns UnsignedBigInt, not Boolean
…checkType) The JVM checkTypes each Tuple item (values.scala:801/804) via SType.isValueOfType, which rejects a tuple type of arity != 2 (and a function type of arity != 1) with "Unsupported tuple type" (SType.scala:200-205). sigma-rust models flat N-ary tuples (TupleItems 2..=255), so an arity-3 tuple carried as a constant (the item of a valid pair) evaluated where the JVM rejects — a consensus accept/reject divergence. Add check_value_type mirroring isValueOfType's reachable rejections and call it at the Tuple-eval items. Placeholder constants are substituted into the body before eval on this branch, so the segregated case reduces to the same inline Tuple-item case; the eni cherry-pick additionally guards ConstantPlaceholder eval (where eni resolves placeholders lazily). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the JVM order: checkType before evaluating each item (values.scala:801/804). Aborts earlier if the type is unsupported rather than doing the eval work first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix: reject unsupported (arity != 2) tuple-typed values at eval (JVM checkType)
fix: checked ERG summation in stateful transaction validation
…okback ergo-nipopow: tolerate skipped prefix entries in has_valid_connections
…compat fix(nipopow): pack_interlinks key encoding — use first-occurrence position
…roof ergo-nipopow: validate the initial proof before selection
`ergo-nipopow/src/lib.rs` sets `#![deny(clippy::expect_used)]` crate-wide, so `cargo clippy --all-targets -- -D warnings` fails on v0.30.0 at the two `curr_first_pos.try_into().expect(..)` casts in `pack_interlinks`. Annotate both sites, matching the existing site-level `#[allow(clippy::unwrap_used)]` usage elsewhere in this file. No behaviour change — this only restores the style gate.
`cargo fmt --check` fails on v0.30.0: develop is clean, so the drift arrived with the nipopow merges. Formatting only, no behaviour change.
style: restore fmt and clippy gates on v0.30.0 (ergo-nipopow)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.