Skip to content

fix(consensus): generalize ErgoTree method-resolution gates to the full per-version registry - #125

Merged
arkadianet merged 1 commit into
mainfrom
fix/unknown-method-ge-ordering
Jun 21, 2026
Merged

fix(consensus): generalize ErgoTree method-resolution gates to the full per-version registry#125
arkadianet merged 1 commit into
mainfrom
fix/unknown-method-ge-ordering

Conversation

@arkadianet

@arkadianet arkadianet commented Jun 21, 2026

Copy link
Copy Markdown
Owner

What

The node knew only the v6/EIP-50-only method ids (is_v3_only_method). Scala's
deserializeErgoTree resolves every MethodCall/PropertyCall against the
tree-header version's registry — v5 for header version 0/1/2, v6 for 3+ — and
throws a method-resolution ValidationException for any id absent from it. That set
is larger than "v6-only": it also includes any genuinely unknown / future
(typeId, methodId) pair. Because the node parses any id as a generic method node,
unknown ids slipped past the gates and diverged from Scala two ways:

  • reject-valid (has_size): a size-flagged tree carrying an unknown method wraps in
    Scala as UnparsedErgoTree, so an off-curve group element decoded after the
    throw point is never curve-checked. The node curve-checked it and rejected the
    block. This now also covers v3 trees (a v3 unknown method wraps; a v3 v6-only
    method stays valid — previously the wrap path only fired for version < 3).
  • accept-invalid (sizeless): a sizeless v0 tree carrying an unknown method is
    hard-rejected by Scala (the ValidationException is uncaught without a size bit to
    wrap it). The node accepted it.

How

  • Add is_v5_method / is_v6_method / is_known_method to opcode/types.rs,
    EXTRACTED from the reference (SMethod.fromIds, sigma-state 6.0.2) over the full
    single-byte range — not transcribed. method_registry_predicates_match_scala_oracle
    pins both predicates against a checked-in oracle vector
    (test-vectors/scala/sigma/method_registry_v5_v6.txt) for all 65 536
    (typeId, methodId) pairs, and asserts the invariants is_v6 ∧ ¬is_v5 == is_v3_only_method
    and v5 ⊆ v6.
  • The group-element checkpoint is keyed on !is_known_method(type, method, tree_version)
    in parse.rs (the parser already carries the header version). The ErgoTree wrap layer
    therefore drops its version < 3 branch — the version split is applied once, in one
    place. All four quadrants {pre-v3, v3+} × {v6-only, unknown} resolve correctly.
  • The sizeless box-deserialize gate check_v3_only_methodscheck_resolvable_methods,
    using the new find_unresolved_v5_method (sizeless ⟹ v0 ⟹ v5 registry). Same for the
    nested SBox-constant sizeless gate in sigma_value.rs.
  • The evaluator v6-only spend-path gate (find_v3_only_method /
    EvalError::PreV3V6Method) is intentionally unchanged — eval-path semantics are
    distinct from deserialize and out of scope.
  • Once the parser has passed an unresolved method, Scala throws-and-wraps at that
    method and never reads the rest of the body. The node parses the whole body
    generically, so a hard parse error (overflow / depth / nested HardReject) in
    unreachable trailing bytes must not override the wrap — the wrap decision is made
    before the parse-result match. To keep that scoped, a nested box-constant script
    (its own deserialization scope, hard-rejected on its own when sizeless) saves and
    restores the outer reader's checkpoint so its methods don't trigger the outer wrap.

Validation

Oracle-validated against sigma-state 6.0.2 (deserializeErgoTree, run during
development):

  • 08279adb6a2add0702ff…ff (v0 has_size, unknown PropertyCall (106, 42), trailing
    off-curve GE) → UNPARSED bytes=41 — wrap, GE dropped.

  • 0b279adb6a2add0702ff…ff (the same body at a v3 header) → UNPARSED bytes=41
    — the v3 case the fix newly handles.

  • 00db6a2add (sizeless v0, unknown method) → THROW SerializerException: "The method
    with code 42 doesn't declared in the type SGlobalMethods."
    — hard reject (was
    accept-invalid).

  • 080b9adb6a2add738080808008 (v0) / 0b0b9adb6a2add738080808008 (v3) —
    Plus(unknown PropertyCall, ConstPlaceholder(0x80000000)), the placeholder a
    getUIntExact overflow — both UNPARSED bytes=13 (Scala wraps at the method,
    never reaching the overflow). The same overflow without a preceding unknown
    method (08089add738080808008) THROWs ArithmeticException: Int overflow
    hard reject.

