Skip to content

chore(ci): fix two pre-existing main CI blockers (cargo-deny wildcard + b4 byte-parity fixture) - #4

Merged
arkadianet merged 3 commits into
mainfrom
chore/ci-green-2
Jun 3, 2026
Merged

chore(ci): fix two pre-existing main CI blockers (cargo-deny wildcard + b4 byte-parity fixture)#4
arkadianet merged 3 commits into
mainfrom
chore/ci-green-2

Conversation

@arkadianet

@arkadianet arkadianet commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Why

CI only runs on pull_request (main was historically direct-pushed), so several failures sat undetected on main and now fail on every open PR's checks:

  1. cargo-deny[bans] wildcards = "deny" trips on internal ergo-* crates: declared by path / { workspace = true } with no version → implicit *.
  2. cargo test b4_byte_parity_* — referenced a gitignored fixture, so a clean checkout had zero committed byte-parity coverage and the test errored.
  3. cargo test config::tests (macOS-only) — temp-fixture filenames were derived from process::id() + SystemTime nanos. macOS's microsecond-grained clock hands two parallel test threads the same name; the racing fs::writes interleave into corrupt TOML → load assertions panic. Linux's finer clock hid it (ubuntu was green, only macOS failed).

Fix

  1. deny.toml — add allow-wildcard-paths = true. Still rejects external * requirements; only exempts path-only / workspace-member deps.
  2. ergo-node/src/api_bridge/tests.rs — extract run_byte_parity(files, min_total). The always-on default test now exercises three committed corpora — genesis (1..200), modern (205000..205200), a 700000-era block. The broad gitignored 700000..700200 corpus moves to an #[ignore]'d companion.
  3. ergo-node/src/config/tests.rs — derive temp-fixture uniqueness from a monotonic AtomicU64 (via a unique_temp_toml helper) 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 → ok
  • cargo test -p ergo-node --lib config::tests -- --test-threads=16, looped 20× → ok
  • cargo clippy -p ergo-node --all-targets --all-features -- -D warnings → clean
  • cargo fmt --all -- --check → clean
  • cargo deny check → ok
  • Codex diff review (both commits) → SAFE

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores

    • Updated ban/configuration to allow wildcard path resolution for workspace-member dependencies.
  • Tests

    • Improved test fixture handling using temporary TOML fixtures to avoid concurrent-write issues and ensure automatic cleanup.
    • Adjusted many tests to use the shared fixture helper for deterministic execution.
  • Refactor

    • Consolidated transaction byte-parity test logic into a shared helper, narrowed default suite, and added a broad ignored corpus with tuned thresholds.

…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.
@coderabbitai

coderabbitai Bot commented Jun 3, 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: f1ea8386-25b2-46ed-9d1a-75231525cbdb

📥 Commits

Reviewing files that changed from the base of the PR and between 0cd2a25 and 14b1cef.

📒 Files selected for processing (1)
  • ergo-node/src/config/tests.rs

📝 Walkthrough

Walkthrough

This 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 run_byte_parity(files, min_total) test helper with an added ignored broad-corpus test.

Changes

Configuration and Test Infrastructure

Layer / File(s) Summary
Wildcard path handling in dependency bans
deny.toml
Added explanatory comments and allow-wildcard-paths = true to the [bans] wildcards configuration to permit wildcard resolution for path/workspace-member dependencies while keeping external wildcard versions denied.
Temp TOML helpers and NamedTempFile guards in config tests
ergo-node/src/config/tests.rs
Added temp_toml(body) and default_toml() using tempfile::NamedTempFile, updated minimal_cli to accept an optional TOML path, changed write_toml to return a NamedTempFile, and updated many tests to pass default_toml() and drop manual file cleanup.
Byte-parity test helper and expanded corpus
ergo-node/src/api_bridge/tests.rs
Extracted run_byte_parity(files, min_total) for byte-parity via JSON round-trip testing; updated the committed mainnet vector test to use it and added an #[ignore] test to run the broader 700k-range corpus with a higher minimum pass threshold.

🎯 3 (Moderate) | ⏱️ ~20 minutes

🐰 I hop with careful, tiny paws,
Wildcards tamed inside the laws,
Temp files wrapped in gentle thread,
Tests refactored, broadened spread,
Quiet changes, steady cause.

🚥 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 clearly summarizes the main changes: fixing CI blockers related to cargo-deny wildcard handling and b4 byte-parity fixture issues, which aligns with the file-level changes in deny.toml and test refactoring.
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 chore/ci-green-2

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.

…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.
@arkadianet

Copy link
Copy Markdown
Owner Author

Upgraded the config-test fix from a monotonic-counter name to tempfile::NamedTempFile (the crate's existing idiom — already used in api_bridge/mem_csv/mem_marker). This kills the whole bug class, not just the symptom: O_EXCL creation makes parallel collisions impossible, and RAII drop removes the file even on a panicking assertion — eliminating 75 manual remove_file calls and a 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 untouched. Net −74 LOC, 102 tests pass, looped 20× at --test-threads=16 with zero leaked temp files. Codex: SAFE.

@arkadianet
arkadianet merged commit 52febb5 into main Jun 3, 2026
8 checks passed
@arkadianet
arkadianet deleted the chore/ci-green-2 branch June 3, 2026 22:50
arkadianet added a commit that referenced this pull request Jul 3, 2026
* 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>
arkadianet added a commit that referenced this pull request Jul 18, 2026
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
arkadianet added a commit that referenced this pull request Jul 18, 2026
…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>
arkadianet added a commit that referenced this pull request Jul 18, 2026
…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
arkadianet added a commit that referenced this pull request Jul 18, 2026
…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>
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