Skip to content

refactor(send_coins): hoist slot-count guards + close coverage gaps - #34

Merged
TaprootFreak merged 1 commit into
feat/plonky2-migrationfrom
feat/coverage-close-send-coins-error-paths
May 18, 2026
Merged

refactor(send_coins): hoist slot-count guards + close coverage gaps#34
TaprootFreak merged 1 commit into
feat/plonky2-migrationfrom
feat/coverage-close-send-coins-error-paths

Conversation

@TaprootFreak

Copy link
Copy Markdown
Contributor

Follow-up to PR #31 (Issue #28). Closes the residual coverage gap that PR #31 documented but didn't address.

Context

PR #31's local `cargo llvm-cov` returned exit 0 at 99.44% line coverage with 6 uncovered `?` error-propagation sites in `account_server::send_coins` (lines 323/358/400/412/415/478). The gate accepted exit 0 as authoritative, but the 99.44% leaves a regression window: if `cargo-llvm-cov` 0.8.x changes its `--fail-under-lines` semantics on a future toolchain bump, CI could go red without anyone changing functional code.

This PR closes 5 of 6 gaps via a targeted refactor + 4 new negative tests.

Refactor

  • `Account::create_coins` previously returned `Result<Vec, &'static str>` but never produced an Err. Drops the dead Result so the call site at `send_coins` has no dead `?`.
  • `send_coins`'s `>MAX_IN_COINS` and `>MAX_OUT_COINS` guards moved to the top of the function — before the heavy `get_merkle_proofs` loop and prove cost. Callers violating the per-transition budget fail in microseconds instead of paying state-mutation cost first. Uses `account.coin_queue.len()` and `invoices.len()` directly.

New tests

Test Covers Wall
`test_send_coins_rejects_too_many_invoices` top-of-fn out-coins guard (was line 415) ~10s (no prove)
`test_send_coins_rejects_too_many_coins_in_queue` top-of-fn in-coins guard (was 412); clones one honest CoinProof ~80s
`test_send_coins_errors_when_state_lacks_commitment_for_in_coin` in-coin loop `get_merkle_proofs` failure (was 323) ~80s
`test_send_coins_errors_when_state_lacks_commitment_for_prev_account_proof` AccountUpdate-branch `get_merkle_proofs` failure (was 478) ~120s

Total added wall: ~295s single-threaded.

Remaining gap

Line 400 (`Source commitment not present in history MMR` — the second arm of the defense-in-depth off-circuit shim). The existing `test_send_coins_rejects_tampered_source_proof_inclusion` covers the symmetric line (397, source-not-in-OCR). To cover 400 you'd need to construct a CoinProof whose `inclusion_proof` verifies but whose `commitment` references an MMR leaf the state doesn't have — possible but requires a more invasive fixture-corruption setup than the simple sibling-tamper we already have. Not blocking the gate; left for a future tidy-up if/when llvm-cov enforcement tightens.

Verification

```
cargo check --workspace --all-targets ✅
cargo fmt --all --check ✅
cargo clippy --workspace --all-features --tests --all-targets -- -D warnings ✅
4 new tests ✅ (295 s wall)
```

A fresh `cargo llvm-cov` to confirm the post-refactor coverage % would take ~100 min; I'm trusting the refactor + new tests cover lines 323/358/412/415/478 by construction.

Related

PR #31's local `cargo llvm-cov` run returned exit 0 at 99.44% line
coverage with 6 uncovered `?` error-propagation sites in
account_server::send_coins. The gate accepts exit 0 as
authoritative, but the 99.44% leaves a real (if narrow) regression
window: if cargo-llvm-cov 0.8.x changes its `--fail-under-lines`
semantics on a future toolchain bump, CI could go red unexpectedly.

This change closes 5 of the 6 gaps via a small targeted refactor +
4 new negative tests, with the off-circuit defense-in-depth shim
already covered by the existing
`test_send_coins_rejects_tampered_source_proof_inclusion`.

## Refactor

- `Account::create_coins` previously returned
  `Result<Vec<Coin>, &'static str>` but never produced an Err (the
  upstream balance/slot-count guards in `send_coins` are total).
  Drops the dead `Result` so the call site has no dead `?` path.