These outcomes are reproduced by unknown_method_size_flagged_wraps_and_drops_trailing_ge,
check_resolvable_methods_rejects_sizeless_unknown_method, and
unresolved_method_wrap_survives_trailing_hard_error. The existing divergence-B
(v6-only) behavior, the nested-SBox-constant hard-reject, and byte-preservation are
unchanged.

Test plan

cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace      # 4404 passed

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor

    • Improved method resolution validation in ErgoTree parsing to detect unresolved methods more comprehensively, replacing previous version-specific checks with broader compatibility assessment.
    • Enhanced checkpoint tracking for method resolution during deserialization.
  • Tests

    • Added oracle-parity tests for method registry predicates to ensure alignment with Scala implementation across protocol versions.

…ll per-version registry

The node knew only the v6/EIP-50-only method ids (`is_v3_only_method`). Scala's
`deserializeErgoTree` resolves EVERY `MethodCall`/`PropertyCall` against the
tree-header version's registry (v5 for header 0/1/2, v6 for 3+) and throws a
method-resolution `ValidationException` for any id absent from it — a v6-only id
in a pre-v3 tree OR a genuinely unknown/future `(typeId, methodId)` pair. Because
the node parses any id as a generic method node, unknown ids slipped past both
gates, diverging from Scala two ways:

- reject-valid (has_size): a size-flagged tree with an unknown method wraps in
  Scala as `UnparsedErgoTree`, so an off-curve group element decoded AFTER the
  throw is never curve-checked. The node curve-checked it and rejected the block.
  This now also covers v3 trees (a v3 unknown method wraps; a v3 v6-only method
  stays valid).
- accept-invalid (sizeless): a sizeless v0 tree with an unknown method is
  hard-rejected by Scala (the `ValidationException` is uncaught without a size
  bit). The node accepted it.

Add `is_v5_method` / `is_v6_method` / `is_known_method`, EXTRACTED from the
reference (`SMethod.fromIds`, sigma-state 6.0.2) over the full single-byte range,
not transcribed; `method_registry_predicates_match_scala_oracle` pins them against
the checked-in oracle vector for all 65 536 pairs. The group-element checkpoint is
now keyed on `!is_known_method(..., tree_version)` (parser knows the header
version), so the ErgoTree wrap layer drops the version branch; the sizeless gate
(`check_v3_only_methods` -> `check_resolvable_methods`) uses `find_unresolved_v5_method`.
The evaluator's v6-only spend-path gate (`find_v3_only_method`) is unchanged.

Once a tree has passed an unresolved method, Scala throws-and-wraps there and never
reads the rest of the body, so the wrap decision is made BEFORE the parse-result
match — a hard parse error (overflow / depth / nested HardReject) in unreachable
trailing bytes cannot override the wrap. To keep that scoped, a nested box-constant
script (its own deserialization scope, hard-rejected on its own when sizeless) saves
and restores the outer reader's checkpoint so its methods do not trigger the wrap.

Oracle-validated (sigma-state 6.0.2): v0 and v3 size-flagged unknown-method trees
are UNPARSED (wrap, trailing off-curve GE dropped); the sizeless v0 unknown-method
tree THROWs; an unknown method followed by a `getUIntExact` overflow is UNPARSED at
v0 and v3, while the same overflow without a preceding unknown method THROWs.

Test plan:
  cargo fmt --all -- --check
  cargo clippy --workspace --all-targets --all-features -- -D warnings
  cargo test --workspace      # 4405 passed

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dc5ff8b2-b65f-445e-bbd8-571eb3e94fce

📥 Commits

Reviewing files that changed from the base of the PR and between d2ec70f and 355c84f.

