Skip to content

feat(api): Scala-compat /emission/at + fix auth-layer capture of the router fallback - #15

Merged
arkadianet merged 2 commits into
mainfrom
feat/api-emission-at-and-fallback-403
Jun 7, 2026
Merged

feat(api): Scala-compat /emission/at + fix auth-layer capture of the router fallback#15
arkadianet merged 2 commits into
mainfrom
feat/api-emission-at-and-fallback-403

Conversation

@arkadianet

@arkadianet arkadianet commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Summary

Two stacked defects made GET /emission/at/{blockHeight} answer 403 invalid.api-key on a node with security configured:

  1. The route was never implemented — the honest answer was a 404.
  2. The api_key middleware was mounted with Router::layer, which also wraps the subtree's implicit fallback; Router::merge then propagates that wrapped fallback into the assembled router. Net effect: every unmatched path node-wide rejected on the key instead of 404ing, masking "route does not exist" as "you need a key". The blockchain indexer status-gate had the same latent capture (unknown paths would have answered 503 indexer-syncing during catch-up once the auth wrap was gone).

The fallback fix

  • All route middleware (require_api_key ×2, enforce_status_gate) now mounts via route_layer — matched routes only, the fallback stays pristine. Unmatched paths answer a plain, ungated 404 (house unmounted-surface rule).
  • Scala's whole-prefix gating survives via explicit catch-all routes: unknown /wallet/* and /node/* subpaths still reject on the key first (parity with pathPrefix(...) & withAuth, probed live against the reference node), then 404 once the key passes.
  • The public /wallet/ui statics are unaffected — exact-path routes outrank the gated wildcard (pinned by the existing wallet_ui_auth_scope tests).

The endpoint

GET /emission/at/{blockHeight} — EIP-27-aware Scala-parity quintuple (height, minerReward, totalCoinsIssued, totalRemainCoins, reemitted) mirroring EmissionApiRoute.emissionInfoAtHeight. Public (no withAuth in Scala), mounted in every node mode — the schedule is static per-network math.

  • Cumulative-issuance primitives join ergo-mining::emission_rules with Scala line citations: issued_coins_after_height (incl. the fixedRate × (fixedRatePeriod − 1) quirk), coins_and_blocks_total (verbatim walk), emission_info_at_height (takes Option<&ReemissionSettings>Nonereemitted: 0 for specs without EIP-27).
  • ergo-api stays traits-only: new EmissionSchedule view + EmissionInfoJson wire shape; ergo-node's EmissionScheduleBridge implements it over the chain spec, caching coinsTotal at boot (Scala holds it as a lazy val).
  • GET /emission/scripts is deferred — the emission/reemission/pay2Reemission tree predefs aren't exposed in the workspace yet. Documented in compatibility.md; its live-Scala oracle capture ships at test-vectors/api/emission/scripts.json.

Testing

  • TDD throughout: the fallback regression test was red (403 ≠ 404) before the fix; the emission tests were captured-oracle-first.
  • 17 differential vectors captured from a live Scala mainnet node (test-vectors/api/emission/): genesis edge, fixed-rate boundary 525599/525600, founders end 655199/655200, EIP-27 activation 777216/777217, the 15 ERG era, emission end 2080799/2080800, past-the-end clamps. Pins coinsTotal = 97,739,925 ERG, blocksTotal = 2,080,799.
  • New assembled-router mount-matrix tests close the previously documented coverage gap in tests/auth.rs (the real merge path was untested — exactly where this bug lived).
  • Full workspace suite green; live-verified on a deployed node: byte-identical /emission/at responses vs the reference Scala node at three heights, /zzz → 404 bare, prefix gating intact, malformed height → 400.

Docs