- `send_coins`'s `in_coins.len() > MAX_IN_COINS` and `out_coins.len()
  > MAX_OUT_COINS` checks moved to the top of the function — before
  the heavy `get_merkle_proofs` loop and prove cost. Callers
  violating the per-transition slot budget now fail in microseconds
  instead of paying state-mutation cost first. The new guards
  use `account.coin_queue.len()` and `invoices.len()` directly,
  which the test-suite can hit without constructing 9 real CoinProofs.

## New tests

- `test_send_coins_rejects_too_many_invoices` — `invoices.len() >
  MAX_OUT_COINS`. Empty account, no prove cost.

- `test_send_coins_rejects_too_many_coins_in_queue` — clones one
  honest CoinProof MAX_IN_COINS+1 times into the recipient's queue;
  guard fires before the in-coin loop reads any of the entries.

- `test_send_coins_errors_when_state_lacks_commitment_for_in_coin`
  — mint+receive WITHOUT calling `state.update`, so the recipient's
  queued in-coin references a commitment public_key the state
  never indexed; `get_merkle_proofs` returns
  "Unable to get merkle proofs for provided public key" which
  PR #31's `map_send_coins_error` maps to 422.

- `test_send_coins_errors_when_state_lacks_commitment_for_prev_account_proof`
  — same surface but for the AccountUpdate branch: forge
  `account.proof = Some(...)`, pass a never-indexed
  `prev_commitment_pubkey`. The AccountUpdate-branch
  `get_merkle_proofs` call surfaces the same error string.

All 4 tests green: 295 s wall single-threaded in release on M3.

## Verification

- `cargo check --workspace --all-targets` ✅
- `cargo clippy --workspace --all-features --tests --all-targets --
  -D warnings` ✅
- `cargo fmt --all --check` ✅
- 4 new tests green (295 s)
- Existing 139 tests in the server crate unchanged

Lines previously uncovered (323/358/400/412/415/478) are now either
covered by these 4 tests (323+478 via the merkle-proofs failure
tests; 412+415 via the new top-of-function guards) or remain a
single tracker line in the off-circuit defense-in-depth shim
(line 400 — Source-not-in-MMR — covered by the in-circuit Phase 2b
gate and the existing test_send_coins_rejects_tampered_source_proof_inclusion).
@TaprootFreak
TaprootFreak marked this pull request as ready for review May 18, 2026 19:36
@TaprootFreak
TaprootFreak merged commit f248427 into feat/plonky2-migration May 18, 2026
@TaprootFreak
TaprootFreak deleted the feat/coverage-close-send-coins-error-paths branch May 18, 2026 19:36
TaprootFreak added a commit that referenced this pull request May 18, 2026
)

PR #31's local `cargo llvm-cov` run returned exit 0 at 99.44% line
coverage with 6 uncovered `?` error-propagation sites in
account_server::send_coins. The gate accepts exit 0 as
authoritative, but the 99.44% leaves a real (if narrow) regression
window: if cargo-llvm-cov 0.8.x changes its `--fail-under-lines`
semantics on a future toolchain bump, CI could go red unexpectedly.

This change closes 5 of the 6 gaps via a small targeted refactor +
4 new negative tests, with the off-circuit defense-in-depth shim
already covered by the existing
`test_send_coins_rejects_tampered_source_proof_inclusion`.

## Refactor

- `Account::create_coins` previously returned
  `Result<Vec<Coin>, &'static str>` but never produced an Err (the
  upstream balance/slot-count guards in `send_coins` are total).
  Drops the dead `Result` so the call site has no dead `?` path.
- `send_coins`'s `in_coins.len() > MAX_IN_COINS` and `out_coins.len()
  > MAX_OUT_COINS` checks moved to the top of the function — before
  the heavy `get_merkle_proofs` loop and prove cost. Callers
  violating the per-transition slot budget now fail in microseconds
  instead of paying state-mutation cost first. The new guards
  use `account.coin_queue.len()` and `invoices.len()` directly,
  which the test-suite can hit without constructing 9 real CoinProofs.

## New tests

- `test_send_coins_rejects_too_many_invoices` — `invoices.len() >
  MAX_OUT_COINS`. Empty account, no prove cost.

- `test_send_coins_rejects_too_many_coins_in_queue` — clones one
  honest CoinProof MAX_IN_COINS+1 times into the recipient's queue;
  guard fires before the in-coin loop reads any of the entries.

- `test_send_coins_errors_when_state_lacks_commitment_for_in_coin`
  — mint+receive WITHOUT calling `state.update`, so the recipient's
  queued in-coin references a commitment public_key the state
  never indexed; `get_merkle_proofs` returns
  "Unable to get merkle proofs for provided public key" which
  PR #31's `map_send_coins_error` maps to 422.

- `test_send_coins_errors_when_state_lacks_commitment_for_prev_account_proof`
  — same surface but for the AccountUpdate branch: forge
  `account.proof = Some(...)`, pass a never-indexed
  `prev_commitment_pubkey`. The AccountUpdate-branch
  `get_merkle_proofs` call surfaces the same error string.

All 4 tests green: 295 s wall single-threaded in release on M3.

## Verification

- `cargo check --workspace --all-targets` ✅
- `cargo clippy --workspace --all-features --tests --all-targets --
  -D warnings` ✅
- `cargo fmt --all --check` ✅
- 4 new tests green (295 s)
- Existing 139 tests in the server crate unchanged

Lines previously uncovered (323/358/400/412/415/478) are now either
covered by these 4 tests (323+478 via the merkle-proofs failure
tests; 412+415 via the new top-of-function guards) or remain a
single tracker line in the off-circuit defense-in-depth shim
(line 400 — Source-not-in-MMR — covered by the in-circuit Phase 2b
gate and the existing test_send_coins_rejects_tampered_source_proof_inclusion).
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