fix(consensus): generalize ErgoTree method-resolution gates to the full per-version registry - #125
Conversation
…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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (10)
📝 WalkthroughWalkthroughThe PR generalizes the existing v6/EIP-50-only method gating to a broader "unresolved method" gate. ChangesUnresolved-method gate generalization
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
…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>
…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>
What
The node knew only the v6/EIP-50-only method ids (
is_v3_only_method). Scala'sdeserializeErgoTreeresolves everyMethodCall/PropertyCallagainst thetree-header version's registry — v5 for header version 0/1/2, v6 for 3+ — and
throws a method-resolution
ValidationExceptionfor any id absent from it. That setis 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:
Scala as
UnparsedErgoTree, so an off-curve group element decoded after thethrow 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).hard-rejected by Scala (the
ValidationExceptionis uncaught without a size bit towrap it). The node accepted it.
How
is_v5_method/is_v6_method/is_known_methodtoopcode/types.rs,EXTRACTED from the reference (
SMethod.fromIds, sigma-state 6.0.2) over the fullsingle-byte range — not transcribed.
method_registry_predicates_match_scala_oraclepins 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 invariantsis_v6 ∧ ¬is_v5 == is_v3_only_methodand
v5 ⊆ v6.!is_known_method(type, method, tree_version)in
parse.rs(the parser already carries the header version). The ErgoTree wrap layertherefore drops its
version < 3branch — the version split is applied once, in oneplace. All four quadrants
{pre-v3, v3+} × {v6-only, unknown}resolve correctly.check_v3_only_methods→check_resolvable_methods,using the new
find_unresolved_v5_method(sizeless ⟹ v0 ⟹ v5 registry). Same for thenested
SBox-constant sizeless gate insigma_value.rs.find_v3_only_method/EvalError::PreV3V6Method) is intentionally unchanged — eval-path semantics aredistinct from deserialize and out of scope.
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) inunreachable 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 duringdevelopment):
08279adb6a2add0702ff…ff(v0 has_size, unknown PropertyCall(106, 42), trailingoff-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 methodwith 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 agetUIntExactoverflow — both UNPARSED bytes=13 (Scala wraps at the method,never reaching the overflow). The same overflow without a preceding unknown
method (
08089add738080808008) THROWsArithmeticException: 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, andunresolved_method_wrap_survives_trailing_hard_error. The existing divergence-B(v6-only) behavior, the nested-
SBox-constant hard-reject, and byte-preservation areunchanged.
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
Refactor
Tests