📒 Files selected for processing (10)
  • ergo-primitives/src/reader.rs
  • ergo-ser/src/ergo_box.rs
  • ergo-ser/src/ergo_tree.rs
  • ergo-ser/src/opcode/mod.rs
  • ergo-ser/src/opcode/parse.rs
  • ergo-ser/src/opcode/tests.rs
  • ergo-ser/src/opcode/types.rs
  • ergo-ser/src/sigma_value.rs
  • ergo-validation/tests/tree_version_oracle_parity.rs
  • test-vectors/scala/sigma/method_registry_v5_v6.txt

📝 Walkthrough

Walkthrough

The PR generalizes the existing v6/EIP-50-only method gating to a broader "unresolved method" gate. VlqReader's v6 checkpoint sideband is renamed to unresolved_method_checkpoint. New version-aware registry predicates (is_v5_method, is_v6_method, is_known_method) and a generic find_method_matching walker replace the prior v3-only-specific logic. check_v3_only_methods is replaced with check_resolvable_methods across all box and tree parsing paths. A Scala-derived method registry oracle test vector is added with corresponding predicate and parity tests.

Changes

Unresolved-method gate generalization

Layer / File(s) Summary
VlqReader unresolved-method checkpoint sideband
ergo-primitives/src/reader.rs
Renames v6_method_checkpoint field to unresolved_method_checkpoint, replaces v6 checkpoint public API (mark_v6_method_checkpoint, v6_method_checkpoint) with unresolved-method equivalents, and updates take_group_elements to clear the new field.
Method registry predicates and generic walker
ergo-ser/src/opcode/types.rs, ergo-ser/src/opcode/mod.rs
Adds is_v5_method and is_v6_method match-table predicates; updates is_known_method to delegate based on tree version; introduces generic find_method_matching, refactors find_v3_only_method, and adds find_unresolved_v5_method; expands pub use re-exports.
Parse-time unresolved-method checkpoint marking
ergo-ser/src/opcode/parse.rs
Replaces is_v3_only_method/mark_v6_method_checkpoint with is_known_method/mark_unresolved_method_checkpoint in PropertyCall and MethodCall parse arms.
ErgoTree gate and size-delimited wrap logic
ergo-ser/src/ergo_tree.rs
Replaces check_v3_only_methods with check_resolvable_methods (any method unresolved in v5 registry for pre-v3 sizeless trees); generalizes size-delimited wrap to use the unresolved-method checkpoint for GE forwarding cutoff; updates doc comments.
Box parsing gate callsite updates
ergo-ser/src/ergo_box.rs, ergo-ser/src/sigma_value.rs
Replaces all four check_v3_only_methods calls in box parsing paths with check_resolvable_methods; updates parse_sizeless_inner_box_script to save/restore the unresolved-method checkpoint and use the v5-registry error message.
Oracle test vector, predicate tests, and integration test updates
test-vectors/scala/sigma/method_registry_v5_v6.txt, ergo-ser/src/opcode/tests.rs, ergo-ser/src/ergo_tree.rs, ergo-validation/tests/tree_version_oracle_parity.rs
Adds Scala method registry oracle test vector; adds exhaustive oracle-parity and find_unresolved_v5_method tests; updates ErgoTree tests for unknown-method wrap/GE-dropping scenarios; updates tree-version oracle parity integration test to use check_resolvable_methods.

Sequence Diagram(s)

