chore(ci): fix two pre-existing main CI blockers (cargo-deny wildcard + b4 byte-parity fixture) - #4
Conversation
…e-parity) Two failures predate the audit PRs and block every open PR's CI: 1. cargo-deny `wildcards = "deny"` tripped on internal `ergo-*` path/workspace deps (no version => implicit `*`). Add `allow-wildcard-paths = true` so the ban still rejects EXTERNAL `*` requirements but exempts path-only members. 2. `b4_byte_parity_*` referenced a gitignored fixture, so a clean checkout had no committed byte-parity coverage. Split into `run_byte_parity` helper; the default test now spans three COMMITTED corpora (genesis 1..200, modern 205000..205200, a 700000-era block) for diverse contract shapes, and the broad gitignored 700K-range corpus moves behind an `#[ignore]`'d companion. Codex-reviewed: SAFE.
|
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 (1)
📝 WalkthroughWalkthroughThis PR updates the bans config to allow wildcard handling for path/workspace-member deps, refactors config tests to use NamedTempFile-backed temp TOML helpers and propagates them across tests, and extracts a reusable ChangesConfiguration and Test Infrastructure
🎯 3 (Moderate) | ⏱️ ~20 minutes
🚥 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 |
…lelism `cargo test` runs cases as parallel threads in one process. The config fixtures derived temp-file names from `process::id()` + `SystemTime` nanos, but a microsecond-grained clock (macOS) hands two threads that reach the path builder in the same tick an identical name; their `fs::write`s then interleave into corrupt TOML and the load assertions panic. Linux's finer clock hid this, so only the macOS CI lane failed. Derive uniqueness from a monotonic `AtomicU64` instead of the clock via a single `unique_temp_toml(tag)` helper, and route every fixture writer through it. Verified with `config::tests --test-threads=16` looped 20x. Codex-reviewed: SAFE.
…afe) Supersedes the monotonic-counter fix from the previous commit with the crate's existing idiom. `ergo-node` already standardizes on `tempfile` (api_bridge, mem_csv, mem_marker), so route every config fixture through `tempfile::NamedTempFile`: - O_EXCL creation makes parallel-thread name collisions impossible regardless of clock resolution (the macOS CI failure), without a hand-rolled counter. - RAII drop removes the file even when an assertion unwinds, killing the 75 manual `remove_file` calls and the per-`minimal_cli(None)` temp-file leak the old helper left behind. `minimal_cli` is now generic over `AsRef<Path>` so the 77 `Some(&path)` call sites are unchanged; `None` callers bind an explicit `default_toml()` guard. Net -74 LOC. 102 config tests pass; looped 20x at --test-threads=16 with zero leaked temp files. Codex-reviewed: SAFE.
|
Upgraded the config-test fix from a monotonic-counter name to |
* docs(fuzz): interface contracts + known-bug catalog for fuzz-differential harness Foundation for the continuous fuzz-differential harness (Rust node vs Scala reference/ergo-core 6.0.2). Fixes the sidecar RPC / replay-driver I/O / generator-output / divergence-record contracts before any parallel build, and commits the 25-entry known-bug rediscovery catalog that gates the generators. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(fuzz): commit streamed-replay reproducibility pins from :9053 Height -> (headerId, stateRoot) pins for the differential replay driver. Makes a retired block range reproducible with zero committed block bytes: the driver asserts the archival node still serves the pinned headerId at each height before applying, and diffs Rust root_digest() against the pinned stateRoot. Covers the hermetic genesis seed (1-5) plus the known-gnarly incident heights (1805523 adproofs, 1808895 register stall, 1818476 indexer degrade, etc.). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(difftest): structure-aware SER generators + coverage gate Add `ergo-difftest/src/gen/`: structure-aware, grammar-position generators for the five SER surfaces (ergo_tree, constant, ergo_box_candidate, transaction, header), replacing the weak byte-mutation baseline for the highest-bug-density wire surface. Each generator runs two modes: on-manifold (build valid structs, serialize via the real ergo-ser writers) and adversarial-assembly (hand-place values the writers never emit). A FeatureSet records which adversarial features each output touched; declared_vocabulary + Coverage make a campaign's bug-surface coverage measurable, and gen_structured_at makes (seed,iter, surface) reproducible. Features map 1:1 to known SER bugs and are all reachable: header version 0x80..0xFF (#8), tree version/size/cseg bits + sizeless non-SigmaProp root (#17/#9/#25), declared size != body len (#19), FunDef nTpeArgs>=0x80 (#14), ill-formed-UTF-8 STypeVar (#1), VLQ > i32::MAX (#20), SUnsignedBigInt in a pre-v3 tree (#21), compact Relation2 bool pair (#12), v6-typed register (#5), off-curve group element (#4), empty outputs / 0-amount token (#23). Wire-in: run_structured_campaign feeds gen output through the same hermetic no-panic + fixed-point invariants and records the coverage union; a `--structured` CLI flag prints the per-surface coverage report and can feed the oracle differential. tests/gen_coverage.rs asserts full declared-vocab coverage per surface, determinism, on-manifold accept rate, no hermetic bugs, and that each adversarial feature lands on its intended verdict. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(difftest): integrate known-bug re-injection gate (Slice 5) Manifest (25 bugs), 9 apply-clean re-injection patches, reinject_gate.sh runner, and a hermetic --check-canonical flag. Kept the foundation's canonical docs over the agent's origin/main-based reconstruction. Merged the --check-canonical CLI flag alongside --structured. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(fuzz): findings + acceptance methodology + triage queue Honest signal assessment after running the generators through the re-injection gate + live JVM oracle. Proven: coverage 16/16; #12 rediscovery via clean-vs-patched Canonical delta (356->1321). Surfaced (not papered): (A) bare-ergo_tree over-reports because node validates in layers vs JVM's monolithic serializer -> acceptance must be per-surface + differential, not absolute count; (B) the oracle campaign reports non-reproducing divergences -> Slice 3 minimize+repro-verify is load-bearing. Escalated TRIAGE-001 (ValUse undefined-id: node Accept / JVM Reject) for a human consensus-truth call — not resolved here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(fuzz): correct findings — no signal-integrity bug; reduce surface is clean Root-caused the divergence output with systematic debugging + campaign instrumentation. CORRECTIONS to the prior findings doc: - WITHDRAWN 'non-reproducing divergences' — was my triage error (repro'd the node's canonical-OUTPUT field 080208d3 instead of the actual INPUT 080108d3). Findings reproduce exactly via input_hex. - The bare ergo_tree ~24% divergence floor is PARSE-SURFACE ARTIFACTS (benign canonical writer diffs [node retains original bytes in consensus], deferred GE curve-check, ValUse-undefined) — ALL agree on the consensus-complete reduce surface. - The reduce surface is CLEAN: 0 divergences/1500 on clean HEAD → trustworthy consensus differential. This is where consensus bugs should be hunted. - TRIAGE-001 downgraded (agrees on reduce; not a consensus bug). Acceptance gate methodology (per-surface + differential) STANDS and is sharper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(difftest): eval-rich SIGMA generators for the reduce surface (Slice 2c) Add sigma_expr generator emitting well-typed ErgoTree bodies that reduce non-trivially against the consensus-complete reduce oracle surface (SELF at 1M nanoErg, activated v3). Vocabulary: sigma props, boolean logic, Int/Long/ BigInt arithmetic, comparisons, collection ops, context accessors, registers/ tuples/options, and the eval/cost bug edges: atLeast (#13), early-vs-late Coll equality (#15), token-collection equality (#16), deserialize nodes (#3). reduce clean baseline = 0 divergences / 3000 (seed 1). Re-injection patches prove the generator rediscovers each mapped bug via a clean-vs-patched delta on reduce: #13 AcceptReject (P:d3|1481 vs Reject), #15 Canonical (P:d2|45 vs 43), #16 Canonical (P:d2|168 vs 60). #3 reported as a coverage gap (the reduce surface bypasses reductionWithDeserialize on both sides). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(difftest): streamed archival replay driver (Slice 1b) New `ergo-difftest --bin replay`: pulls full blocks from a live Scala archival node (:9053) one at a time over plain HTTP/1.1, applies each in-process via validate_full_block_parallel + StateStore::apply_block (forced full validation), and diffs the Rust root_digest() against the Scala-committed stateRoot. JSONL divergence stream + summary; exits non-zero on divergence or pin mismatch. Verifies covered heights by hash against replay-pins.json. This makes the immutable chain the oracle — the mechanism to retire committed block fixtures. Proof: --from 1 --to 200 -> {blocks:200, tx_total:200, divergences:0, pins_verified:5} against a live mainnet node. The driver FOUND a real Rust-vs-Scala bug on block 3: ergo-rest-json decodes the Autolykos v1 pow `d` field as signed two's-complement, but Scala serializes it unsigned (asUnsignedByteArray). Per operator decision the production fix is DEFERRED (see gitignored dev-docs/fuzz-differential/autolykos-v1-d-signed-vs- unsigned-bug.md); production ergo-rest-json is left UNTOUCHED. The driver works around it locally + byte-exact in correct_v1_pow_d (strip the spurious sign byte), clearly documented as removable once the production fix lands. Only non-difftest change is a lint-only cfg_attr(allow(dead_code)) on ergo-state PersistJob (the field is read only under test-helpers; ergo-difftest now builds ergo-state as a dep). No consensus logic touched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(difftest/slice3): minimize + classify + auto-file divergence pipeline Slice 3 of the fuzz-differential harness. Three additions: * `minimize.rs` — deterministic ddmin-style minimizer: chunk removal, truncation, single-byte deletion, fixed-point loop. `minimize_divergence` builds the predicate from `diff` (same surface + kind + verdict class), re-verifies minimized result. * `regressions.rs` — `DivergenceRecord` (§4 schema), `Triage` (Pending / KnownArtifact), `classify` (parse-surface reconciles-on-reduce rule), `auto_file` (sha256-addressed JSON + QUEUE.md Pending-only gate), `classify_and_file` pipeline. * CLI `--minimize [--regressions-dir D]`: after campaign, minimize+classify+file each unique divergence; `--repro --minimize --surface <s>` for single inputs. * `tests/minimize.rs` (9 hermetic tests): shrinks to [0xAB], correctness invariant, determinism, fixed-point, empty reduction, noop-on-unsatisfied, record JSON round-trip, QUEUE gate (Pending queued / KnownArtifact not), idempotency. Gate: fmt clean, clippy -D warnings clean, all 35 tests pass (16 unit + 9 gen_coverage + 9 minimize + 1 selftest). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(difftest): gitignore generated regressions/ directories Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(difftest): set default-run=difftest so bare `cargo run` still resolves Adding the replay binary made `cargo run -p ergo-difftest` ambiguous; pin the default to difftest so the documented `cargo run -p ergo-difftest -- ...` invocations keep working. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(difftest/slice1a): validate + verify_avl differential surfaces + avl_frame wire Adds two new JVM-oracle differential surfaces (Slice 1a): * `validate` — stateless structural TX validity (mirrors Scala `ErgoTransaction.statelessValidity()`; no UTXO/chain state). Rust: `validate_structural` + `read_transaction`. JVM: `ErgoTransactionSerializer.parseBytes` + `.statelessValidity()`. * `verify_avl` — AVL+ batch-proof verification twin of `ergo_sigma::avl::AvlVerifier`; targets bug #6 (valid-but-wrong proof → `ergo_avltree_rust` panics at op-time while Scala fails closed). Clean: both sides REJECT (guard catches panic). Patched (re-injection): Rust `Outcome::Bug` vs JVM REJECT. Rust: `AvlVerifier` via `avl_frame::AvlFrame::decode`. JVM: `CAvlTreeVerifier` (via reflection; constructor is `private[eval]` in Scala source despite `public` in bytecode). New shared module `ergo-difftest/src/avl_frame.rs` defines the wire layout (startingDigest‖keyLen‖valueLenOpt‖proofLen‖proof‖ opCount‖[opTag‖key‖(val)?]*) used by both Rust and Scala, with encode/decode + 7 unit tests including the trigger-generation test that emits the bug #6 hex. Re-injection patch: `ergo-difftest/known_bugs/patches/avl-verifier-panic.patch` removes `catch_unwind` from `AvlVerifier::guarded()`. Manifest entry for bug #6 (`avl-verifier-panic`), surface `verify_avl`, class `panic`, `wire_reachable=true`, trigger hex pinned. Gate: fmt clean, clippy clean (-D warnings), all 43 tests pass. JVM oracle: compiles, answers `ergo_tree`/`validate`/`verify_avl`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(difftest): persistent CI/nightly fuzz harness — fuzz_one + cargo-fuzz scaffold + coverage gate - ergo-difftest/src/fuzz.rs: stable fuzz_one(surface, data) entry point (real logic on stable; panics on Outcome::Bug → libFuzzer crash signal; silent on unknown surface; 5 unit tests: valid/garbage/unknown/all-surfaces/empty) - difftest --min-coverage <ratio>: machine-checkable coverage gate exits non-zero when union ratio < threshold; CI uses 0.80 (50k iters hits 1.000) - ergo-difftest/fuzz/: cargo-fuzz scaffold detached from workspace via [workspace] in fuzz/Cargo.toml (proven by clippy --workspace clean); 6 targets (ergo_tree, constant, ergo_box_candidate, transaction, header, sigma_expr) each a 3-line shim calling fuzz_one - ergo-difftest/fuzz/corpus/: 17 committed seed files from real mainnet vectors (failing_tree_*.hex, fee_proposition, height-1 header, genesis tx, recent box) + hand-crafted constant seeds - .github/workflows/ci.yml: +difftest job (stable, gating) — 50k iters + min-coverage 0.80 + fuzz_one unit tests on every PR/push to master - .github/workflows/fuzz.yml: nightly scheduled job (02:00 UTC) — 2M-iter structured campaign + corpus mutation run; NOT gating Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(test-vectors): curate mainnet fixtures — retire 3 dead-weight files + manifest Retire 3 committed fixtures with ZERO consumers anywhere (verified across .rs/.toml/.sh/.md): input_boxes_205000_205200.json (2.1MB), ergotrees_700000_700200.json (0.6MB), boxes_recent_range.json (0.9MB). 101MB -> 97MB committed mainnet, zero coverage loss. Add FIXTURES.md — the curated keep-list mapping every remaining fixture to the CI-run key test that needs it + why it can't shrink. KEY FINDING: the committed ranges are NOT redundant bloat; each large file backs a distinct CI test with exact count assertions (e.g. chain_validate_1_10000's first multi-tx block is at height 3355, so a 1-1000 sibling loses unique coverage), and the streamed replay driver ADDS deep coverage but can't run in CI. So retirement beyond dead weight is a real CI-coverage trade — documented as optional, not taken. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(test-vectors): retire bulk contiguous ranges -> replay driver; curate to key uses Per the curation principle (committed CI fixtures cover each tx/script TYPE + each TRANSITION, not long contiguous runs), retire the two bulk contiguous ranges (~77MB) whose only unique coverage is cumulative state-accumulation -- which the streamed replay driver reproduces against the live chain, deeper and reproducibly (pinned by hash): - 1_10000 trio (tx+headers+digests, 27MB): deep test chain_validate_1_10000 DELETED; short-seed sanity stays in chain_validate_1_1000; deep coverage moves to replay --from 1 --to 10000. - 1761000_1762000 trio (tx+input_boxes+headers, 51MB): recent_block_validation DELETED; EIP-27 stays hermetic via reemission.rs (20 tests); EIP-37 boundary preserved via new curated headers_1761792_1761795_eip37_curated.json (4 hdrs). Rewired the 4 non-deep consumers (difficulty/header_validation/header_sync EIP-37 + decode v2 roundtrip) to the curated file; trimmed transactions_roundtrip broad cases; m7_mainnet_corpus skip-if-missing; deleted throwaway m7_fee_discovery. Wire the replay driver as the fuzz.yml replay job (gated on REPLAY_NODE_URL secret, no-ops on hosted runners). FIXTURES.md rewritten as the durable curated manifest. Committed mainnet: 101MB -> 20MB. Whole-workspace test + clippy green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(fuzz): name cargo-fuzz targets by surface so committed seeds auto-load cargo-fuzz auto-loads corpus/<target-name>, but targets were fuzz_<surface> while the committed seed corpora are corpus/<surface> — so seeds never loaded. Rename the 6 targets fuzz_<surface> -> <surface> (cargo fuzz run ergo_tree now auto-loads corpus/ergo_tree). Found by actually running cargo-fuzz on nightly. Nightly verification (1.98.0-nightly + cargo-fuzz 0.13.2): the ergo_tree target builds with ASan (48s) and fuzzes coverage-guided (~24.8k exec/s). It surfaced a parse->serialize fixed-point violation (read_ergo_tree accepts X; re-read of write(X) hits the type-recursion depth guard) that bounded stable campaigns missed. Oracle triage: node and JVM AGREE at every step (both accept X -> same canonical b1; both reject b1 on re-read) -> bug-for-bug PARITY with Scala, NOT a consensus divergence (a shared codec quirk / KnownArtifact). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(fuzz): nightly cargo-fuzz CI job + exclude the SER-003 type-depth guard from Bug class Add the cargo-fuzz-nightly job (fuzz.yml): matrix over the 6 surfaces, installs nightly + cargo-fuzz, bounded coverage-guided smoke per target seeded from the committed corpus, uploads crash artifacts. Non-gating; the deeper libFuzzer complement to the stable structured campaign. Verified live (nightly 1.98.0 + cargo-fuzz 0.13.2): targets build with ASan and fuzz coverage-guided (~24.8k exec/s). Fix the parse->serialize fixed-point check to NOT flag the type-recursion depth guard: MAX_TYPE_DEPTH (100) is a documented stack-overflow safeguard, not a consensus boundary (Scala TypeSerializer has no type-depth limit — only the 4096-byte proposition cap), i.e. QA SER-003 kept doc-only. A re-decode tripping ONLY that cap is that documented divergence on a near-boundary re-encoding, not a codec inconsistency -> WriteRejected, not Bug. Narrow: the value/expression tree-depth guard (Scala MaxTreeDepth=110) IS a real consensus limit and still counts as a Bug. Selftest still green (detector keeps teeth for real panics). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(fuzz): gitignore the fuzz crate Cargo.lock (regenerated on nightly) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(fuzz): CI compile blocker (worktree-relative include path) + CodeRabbit findings CI blocker: replay.rs include_str! used a 6-level '../' path that only resolved inside the nested .claude/worktrees checkout (reaching the sibling main repo); CI's clean checkout couldn't find it -> ergo-difftest failed to compile on all platforms. Correct to '../../../' (repo-root relative, works in both). CodeRabbit review (PR #155): - difftest.rs: route the 'validate' oracle surface to the 'transaction' generator (was falling back to ergo_tree -> both sides reject -> zero differential signal). - ErgoSerdeOracle.scala: bounds-check readBytes before Arrays.copyOfRange, which zero-pads (not throws) on a truncated verify_avl frame -> could silently accept. - avl_frame.rs: cap the untrusted op_count pre-allocation (multi-GB Vec DoS). - difftest.rs: reject --check-canonical without --repro (was a silent no-op). - regressions.rs: make the QUEUE.md append idempotent (no duplicate line on re-file). - ci.yml / fuzz.yml: add least-privilege 'permissions: contents: read'; drop the empty 'needs: []' (actionlint error). - interface-contracts.md: tag a fenced block. Verified locally: fmt --all, build --bins, clippy, test -p ergo-difftest, the --structured --min-coverage difftest job, both workflows' YAML, and the Scala oracle still compiles + answers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(fuzz): address CodeRabbit round 2 (still-valid findings) Verified each against current code; fixed the still-valid ones, skipped the already-fixed with reason. Fixed: - ci.yml/fuzz.yml: persist-credentials: false on the new-job checkouts (top-level permissions were already added); difftest job timeout-minutes: 15. - transactions_roundtrip.rs: drop the two retired ranges from EXPECTED_TX_FILES so the (ignored) manifest-coverage guard reflects the retirement. - minimize.rs: truncation phase now ascends to the SHORTEST satisfying prefix (the descending scan kept the longest — least reduction). Real minimizer bug. - difftest.rs: reuse the to_hex helper for the canonical-gate hex. - tests/minimize.rs: assert QUEUE.md has exactly one [PENDING] after re-file (covers the regressions.rs dedup). - m7_mainnet_corpus.rs: skip-guard now checks all three retired fixtures (txs/ headers/input_boxes), not just txs, so a partial extraction can't panic. - ErgoSerdeOracle.scala: unwrap InvocationTargetException from the reflective CAvlTreeVerifier ctor so REJECT reports the real cause, not the wrapper. Skipped (already fixed in d3b070a): validate->transaction routing, --check-canonical guard, replay include_str path, avl_frame op_count cap, QUEUE dedup, readBytes bounds, needs:[] removal, top-level permissions, md fence (text tag satisfies MD040). verify_avl structured fallback left as-is: no avl_frame generator exists, so it is exercised via the #6 re-injection trigger, not the structured sweep. Verified: fmt --all, clippy (difftest/ser/mempool), ergo-difftest tests, YAML, Scala oracle compiles + answers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: arkadianet <rkadias@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fixed the directly Scala-verifiable findings; the three Critical evaluator heavy-lifts (#2/#4/#5) need JVM-oracle differential validation and are tracked separately. - #7 (option.rs, Critical): Option.filter charged JitCost(10); Scala SOptionMethods.FilterMethod is FixedCost(JitCost(20)) — same as map. Charging 10 is a consensus cost divergence. Now charges 20 (+ a cost test pinning the total so a regression to 10 is caught). - #1 (serialize.rs, Major): trace_val truncated the Debug string at byte 150 with &s[..150], which panics when a multibyte char straddles that offset. Truncate on a UTF-8 char boundary instead. - #3 (serialize.rs, Minor): value_to_typed_sigma re-tagged any SColl(_) with a CollValue::Bytes payload as CollBytes via a catch-all — the legit SByte case is already handled above, so a non-SByte element type with a bytes payload is now rejected as an inconsistent shape. Also check STuple arity before zip (previously extra elements were dropped and short tuples still produced a value). - #6 (getReg activated-version gate): investigated, left as-is. Scala has both an ErgoTree-version (getMethodById/isV3OrLaterErgoTreeVersion) and an activated-version (methodById/isV6Activated) resolution path, so the premise that getReg must not be activated-gated is not established; the claimed divergence is unreachable given VersionContext's ergoTreeVersion<=activatedVersion invariant. Reasoning recorded in the tracked follow-up. ergo-sigma suite 498 -> 499; clippy -D warnings + fmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi
…215) * refactor(ergo-sigma): split method_call.rs into per-receiver-type modules Decompose the 3,176-line `evaluator/opcodes/method_call.rs` (the `0xDC MethodCall` dispatcher) into a directory of per-receiver-type sibling modules. `eval_method_call` becomes a thin router: it charges the dispatcher cost (0xDC), applies the v6 soft-fork gate, evaluates the receiver once, and delegates each `(type_id, method_id)` arm to a named handler. The outer `match` is preserved (not eliminated) for exhaustiveness/wire-fidelity, and the catch-all no-arg fallthrough stays in `mod.rs`. Arm bodies moved VERBATIM (only relative module paths rewritten for the deeper location: `super::super::` -> `crate::evaluator::`, `super::` -> `crate::evaluator::opcodes::`); rustfmt reindented. Split map: - mod.rs router + is_v6_method (pub(super)) + check_arity (pub(super)) - coll.rs SColl(12): indexOf/zip/startsWith/endsWith/get/flatMap/ patch/updated/updateMany - global.rs SGlobal(106): encodeNbits/decodeNbits/some/powHit/xor/ serialize/deserializeTo/fromBigEndianBytes + nbits codec + the serialize DynamicCost put-cost subsystem (serialize_put_cost & helpers) - numeric.rs SNumericType(2..=6) bitwise/shift + SBigInt.toUnsignedMod + expect_bigint - unsigned_bigint.rs SUnsignedBigInt(9) bitwise/shift/modular block (lifted whole, not sub-split) + expect_unsigned_bigint (pub(super)) - avl.rs SAvlTree(100) block (lifted whole, not sub-split) + AvlEntries/AvlMutOp/extract_avl_entries/extract_avl_keys/ eval_avl_mutate - misc.rs SBox(99)/SContext(101)/SHeader(104) small arms + SGroupElement(7).exp - option.rs SOption(36) map/filter Honored "do NOT split": the outer eval_method_call match kept intact; the AVL (100,*) and SUnsignedBigInt (9,*) contiguous clusters lifted whole into one file each. Visibility promotions (minimum the compiler demanded): - check_arity: file-private fn -> pub(super) (called by arm handlers in coll/global/numeric/unsigned_bigint/misc, i.e. across sibling modules). - expect_unsigned_bigint: file-private -> pub(super) (used by both numeric.rs's toUnsignedMod and the unsigned_bigint arms). - is_v6_method stays pub(super) (already; consulted by the router and by property_call.rs via `super::method_call::is_v6_method`). - serialize_put_cost stays pub(in crate::evaluator), re-exported from mod.rs so the test path `method_call::serialize_put_cost` is byte-stable (`#[allow(unused_imports)]`: consumed only under cfg(test)). Verification: cargo test -p ergo-sigma = 454 lib passed (matches baseline), 0 failures; clippy --all-targets --all-features -D warnings clean; cargo fmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi * refactor(ergo-sigma): split helpers.rs by concern Decompose the 1,501-line `evaluator/helpers.rs` into a `helpers/` directory grouped by concern. `mod.rs` re-exports the pub(crate) surface so every existing `crate::evaluator::helpers::*` path stays valid, and keeps `sigma_to_value`'s public re-export intact (`evaluator/mod.rs` does `pub use helpers::sigma_to_value`). Bodies moved VERBATIM; the only path rewrite is depth-driven (`super::` -> `crate::evaluator::`, since the files now sit one level deeper), affecting just `use super::types::*` and the `super::opcodes::sigma::canonicalize_group_element` call in `sigma_to_value`. Split map: - mod.rs re-export hub (glob for coll/type_infer/equality/ subst_constants; explicit for serialize to keep sigma_to_value `pub`) - coll.rs CollKind, collection_to_values, coll_elem_type, values_to_collection, infer_collection, sigma_type_to_coll_kind, expand_box_collection, unpack_collection + strict_value_sigma_type, contains_sany - type_infer.rs sigma_type_compatible, infer_expr_type, infer_op_type, value_to_sigma_type - equality.rs require_comparable, check_comparable, values_equal, seq_equal, resolve_box - serialize.rs trace_val, value_to_typed_sigma, count_sigma_nodes, sigma_to_value_versioned, sigma_to_value (both wire directions kept together per "do NOT split") - subst_constants.rs subst_constants Honored "do NOT split": `sigma_to_value` kept as one exhaustive type-directed match; `value_to_typed_sigma` and `sigma_to_value*` kept in the same file (serialize.rs). Visibility promotions: - strict_value_sigma_type: file-private `fn` -> `pub(crate)`. Its sole caller is `infer_collection` (coll.rs), which now sits in a sibling module; `pub(crate)` matches the uniform convention of this file and is re-exported by the mod.rs glob. (This is the one item the prompt's "no promotions needed" note did not account for — it was already crossing the coll/type_infer boundary.) `contains_sany` stays private (used only by `strict_value_sigma_type`, co-located in coll.rs). Deviation from the prompt's file map: `strict_value_sigma_type` and `contains_sany` are placed in coll.rs (next to their only caller `infer_collection`) rather than type_infer.rs, keeping `contains_sany` private and confining the promotion to the single `strict_value_sigma_type`. Verification: cargo test -p ergo-sigma = 454 lib passed (matches baseline), 0 failures; clippy --all-targets --all-features -D warnings clean; cargo fmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi * refactor(ergo-sigma): split dispatch.rs by concern Decompose the 1,031-line `evaluator/dispatch.rs` into a `dispatch/` directory. `mod.rs` keeps the crate-facing reduce/trace API and re-exports the submodule items so `crate::evaluator::dispatch::*` (and, transitively, `evaluator::*` via `pub use dispatch::*`) surface the same names as before. dispatch used glob imports + `use super::opcodes` with bare references, so bodies moved VERBATIM with no path rewrite — only per-file import lines changed (`super::` -> `crate::evaluator::`). Split map: - mod.rs reduce_expr, reduce_expr_with_cost, TraceEntry, eval_to_value (test-only), reduce_expr_traced, reduce_expr_traced_with_cost + submodule decls & re-exports - ast_walk.rs expr_has_deserialize, inline_placeholders (paired exhaustive Payload walks) - pre_checks.rs validate_group_element_constants, check_extension_key_domain, pre_reduction_checks, check_v3_only_methods, validate_group_element (the consensus reject-gate cluster) - eval.rs eval_expr (depth-tracked router) + eval_op (opcode dispatch table), kept together per "do NOT split" Honored "do NOT split": `eval_op`'s ~580-line `match (node.opcode, &node.payload)` opcode table kept intact as one exhaustive match; eval_expr + eval_op kept in one file (shared depth/cost/trace/env). Visibility promotions: - inline_placeholders: file-private `fn` -> `pub(super)` (its only caller, `eval_expr`, now sits in the sibling `eval` module). All other items keep their original visibility; re-exports in mod.rs reproduce the pre-split surface (`eval_expr` pub(in crate::evaluator), `expr_has_deserialize`/`pre_reduction_checks` pub(crate), `validate_group_element` pub — the latter three consumed by crate::reduce and the opcode handlers). Deviation: the optional `ConcreteCollection` (0x83) arm extraction flagged in the prompt as a nice-to-have was intentionally NOT done — the prompt marks it non-blocking, and touching that arm would move consensus logic out of the exhaustive opcode table for cosmetic uniformity only. Verification: cargo test -p ergo-sigma = 454 lib passed (matches baseline), 0 failures; clippy --all-targets --all-features -D warnings clean; cargo fmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi * fix(ergo-sigma): address CodeRabbit findings on #215 (verified vs Scala) Fixed the directly Scala-verifiable findings; the three Critical evaluator heavy-lifts (#2/#4/#5) need JVM-oracle differential validation and are tracked separately. - #7 (option.rs, Critical): Option.filter charged JitCost(10); Scala SOptionMethods.FilterMethod is FixedCost(JitCost(20)) — same as map. Charging 10 is a consensus cost divergence. Now charges 20 (+ a cost test pinning the total so a regression to 10 is caught). - #1 (serialize.rs, Major): trace_val truncated the Debug string at byte 150 with &s[..150], which panics when a multibyte char straddles that offset. Truncate on a UTF-8 char boundary instead. - #3 (serialize.rs, Minor): value_to_typed_sigma re-tagged any SColl(_) with a CollValue::Bytes payload as CollBytes via a catch-all — the legit SByte case is already handled above, so a non-SByte element type with a bytes payload is now rejected as an inconsistent shape. Also check STuple arity before zip (previously extra elements were dropped and short tuples still produced a value). - #6 (getReg activated-version gate): investigated, left as-is. Scala has both an ErgoTree-version (getMethodById/isV3OrLaterErgoTreeVersion) and an activated-version (methodById/isV6Activated) resolution path, so the premise that getReg must not be activated-gated is not established; the claimed divergence is unreachable given VersionContext's ergoTreeVersion<=activatedVersion invariant. Reasoning recorded in the tracked follow-up. ergo-sigma suite 498 -> 499; clippy -D warnings + fmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…tor fixes Adds a (scala-cli-gated, #[ignore]) reduce-surface differential that pins the #215 serialize/flatMap fixes against the sigma-state 6.0.2 reference, replacing the "verified by source-analogy" caveat with an actual oracle run: serialize(SELF)==SELF.bytes node=P:d3|142 jvm=P:d3|142 serialize(INPUTS).size>0 node=P:d3|145 jvm=P:d3|145 empty flatMap type node=P:d3|166 jvm=P:d3|166 Each tree reduces to a Bool→SigmaProp that is true (P:d3) on BOTH the node and the JVM, with matching JIT cost — confirming: - #2: serialize(SELF) equals the canonical box bytes (ErgoBox.bytes) and serialize(INPUTS) resolves the box collection, both at the JVM's cost (a pre-fix node REJECTED these → would diverge); - #4: the empty-flatMap output is tagged Coll[Long], not Coll[Byte] — its serialize bytes match an empty Coll[Long] byte-for-byte (a type mismatch would reduce to false / P:d2). Run: cargo test -p ergo-difftest --lib reduce_diff_serialize_and_flatmap -- --ignored Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi
…ings 2, 4, 5) (#223) * fix(ergo-sigma): Coll.patch accepts every element carrier (#215 finding 5) Scala `Coll[A].patch` (CollsOverArrays) is generic over the element type, but the Rust impl matched only Byte/Int/Long carriers and rejected valid `Coll[Short]`, `Coll[Boolean]`, tuple, box, header, and generic-carrier receivers — a reject-valid (the node would reject a spend the reference accepts). Rewrite the carrier-specific match to the generic collection_to_values / values_to_collection path `updateMany` already uses, with a per-patched-element sigma_type_compatible check (Array[A] backing throws ArrayStoreException on a foreign element; an empty patch writes nothing and is accepted regardless of declared type, matching Scala). The oracle-verified splice-index algorithm and the Scala-anchored PerItemCost are unchanged. Tests: +Coll[Short] and +Coll[Boolean] carriers; all 12 existing CollInt/Byte/ Long splice-semantics tests still pass (identical results via the generic path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi * fix(ergo-sigma): flatMap preserves output element type for an empty receiver (#215 finding 4) When the flatMap receiver is empty there is no inner collection to read the output shape from, so the result collapsed to Coll[Byte] even when the static result type is Coll[Long], Coll[Int], etc. — the wrong element-type tag, which serializes/dispatches differently from the reference (Scala threads RType[B]). Recover B from the mapper body's static Coll[B] type: Const{SColl(B)}, ConcreteCollection{elem_type=B}, and If-branches all determine B directly. A recovered type is always correct (it IS the body's collection element type), so this never introduces a new divergence; bodies whose type isn't statically determinable from the IR node keep the legacy Coll[Byte] fallback — a documented, strictly-smaller residual. Tests: empty receiver with a Coll[Long] Const body and a Coll[Int] ConcreteCollection body both yield the correctly-typed empty collection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi * fix(ergo-sigma): SGlobal.serialize resolves context-backed box carriers (#215 finding 2) value_to_typed_sigma rejected SELF / INPUTS / OUTPUTS / DATAINPUTS / Coll[Box] in its catch-all, so serialize(SELF), serialize(INPUTS), serialize(SELF.tokens) etc. failed where Scala accepts — a reject-valid. Thread an Option<&ReductionContext> through value_to_typed_sigma. On the SGlobal.serialize path (Some) the box carriers resolve via the existing resolve_box to their concrete boxes and emit OpaqueBoxBytes(raw_bytes) — the SAME canonical bytes ExtractBytes (0xC3) and the InlineBox arm already use, which is exactly what Scala DataSerializer/ErgoBox.sigmaSerializer writes, so this is correct by analogy to those oracle-verified paths (no new byte format). BoxCollection resolves to Coll[SBox]; CollBox resolves each element. On the SubstConstants path (None) behavior is unchanged — a context box as a substituted constant stays unsupported (pre-existing, narrower residual). Test: SelfBox resolves through a context to the box's canonical bytes (matching the InlineBox path); rejects without a context. ergo-sigma suite 503 -> 504. NOTE: verified against the reference by analogy to ExtractBytes/InlineBox (identical raw_bytes); a JVM-oracle differential on top-level serialize(SELF)/ serialize(INPUTS) vectors is still recommended before relying on it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi * test(ergo-difftest): live JVM-oracle differential for the #215 evaluator fixes Adds a (scala-cli-gated, #[ignore]) reduce-surface differential that pins the #215 serialize/flatMap fixes against the sigma-state 6.0.2 reference, replacing the "verified by source-analogy" caveat with an actual oracle run: serialize(SELF)==SELF.bytes node=P:d3|142 jvm=P:d3|142 serialize(INPUTS).size>0 node=P:d3|145 jvm=P:d3|145 empty flatMap type node=P:d3|166 jvm=P:d3|166 Each tree reduces to a Bool→SigmaProp that is true (P:d3) on BOTH the node and the JVM, with matching JIT cost — confirming: - #2: serialize(SELF) equals the canonical box bytes (ErgoBox.bytes) and serialize(INPUTS) resolves the box collection, both at the JVM's cost (a pre-fix node REJECTED these → would diverge); - #4: the empty-flatMap output is tagged Coll[Long], not Coll[Byte] — its serialize bytes match an empty Coll[Long] byte-for-byte (a type mismatch would reduce to false / P:d2). Run: cargo test -p ergo-difftest --lib reduce_diff_serialize_and_flatmap -- --ignored Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kw7uCwqmiGna8fpg6TvxGi --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Why
CI only runs on
pull_request(main was historically direct-pushed), so several failures sat undetected onmainand now fail on every open PR's checks:cargo-deny—[bans] wildcards = "deny"trips on internalergo-*crates: declared by path /{ workspace = true }with no version → implicit*.cargo testb4_byte_parity_*— referenced a gitignored fixture, so a clean checkout had zero committed byte-parity coverage and the test errored.cargo testconfig::tests(macOS-only) — temp-fixture filenames were derived fromprocess::id()+SystemTimenanos. macOS's microsecond-grained clock hands two parallel test threads the same name; the racingfs::writes interleave into corrupt TOML → load assertions panic. Linux's finer clock hid it (ubuntu was green, only macOS failed).Fix
deny.toml— addallow-wildcard-paths = true. Still rejects external*requirements; only exempts path-only / workspace-member deps.ergo-node/src/api_bridge/tests.rs— extractrun_byte_parity(files, min_total). The always-on default test now exercises three committed corpora — genesis (1..200), modern (205000..205200), a700000-era block. The broad gitignored700000..700200corpus moves to an#[ignore]'d companion.ergo-node/src/config/tests.rs— derive temp-fixture uniqueness from a monotonicAtomicU64(via aunique_temp_tomlhelper) instead of the wall clock, so parallel threads can never collide regardless of clock resolution.Verification
cargo test -p ergo-node --lib b4_byte_parity→ okcargo test -p ergo-node --lib config::tests -- --test-threads=16, looped 20× → okcargo clippy -p ergo-node --all-targets --all-features -- -D warnings→ cleancargo fmt --all -- --check→ cleancargo deny check→ ok🤖 Generated with Claude Code
Summary by CodeRabbit
Chores
Tests
Refactor