fix(consensus): faithful UnparsedErgoTree — preserve bytes + eval-error - #118
Conversation
📝 WalkthroughWalkthroughIntroduces ChangesExpr::Unparsed Byte-Preserving Soft-Fork Wrapping
Sequence Diagram(s)sequenceDiagram
participant Caller
participant read_ergo_tree_tracking_wrap
participant determinable_root_type
participant unparsed_soft_fork_tree
participant write_ergo_tree
Caller->>read_ergo_tree_tracking_wrap: parse bytes
read_ergo_tree_tracking_wrap->>read_ergo_tree_tracking_wrap: record tree_start, capture full_tree_bytes
alt version > MAX or parse Err
read_ergo_tree_tracking_wrap->>unparsed_soft_fork_tree: full_tree_bytes
unparsed_soft_fork_tree-->>read_ergo_tree_tracking_wrap: ErgoTree { body: Expr::Unparsed(full_tree_bytes) }
else parse Ok
read_ergo_tree_tracking_wrap->>determinable_root_type: resolve root SigmaType
alt non-SSigmaProp root
read_ergo_tree_tracking_wrap->>unparsed_soft_fork_tree: full_tree_bytes
unparsed_soft_fork_tree-->>read_ergo_tree_tracking_wrap: ErgoTree { body: Expr::Unparsed(full_tree_bytes) }
else SSigmaProp or unresolvable
read_ergo_tree_tracking_wrap-->>Caller: parsed ErgoTree
end
end
read_ergo_tree_tracking_wrap-->>Caller: ErgoTree
Caller->>write_ergo_tree: re-serialize
alt body == Expr::Unparsed(raw)
write_ergo_tree-->>Caller: write raw bytes verbatim
else normal body
write_ergo_tree-->>Caller: serialize AST
end
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 |
A soft-fork-wrapped ErgoTree (Scala's `Left(UnparsedErgoTree(bytes, error))`) must behave two ways the node previously got wrong: 1. On spend it must FAIL, not reduce to true. Scala's `propositionFromErgoTree` returns `TrueSigmaProp` only when `isSoftFork(error)` holds — and `isSoftFork` defaults false (it is true only for a rule the active block extensions have replaced/changed via voting). The node models no such activation, so an unparsed tree must hard-error on evaluation. Reducing it to true was a spend-path accept-invalid. 2. It must re-serialize byte-identically. Scala preserves the wrapped tree's `propositionBytes` verbatim; the node was substituting a synthetic `Const(SBoolean, true)` placeholder body, so re-emission diverged from the wire bytes (the SANTA wire round-trip divergences). Represent the wrapped body as a new `Expr::Unparsed(Vec<u8>)` holding the full original tree bytes (header + size + body). `read_ergo_tree_tracking_wrap` captures those bytes at every wrap site; `write_ergo_tree` emits them verbatim; `eval_expr` returns `EvalError::UnparsedErgoTree`. The group elements collected before the wrap are still forwarded for the curve-check, matching Scala's deserialize-time obligation. The non-SigmaProp-root wrap check (`CheckDeserializedScriptIsSigmaProp`) now also covers a `ConstPlaceholder` root, resolving its type from the segregated constants table the way Scala's `ConstantPlaceholderSerializer.parse` does — a placeholder root pointing at a non-SigmaProp constant wraps just like an inline non-SigmaProp constant, instead of being accepted as a parsed proposition. Test fixtures that built a size-delimited tree from a non-SigmaProp root (`Const(SBoolean, true)` + has_size, or a `ConstPlaceholder` resolving to a non-SigmaProp constant) now correctly wrap to `Expr::Unparsed` on re-read — such a root is genuinely unspendable. Migrate them to a `SigmaProp` root, which is what a valid size-delimited proposition must be. The REST submit path's soft-fork reject now keys on the `Expr::Unparsed` body directly instead of the obsolete placeholder heuristic. Also pins a known adjacent divergence in the same GE-forwarding path (divergence B: an off-curve group element placed after a v6-only method in a size-flagged pre-v3 tree is curve-checked here but never reached by Scala's wrap-on-throw) via a documented test and a pointer comment. Its fix (parser-side sideband checkpointing) needs Scala oracle vectors and is deferred. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
232beb0 to
a04ec94
Compare
…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>
What
Makes a soft-fork-wrapped ErgoTree — Scala's
Left(UnparsedErgoTree(bytes, error))—behave faithfully on the two axes the node previously got wrong.
1. Eval must hard-ERROR, not reduce to
true.Scala's
Interpreter.propositionFromErgoTreereturnsTrueSigmaProponly whenisSoftFork(error)holds, andisSoftForkdefaults false (true only for a rule theactive block's validation settings have replaced/changed via voting). This node models no
such soft-fork activation, so an unparsed tree must error on evaluation. Reducing it to
truewas a spend-path accept-invalid.2. It must re-serialize BYTE-IDENTICALLY.
Scala preserves the wrapped tree's
propositionBytesverbatim. The node was substituting asynthetic
Const(SBoolean, true)placeholder body, so re-emission diverged from the wirebytes — the SANTA wire round-trip divergences.
How
Expr::Unparsed(Vec<u8>)whole-tree body holds the full original tree bytes(header + size + body).
read_ergo_tree_tracking_wrapcapturesdata_slice(tree_start, tree_end)at every wrap site;write_ergo_treeemits themverbatim;
eval_exprreturns the newEvalError::UnparsedErgoTree.curve-check, matching Scala curve-checking GEs as it deserializes (before producing the
UnparsedErgoTree).CheckDeserializedScriptIsSigmaPropparity widened: the non-SigmaProp-root wrap check nowalso resolves a
ConstPlaceholderroot against the segregated constants table (as Scala'sConstantPlaceholderSerializer.parsetypes it), so a placeholder root pointing at anon-SigmaProp constant wraps like an inline non-SigmaProp constant.
(
Const(SBoolean, true)+ has_size, or aConstPlaceholder→ non-SigmaProp constant) nowcorrectly wrap to
Expr::Unparsedon re-read — such a root is genuinely unspendable.Migrated to a
SigmaProproot. The REST submit reject now keys on theExpr::Unparsedbody directly instead of the obsolete placeholder heuristic.
Review
Reviewed source-level against the Scala oracle (reference/ergo-core) before opening. The core
was confirmed correct:
isSoftForkdefault-false → eval-error is right; wrapped bytes matchScala's
UnparsedErgoTree(bytes)path; GE forwarding correct; every newExpr::Unparsedmatch arm hard-errors/rejects. The
ConstPlaceholderroot completeness above was addressedper that review.
A closely-related divergence in the same
read_ergo_tree_tracking_wrappath — group-elementordering past a v6 method (a size-delimited pre-v3 tree wrapping at the method, where group
elements after it must not be curve-checked) — is fixed in the follow-up #119.
Known pre-existing follow-ups (NOT introduced here; out of scope for this PR)
The wrap-trigger classification (which the byte/eval representation here does not change)
has two pre-existing gaps worth a focused follow-up:
version > MAX_SUPPORTED_TREE_VERSIONis activation-unaware. The node always wraps afuture-version size-delimited tree opaquely; Scala only skip-accepts when
activatedScriptVersionis beyond the supported max, elseVersionContext.withVersionsraises and
deserializeErgoTreehard-rejects (not wraps).ValidationException. Non-validationfailures (e.g. a
ConstantPlaceholderindex overflow →ReadError::ValueTooLarge, a ScalaSerializerException) are wrapped here but hard-rejected by Scala at tx parse.Both are pre-existing on
mainand need a careful Scala exception-class / version-activationaudit; tracked separately.
Test plan
New/updated tests:
size_flagged_non_sigmaprop_root_wraps_as_unparsed(now assertsExpr::Unparsed+ byte-identity),size_flagged_const_placeholder_non_sigmaprop_root_wraps_as_unparsed,unparsed_ergo_tree_body_eval_errors_not_true, plus the fixture migrations.🤖 Generated with Claude Code