sequenceDiagram
  participant BoxParser
  participant ErgoTreeParser
  participant OpcodeParser as OpcodeParser (parse_expr)
  participant VlqReader
  participant Gate as check_resolvable_methods

  BoxParser->>ErgoTreeParser: parse ErgoTree bytes
  ErgoTreeParser->>OpcodeParser: parse_body (size-delimited)
  OpcodeParser->>VlqReader: mark_unresolved_method_checkpoint() on unknown (type_id, method_id)
  OpcodeParser-->>ErgoTreeParser: Err or partial Expr
  ErgoTreeParser->>VlqReader: unresolved_method_checkpoint()
  ErgoTreeParser->>VlqReader: take_group_elements() up to checkpoint
  ErgoTreeParser-->>BoxParser: UnparsedErgoTree (with GE prefix)
  BoxParser->>Gate: check_resolvable_methods(&ergo_tree)
  Gate->>Gate: find_unresolved_v5_method (sizeless + version < 3 only)
  Gate-->>BoxParser: Ok or InvalidData error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • arkadianet/ergo#116: Introduced the original is_v3_only_method/find_v3_only_method logic and mark_v6_method_checkpoint that this PR directly generalizes and replaces.
  • arkadianet/ergo#119: Introduced the v6-specific VlqReader sideband checkpoint and the read_ergo_tree_tracking_wrap GE-forwarding cutoff logic that this PR renames and generalizes to unresolved_method_checkpoint.
  • arkadianet/ergo#123: Modifies the same read_ergo_tree_tracking_wrap wrap/Unparsed decision path and v6-checkpoint-based wrapping logic that this PR refactors to use the unresolved-method checkpoint.

Poem

🐇 Hopping past the v6 gate,
No method unknown shall frustrate!
The registry speaks — v5 or v6 clear,
Unresolved paths are caught right here.
With oracle vectors and tests aligned,
A sharper gate is now defined! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: generalizing ErgoTree method-resolution gates from v6-only to the full per-version registry, which is the primary objective across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/unknown-method-ge-ordering

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@arkadianet
arkadianet merged commit e4ade4c into main Jun 21, 2026
8 checks passed
@arkadianet
arkadianet deleted the fix/unknown-method-ge-ordering branch June 21, 2026 17:14
arkadianet added a commit that referenced this pull request Jun 21, 2026
…lease cut (#126)

0.4.3 was version-cut at #117, but #106#114 and #118#125 landed at the same
version without a CHANGELOG entry — so [0.4.3] documented only #115/#116/#117
while the deployed 0.4.3 binary actually contains all 18 PRs since the v0.4.2
cut. Backfills the section to match what shipped:

- New ### Added subsection for the native /api/v1/wallet surface (#112, #113,
  #114).
- ### Fixed now covers the signed-byte parity fixes (#106, #107), the
  Coll-equality cost short-circuit (#108), EIP-27 re-emission enforcement
  (#109 + #111, merged — the P0 fork fix, now across block/mempool/mining), and
  the full v6/EIP-50 + ErgoTree wire-deserialization cluster (#118#125).
- Broadened the release intro to name the EIP-27 enforcement and the native
  wallet surface alongside the v6/EIP-50 conformance work.
- Added the missing (#117) reference to the box-deserialize entry.

Docs only; no code or behavior change. Entries were drafted from each PR's own
commit/description and accuracy-reviewed (e.g. the EIP-27 soak figure is the
corrected 172 reward-box burns, not the repudiated 1,173).

Co-authored-by: arkadianet <rkadias@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
arkadianet added a commit that referenced this pull request Jun 22, 2026
…box-layer gates (#128)

The JVM serde oracle (`ErgoSerdeOracle.scala`) deserialized WITHOUT a
`VersionContext`, so it ran at the default activatedVersion=1 — whose `withVersions`
require short-circuits, meaning a tree whose header version exceeds the activated
version was NEVER rejected. Against the node (activatedVersion=3 = mainnet 6.0.2,
which rejects tree version > activated per #120) every future-version (v4+) tree
showed as a false ACCEPT/REJECT divergence — pure Phase-2 triage noise.

Wrap each deserialize in `VersionContext.withVersions(3, 0)` (the tree-version arg is
overridden from the header). Paired node-side fix: `ergo_tree_verdict` applied only
`check_header_size_bit`; add `check_tree_version_supported` (#120) and
`check_resolvable_methods` (#125) so the standalone `ergo_tree` differential models
the same consensus reject the real box-parse path applies — otherwise the oracle fix
would just flip the false divergence to the other direction.

Verified: the v4 tree `9caefdca01…` now REJECTs in the oracle (was ACCEPT); a Phase-2
`ergo_tree` corpus run shows the version-4 mismatch class eliminated. Tooling only
(the difftest harness/oracle), no node consensus change.

Co-authored-by: arkadianet <rkadias@users.noreply.github.com>
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.

1 participant