CHANGELOG.md (Added + Fixed), docs/configuration.md + docs/operating.md (gate-scope clarification, /emission/* listed public), docs/compatibility.md (/emission surface status).

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added /emission/at/{blockHeight} API endpoint for querying per-height emission schedule data, including EIP-27-aware reemission handling.
  • Bug Fixes

    • Fixed API key authentication behavior: unknown paths now return 404 Not Found instead of 403 Forbidden when the api_key gate is configured.
  • Documentation

    • Clarified API security scope and authentication boundaries for /wallet/* and /node/* routes.
    • Documented the new emission endpoint with known limitations (emission scripts endpoint not yet implemented).

…router fallback

Two stacked defects made GET /emission/at/{h} answer 403 invalid.api-key:
the route was never implemented, and the api_key middleware was mounted
with Router::layer, which also wraps the subtree's implicit fallback —
Router::merge then propagates that wrapped fallback router-wide, so every
unmatched path rejected on the key instead of 404ing. The indexer
status-gate had the same latent capture (unknown paths would have 503'd
while the indexer caught up).

Fallback fix: all route middleware now mounts via route_layer (matched
routes only); unmatched paths answer a plain ungated 404. Scala's
whole-prefix gating survives via explicit catch-alls — unknown /wallet/*
and /node/* subpaths still reject on the key first (parity with
pathPrefix(...) & withAuth, probed live), then 404 once the key passes.
The public /wallet/ui statics outrank the gated wildcard.

/emission/at: EIP-27-aware Scala-parity quintuple (height, minerReward,
totalCoinsIssued, totalRemainCoins, reemitted) mirroring
EmissionApiRoute.emissionInfoAtHeight. Cumulative-issuance primitives
(issued_coins_after_height with the fixedRate*(fixedRatePeriod-1) quirk,
coins_and_blocks_total, emission_info_at_height) join
ergo-mining::emission_rules; ergo-api stays traits-only via a new
EmissionSchedule view, implemented by ergo-node's EmissionScheduleBridge
over the chain spec (coinsTotal cached at boot). Mounted in every node
mode; public by parity. /emission/scripts deferred (tree predefs not
exposed) — documented in compatibility.md.

Differential-tested against 17 vectors captured from a live Scala
mainnet node (test-vectors/api/emission/): coinsTotal pins at
97,739,925 ERG, blocksTotal at 2,080,799; EIP-27 boundary 777216/777217
and the fixed-rate boundary 525599/525600 covered. Live-verified on a
deployed node: byte-identical responses vs the reference at three
heights, /zzz → 404, prefix gating intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@arkadianet, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 50 minutes and 34 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3730910e-f146-413d-b5ee-8a697b88dc4d

📥 Commits

Reviewing files that changed from the base of the PR and between 933117c and 89b5a8d.

📒 Files selected for processing (4)
  • ergo-api/src/server.rs
  • ergo-mining/src/emission_rules.rs
  • ergo-node/src/api_bridge.rs
  • ergo-node/src/node/boot.rs
📝 Walkthrough

Walkthrough

This PR introduces a Scala-compatible /emission/at/{blockHeight} REST endpoint and fixes API-key middleware to return 404 (not 403) for unknown routes. Emission computation is added to mining rules, bridged through an adapter in the node layer, and mounted into the server when configured; auth gating now uses route-scoped middleware instead of subtree-wide layers.

Changes

Emission API & Auth Behavior

Layer / File(s) Summary
Documentation updates
CHANGELOG.md, docs/compatibility.md, docs/configuration.md, docs/operating.md
CHANGELOG and docs clarify new /emission/at/{blockHeight} endpoint (EIP-27-aware), confirm /emission/scripts remains unimplemented, and document corrected API-key gating behavior (unknown routes return ungated 404, not 403).
Auth middleware fallback correction
ergo-api/src/auth.rs, ergo-api/src/wallet/mod.rs, ergo-api/tests/auth.rs
Introduces unknown_gated_subpath handler returning 404 for post-auth catches; updates /wallet/* and admin shutdown routing to use route_layer (not layer) so auth only applies to matched routes, preventing fallback from masking unmatched paths as 403.
Emission computation logic
ergo-mining/src/emission_rules.rs
Adds cumulative-emission helpers (issued_coins_after_height, coins_and_blocks_total) and EmissionInfo struct composing miner reward, issued/remaining coins, and reemission amount (EIP-27-aware). Extends tests with live-Scala oracle vector validation.
Emission API contract & routing
ergo-api/src/emission.rs, ergo-api/src/lib.rs
Defines EmissionSchedule trait and EmissionInfoJson struct with camelCased fields; implements emission_router that mounts GET /emission/at/:height and attaches schedule as shared state.
Emission bridge (mining → API)
ergo-node/src/api_bridge.rs, ergo-node/src/api_bridge/emission.rs
Implements EmissionScheduleBridge as trait adapter, caching coins_total and delegating emission_info_at to mining rules; exposes bridge through api_bridge module and validates against live-Scala vectors.
Server context & route mounting
ergo-api/src/server.rs, ergo-node/src/node/boot.rs
ServerCtx adds optional emission field; router construction threads field through legacy/test paths (as None) and production path (as EmissionScheduleBridge); admin and blockchain routes switch to route_layer to restrict gating scope.
Auth behavior & emission routing tests
ergo-api/tests/openapi_native_runtime_mount.rs
Adds regression tests for auth fallback behavior (unknown paths return 404, never 403), whole-prefix gating on /wallet/* and /node/* (403 without key, 404 with), and /emission/at/:height public access with JSON envelope validation.
Emission test vectors (Scala oracle)
test-vectors/api/emission/*
Captures per-height emission snapshots at key mainnet heights and scripts fixture; used for live-Scala oracle validation.
Test helper updates (ServerCtx)
ergo-api/tests/*.rs
Updates all test fixtures to include emission: None field in ServerCtx initializers across blockchain, wallet, mempool, and integration test suites.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hopping with glee, the API takes flight,
New emission routes shining in pure Scala light,
Auth gates now answer with proper 404s,
Not hiding as 403s behind closed doors,
Whole-prefix paths guarded, the routing pure and bright! 🚀

🚥 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 accurately summarizes the two main changes: implementing the Scala-compatible /emission/at endpoint and fixing the auth-layer middleware mounting issue that was incorrectly capturing router fallbacks.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/api-emission-at-and-fallback-403

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.

CI's first check step (.github/workflows/ci.yml) runs
`cargo fmt --all -- --check`; four files from the emission/fallback
change needed mechanical rewrapping (and one `pub use` reorder).
No behavioral change — touched test targets re-verified green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@arkadianet
arkadianet merged commit 08ee11e into main Jun 7, 2026
8 checks passed
@arkadianet
arkadianet deleted the feat/api-emission-at-and-fallback-403 branch June 7, 2026 00:38
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>
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