feat(api): /script/p2sAddress + /script/p2shAddress compile routes (M6) - #165
Conversation
…EST surface) M6 recon §5 flagged parse.rs's expr()/type_() recursive descent as recursion-unbounded: fine for trusted CLI/test input, a stack-overflow DoS surface once the /script/p2sAddress + /script/p2shAddress REST routes expose it to untrusted source text. Add a single Cursor::depth counter shared by both expr() and type_() (each wrapped so every recursive call in the file is guarded, not just the top-level entry), capped at MAX_PARSE_DEPTH = 128 — conservative and non-oracle-pinned (the compiler's own limits are explicitly not consensus-critical), set above ergo-ser's consensus MAX_EXPR_DEPTH = 110. Recursion audit of the M4/M5 transform passes (fold/lower/inline/cse/ isproven/tuple.rs): all six are call-stack-recursive 1:1 tree walks over the already-depth-bounded typed AST (no Vec-based explicit stack anywhere), but none amplifies depth beyond what the parser already accepted, so the single parse-time cap transitively bounds them too — no independent per-module guards needed. Also removes an untracked, explicitly "do NOT commit" scratch probe binary (ergo-compiler/examples/probe.rs) that was failing `cargo clippy --all-targets` (clone_on_copy on GroupElement) and blocking the gate.
D-E3 documented the gap: the only pre-M6 SigmaProp-family env constant (EnvValue::SigmaProp(String)) is an opaque label emit rejects with UnsupportedNode — no env entry point produced a REAL, emittable SigmaProp. Scala's ScriptApiRoute.keysToEnv injects each wallet ProveDlog pubkey as myPubKey_N for /script/p2sAddress + p2shAddress, so M6 needs one. Add EnvValue::ProveDlog([u8; 33]) + its lift arm: on-curve check via the same decompress_to_affine_hex path as GroupElement (D-T5 policy — rejects off-curve AND identity), producing ConstPayload::ProveDlog + SType::SSigmaProp — the identical shape binder.rs's PK(...) rule already produces, so binder/typer/emit need no other change. Ledgered as D-E4 (distinct from, not a fix to, D-E3's still-opaque SigmaProp label).
The two compile-requiring members of Scala's ScriptApiRoute this node
previously omitted (utils.rs served only the decode-only addressToTree
/ addressToBytes members — "this node ships no compiler" is no longer
true after M1-M6). Wires ergo_compiler::compile up through a new
ergo-api/src/script.rs module: CompileRequestDto {source, treeVersion},
a wallet-address -> ScriptEnv builder mirroring Scala's keysToEnv
(myPubKey_N -> ProveDlog(pk), capped at loadMaxKeys=100), and two
handlers reading CompileResult.p2s_address / .p2sh_address into
{"address": ...}.
Mounted as a NEW Router<(NetworkPrefix, Arc<dyn WalletAdmin>)>
sub-router beside utils_routes (server.rs), mirroring the
miner_stats_routes tuple-state precedent. PUBLIC/ungated, matching
Scala's ScriptApiRoute (no withAuth) — even though it reads wallet
pubkeys server-side, same as the two existing decode-only /script/*
routes. Rewrites the now-false "ships no compiler" doc comments in
utils.rs.
An empty or errored wallet-address read degrades to an empty env
(a keyless script still compiles) rather than a 500 — the real
ergo-node WalletAdmin::addresses() backend never actually returns Err
regardless of lock state, confirmed by reading
ergo-node/src/node/wallet_bridge/commands/admin.rs; a genuine Err from
an alternate WalletAdmin implementation is surfaced honestly (Locked/
Uninitialized -> 400, else 500) rather than invented.
Tests: script.rs unit tests for the env builder (injection, cap,
empty-list, invalid-address); ergo-api/tests/script_compile_routes.rs
end-to-end route tests (self-consistent against direct
ergo_compiler::compile calls — no live Scala node needed) covering the
happy path, wallet-pubkey env injection, keyless-on-empty-wallet,
compile-error 400 mapping, and reachability/no-auth on the REAL
merged router via router_with_wallet.
|
Warning Review limit reached
Next review available in: 46 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR adds compile-requiring script endpoints ( ChangesCompile-requiring script API routes
Compiler ProveDlog env value and parser depth guard
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant p2s_address_handler
participant compile_address_response
participant WalletAdmin
participant ergo_compiler
Client->>p2s_address_handler: POST /script/p2sAddress (source, treeVersion)
p2s_address_handler->>compile_address_response: forward request
compile_address_response->>WalletAdmin: addresses()
WalletAdmin-->>compile_address_response: tracked addresses
compile_address_response->>compile_address_response: build_env (myPubKey_N -> ProveDlog)
compile_address_response->>ergo_compiler: compile(source, env, tree_version)
ergo_compiler-->>compile_address_response: compiled result
compile_address_response-->>Client: JSON { p2s_address }
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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 |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
ergo-compiler/src/env.rs (1)
200-213: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider extracting shared on-curve validation.
The
GroupElement(Lines 184-194) andProveDlogarms perform an identical validate-then-wrap pattern viadecompress_to_affine_hex. A small helper (e.g.fn validated_curve_bytes(bytes: &[u8;33]) -> Result<[u8;33], GroupElementError>) would remove the duplication, but with only two call sites the benefit is marginal.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ergo-compiler/src/env.rs` around lines 200 - 213, The GroupElement and ProveDlog branches in EnvValue both repeat the same validate-then-wrap logic with decompress_to_affine_hex, so extract that shared on-curve check into a small helper (for example, a validated_curve_bytes-style function) and have both arms call it before constructing the TypedExpr::Constant payload. Keep the existing rejection behavior and return types aligned with the current GroupElement and ProveDlog paths so the helper centralizes validation without changing semantics.ergo-compiler/src/parse.rs (1)
468-479: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider extracting the duplicated depth-guard wrapper.
type_/type_implandexpr/expr_implimplement the identical increment-check-decrement pattern. A shared helper would prevent the two guards from drifting if the logic ever changes (e.g., adding telemetry, adjusting the error position captured).♻️ Proposed shared helper
+fn with_depth_guard<T>( + c: &mut Cursor, + f: impl FnOnce(&mut Cursor) -> Result<T, ParseError>, +) -> Result<T, ParseError> { + c.depth += 1; + if c.depth > MAX_PARSE_DEPTH { + let pos = c.peek().start; + let depth = c.depth; + c.depth -= 1; + return Err(ParseError::TooDeep { pos, depth }); + } + let result = f(c); + c.depth -= 1; + result +} + fn type_(c: &mut Cursor, raw_entry: bool) -> Result<SType, ParseError> { - c.depth += 1; - if c.depth > MAX_PARSE_DEPTH { - let pos = c.peek().start; - let depth = c.depth; - c.depth -= 1; - return Err(ParseError::TooDeep { pos, depth }); - } - let result = type_impl(c, raw_entry); - c.depth -= 1; - result + with_depth_guard(c, |c| type_impl(c, raw_entry)) }Also applies to: 959-970
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ergo-compiler/src/parse.rs` around lines 468 - 479, Extract the repeated depth-guard pattern used by type_/type_impl and expr/expr_impl into a shared helper so the increment, MAX_PARSE_DEPTH check, error capture, and decrement live in one place. Update type_ and expr to delegate their depth handling to that helper while still calling type_impl and expr_impl for the actual parsing work. Keep the helper generic enough to preserve the existing ParseError::TooDeep behavior and to avoid the two wrappers drifting if the depth logic changes later.ergo-api/src/script.rs (1)
84-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNo test exercises the
wallet_error_responseLocked/Uninitialized/Other branches.Both the unit tests here and
ergo-api/tests/script_compile_routes.rs'sStubAdmin::addresses()always returnOk, so the 400-vs-500 mapping logic inwallet_error_responseis never actually run by any test.♻️ Suggested addition
#[test] fn wallet_error_response_locked_maps_to_bad_request() { let resp = wallet_error_response(WalletAdminError::Locked); assert_eq!(resp.status(), axum::http::StatusCode::BAD_REQUEST); } #[test] fn wallet_error_response_other_maps_to_internal_error() { // pick a non-Locked/Uninitialized variant let resp = wallet_error_response(WalletAdminError::/* other variant */); assert_eq!(resp.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR); }Also applies to: 142-187
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ergo-api/src/script.rs` around lines 84 - 99, Add test coverage for the wallet_error_response mapping logic in script.rs, since the current tests only exercise Ok paths and never verify the Locked/Uninitialized versus other error branches. Create focused unit tests for wallet_error_response that pass WalletAdminError::Locked, WalletAdminError::Uninitialized, and at least one non-ready variant to assert BAD_REQUEST vs INTERNAL_SERVER_ERROR. Also update the script_compile_routes.rs StubAdmin::addresses() test path so it can return an error and drive those branches through the route layer, ensuring the status mapping is actually exercised by tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@ergo-api/src/script.rs`:
- Around line 84-99: Add test coverage for the wallet_error_response mapping
logic in script.rs, since the current tests only exercise Ok paths and never
verify the Locked/Uninitialized versus other error branches. Create focused unit
tests for wallet_error_response that pass WalletAdminError::Locked,
WalletAdminError::Uninitialized, and at least one non-ready variant to assert
BAD_REQUEST vs INTERNAL_SERVER_ERROR. Also update the script_compile_routes.rs
StubAdmin::addresses() test path so it can return an error and drive those
branches through the route layer, ensuring the status mapping is actually
exercised by tests.
In `@ergo-compiler/src/env.rs`:
- Around line 200-213: The GroupElement and ProveDlog branches in EnvValue both
repeat the same validate-then-wrap logic with decompress_to_affine_hex, so
extract that shared on-curve check into a small helper (for example, a
validated_curve_bytes-style function) and have both arms call it before
constructing the TypedExpr::Constant payload. Keep the existing rejection
behavior and return types aligned with the current GroupElement and ProveDlog
paths so the helper centralizes validation without changing semantics.
In `@ergo-compiler/src/parse.rs`:
- Around line 468-479: Extract the repeated depth-guard pattern used by
type_/type_impl and expr/expr_impl into a shared helper so the increment,
MAX_PARSE_DEPTH check, error capture, and decrement live in one place. Update
type_ and expr to delegate their depth handling to that helper while still
calling type_impl and expr_impl for the actual parsing work. Keep the helper
generic enough to preserve the existing ParseError::TooDeep behavior and to
avoid the two wrappers drifting if the depth logic changes later.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1e16f662-b04d-4198-9c75-d5c600ba4f0e
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (10)
ergo-api/Cargo.tomlergo-api/src/lib.rsergo-api/src/script.rsergo-api/src/server.rsergo-api/src/utils.rsergo-api/tests/script_compile_routes.rsergo-compiler/src/env.rsergo-compiler/src/error.rsergo-compiler/src/lib.rsergo-compiler/src/parse.rs
- parse.rs: extract the duplicated increment/check/decrement depth-guard into a shared with_depth_guard helper (type_/expr_ can no longer drift); all four depth-guard tests unchanged and green. - script.rs: cover the wallet_error_response mapping — unit tests for Locked/Uninitialized→400 and Internal→500, plus a route-level test driving a Locked wallet through /script/p2sAddress (400 + bad-request envelope, never a 500). StubAdmin gains a fail_locked switch. - env.rs helper extraction SKIPPED: two call sites, CodeRabbit's own grading calls the benefit marginal — the duplication is a straight-line validate-then-wrap with no drift hazard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8xw
|
Addressed the review:
|
…ain) #165 (M6) merged to main added ParseError::TooDeep; rebasing M7 onto current main makes shift_err's match non-exhaustive. Shift TooDeep's pos like the other arms. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
#166) * feat(compiler): M7 ContractParser + ContractTemplate assembly Adds the @contract template layer above the expression grammar, mirroring Scala's sigmastate.lang.ContractParser being a sibling of SigmaParser (sigma-state 6.0.2), plus the SigmaTemplateCompiler.compile/assemble pipeline. Why: contracts declare doc metadata + named parameters that compile to segregated ConstantPlaceholder slots; this is the parser/metadata layer M1 deliberately deferred. Reuses parse()/parse_type()/typer/graph-build verbatim so the body grammar and compile pipeline are never duplicated. - contract_parse.rs: Docs + Signature sub-parsers + parse_contract; new AST (ContractDoc/ParameterDoc/ContractParam/ContractSignature/ ParsedContractTemplate). Type reuses parse_type; body reuses parse; default is a restricted literal-only parse. Faithful @returns-with-text / non-star-line / non-literal-default rejects. - typecheck.rs: typecheck_contract_body threads a name->SType param env into the existing typer env-lookup path (empty ScriptEnv, no new typer logic). - emit.rs: Scope gains a placeholder env; a param identifier emits ConstantPlaceholder(index) (opcode 0x73). Empty on the normal path. - tree.rs: extract the graph-building pipeline into graph_build(), shared by compile() and the contract assembler so both stay byte-identical. - contract_template.rs: ContractTemplate + compile_contract; declaration-order placeholder indices for <=4 params, canonical ContractTemplate.serializer wire form. >=5 params return ContractError::TooManyParamsForOrdering (TODO(M7-hashmap-order)) rather than a silently-wrong tree. - token.rs: expose is_id_start/is_id_char to the crate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * test(compiler): M7 ct oracle verb + contract corpus + byte-exact parity gate Why: grade the assembled ContractTemplate against the real Scala reference so the <=4-param declaration-order claim is proven, not asserted. - TyperOracle.scala: new `ct` verb runs SigmaTemplateCompiler(NET).compile and prints ContractTemplate.serializer.toBytes as hex. - test-vectors/ergoscript/contract/: 12 hand-authored sources + committed contract_seed.json (verbatim oracle captures, sigma-state 6.0.2 / Scala 2.12.21, tree_version=3, testnet). - contract_template_parity.rs: oracle OK (<=4 params) => byte-identical serialize(); oracle OK (>=5 params) => our deliberate TooManyParamsForOrdering deferral; oracle REJECT => we reject. Result: 10 <=4-param vectors byte-exact vs the oracle; the 5+ HashMap-order port is cleanly deferred (flagged, never mis-emitted). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * chore: remove committed sdd report from tree (gitignored scratch, local-path leak) .superpowers/ is session scratch — reports never ship in the public tree (and this one carried local absolute paths). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * fix(compiler): M7 ContractParser CodeRabbit findings — string-literal delimiters + dup-param reject Three findings on PR #166's @contract ContractParser, each verified against the sigma-state 6.0.2 reference and the `ct` JVM oracle before fixing. Finding 1 (Major) — string-literal-aware delimiters. Scala's `param.rep(1, ",")` and `params = "(" ~ ... ~ ")"` read each `ExprLiteral` with the real `Literals.String` grammar (\"-escape aware), so a `,`/`)` inside a string-literal default is content, not structural. Oracle ACCEPTs `s: String = "a,b"`, `= ")"`, `= "\""`, and a multi-param mix; the Rust splitter/paren-matcher were byte-blind and rejected them (reject-valid gap). Fixed by tracking string/escape state (StrScan) in both the top-level comma splitter and the closing-`)` matcher. All four now compile byte-identical to the oracle (new corpus vectors). Finding 2 (Major) — duplicate parameter names. Scala does NOT last-wins: `ContractTemplate.validate()` runs `require(!paramNames.contains(p.name), ...)` and throws IllegalArgumentException. Oracle REJECTs `def f(a: Int, a: Long)`. The Rust path silently accepted it (two Parameters sharing a name). Added a post-typecheck uniqueness check mirroring the Scala require (ContractError::DuplicateParamName). Finding 3 (Minor) — param-type parse errors reported positions relative to the sliced type string, not source coordinates (the body path already used shift_err). Shifted param-type `parse_type` errors by the same offset (base + colon + 1). Corpus stays byte-exact (5 new vectors: 4 byte-exact ACCEPT, 1 REJECT). Gate green: fmt/clippy -D warnings/test --workspace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * fix(compiler): cover ParseError::TooDeep in shift_err (M6 merged to main) #165 (M6) merged to main added ParseError::TooDeep; rebasing M7 onto current main makes shift_err's match non-exhaustive. Shift TooDeep's pos like the other arms. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx --------- Co-authored-by: arkadianet <rkadias@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Bump workspace version to 0.5.2 and promote the changelog: the complete v1 product API (#168-#185, #188), shadow validation as a production mode (#193-#195), the operator observability wave (#187, #190, #192, #194), two live accept-invalid consensus fixes (#176, #179), ErgoScript compiler byte-parity completion (#165-#167, #175), and the #160-#163 sync/recovery fixes. Full workspace gate run on the merge result. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
What this is
The M6 follow-up to #164: wires the merged byte-parity compiler into the node's REST surface as the two compile-requiring
/script/*endpoints the Scala node has and this node previously omitted —POST /script/p2sAddressandPOST /script/p2shAddress— plus the DoS hardening the REST exposure requires.Three commits:
parse.rsdepth guard (ParseError::TooDeep, cap 128 > ergo-ser's consensusMAX_EXPR_DEPTH=110) — source-text depth upper-bounds every downstream structure, so one cap at parse covers the whole pipeline. Audited all six transform passes: call-stack-recursive but none deepens beyond source depth. Deepest real corpus contract still parses.EnvValue::ProveDlog([u8; 33])— the one missing env entry point for a real, emittableSigmaPropconstant (mirrors theGroupElementarm: on-curve check,ConstPayload::ProveDlog,SType::SSigmaProp). Downstream (binder/typer/emit) already supported it viaPK(...).ergo-api/src/script.rs— the two handlers on aRouter<(NetworkPrefix, Arc<dyn WalletAdmin>)>tuple-state sub-router; wallet-addresses →myPubKey_Nenv (.take(100)= Scala'sloadMaxKeys); every compile failure → the standard 400bad_requestenvelope (Scala parity: one BadRequest for all phases). Route tests with a stubWalletAdmin+ committed oracle vectors.Security note (deliberate, operator-approved)
Scala's
ScriptApiRoutecarries no auth, so these routes are public for parity — even though the server-side env bakes wallet pubkeys into the returned P2S address (decodable back to those pubkeys). This was reviewed and explicitly accepted to mirror Scala rather than gate behindapi_key; an operator who wants them gated can front them at the proxy. (The native v1 API design keeps wallet-keyed compile OUT of the public surface — this compat behavior stays frozen here only.)Test plan
Route tests:
ergo-api/tests/script_compile_routes.rs(envelope, tier, empty-wallet, error mapping). Compile parity itself is guarded by the merged #164 gate (110/110).🤖 Generated with Claude Code
https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
Summary by CodeRabbit
New Features
p2sandp2shaddresses from submitted script source.Bug Fixes