Skip to content

fix(core): surface partial external-signer merkle uploads as PartialUpload - #167

Merged
jacderida merged 2 commits into
mainfrom
bug/merkle-external-signer-partial-upload
Aug 11, 2026
Merged

fix(core): surface partial external-signer merkle uploads as PartialUpload#167
jacderida merged 2 commits into
mainfrom
bug/merkle-external-signer-partial-upload

Conversation

@Nic-dorman

Copy link
Copy Markdown
Member

Linear issue

V2-944

Fixes #166.

Risk tier

  • T0 — docs / tooling / CI / pure UX-output. Repo CI only.
  • T1 — client-only, no network-facing behavior change. CI + prod compat smoke.
  • T2 — node/client logic with behavioral surface, no protocol/format/economics change. Dev testnet + ADR.
  • T3 — protocol / storage format / payments / routing. T2 evidence + adversarial testing.

Compatibility

  • Wire: none
  • Storage: none
  • API: no signature or type changes. Behavioral: Client::finalize_upload_merkle(_with_progress) now returns Err(Error::PartialUpload) when chunks remain short of quorum after all retries, where it previously returned Ok with chunks_failed > 0. This matches the wave-batch finalize's existing contract and FileUploadResult::chunks_failed's own documented contract ("Always 0 for a successful upload — partial-failure information is conveyed via PartialUpload instead"). Callers that inspected chunks_failed keep working (it is now always 0 on Ok); callers that ignored it — every known one: desktop app, mobile FFI — stop reporting paid, partially stored, unretrievable files as complete.

Semver impact

  • breaking
  • feature
  • fix

Test evidence

  • 2 new unit tests on the extracted merkle_finalize_result fold: quorum shortfall → PartialUpload with correct stored/failed address sets (preflight carry-ins included in stored, no address expected for them), and full success → identical previous Ok shape with chunks_failed == 0.
  • cargo test -p ant-core --lib: 432/432 pass.
  • cargo clippy --all-targets --all-features -- -D warnings and cargo fmt --all -- --check clean.

New dependency

none

ADR

n/a

Mitigation / rollback

Single-commit revert restores the old semantics; a caller needing the previous behavior can match Error::PartialUpload, which carries the same counts the Ok used to (plus the failed addresses).

Notes

🤖 Generated with Claude Code

…pload

finalize_upload_merkle(_with_progress) returned Ok with
chunks_failed > 0 when chunks stayed short of quorum after all
retries, while the wave-batch finalize returns Err(PartialUpload)
for the same failure. Every known caller (desktop app, mobile FFI)
took that Ok as success, reporting a paid but not fully retrievable
file as complete (#166).

The store outcome now folds through merkle_finalize_result: quorum
shortfalls surface as Error::PartialUpload carrying the stored and
failed address sets (preflight carry-ins included in stored), and a
full success keeps the exact previous result shape with
chunks_failed hard-wired to 0 — honouring FileUploadResult's own
"always 0 on success" contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Nic-dorman

Copy link
Copy Markdown
Member Author

Note on the Security Audit failure: it's pre-existing, not introduced here — this PR changes no dependencies. cargo audit fails on main's lockfile too: RUSTSEC-2026-0220 (ruint 1.18.0, "Uint shift operations: incorrect overflow flags and truncated shift amounts", advisory dated 2026-07-08, fix >=1.20.0). ruint comes in via the alloy/evmlib tree; cargo update -p ruint should clear it repo-wide as a separate chore PR.

@dirvine dirvine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Panel review (6 seats: correctness, storage/lifecycle/concurrency, API/compatibility, tests/operations, security, and independent GLM-5.2), against head 52db38988c5384bbd0a3daf85067e72d091de418.

Verdict: no material blocker found. This is a focused and internally consistent fix: successful stores from the same pass remain reported, while quorum shortfalls now surface as Error::PartialUpload instead of an apparent success with chunks_failed > 0.

Verification performed:

  • git diff --check and cargo fmt --all -- --check: clean
  • cargo check -p ant-core: passed
  • both focused #167 regression tests: 2 passed
  • current GitHub build, unit, Clippy, format, docs, security and Merkle E2E checks: passed

The generic macOS/Ubuntu E2E jobs are still pending, so this is a review comment rather than an approval.

Relationship note: #168 contains/supersedes this change. If both are retained, merge #167 first and rebase/collapse #168; merging #168 alone includes the fix.

Nic-dorman added a commit that referenced this pull request Aug 11, 2026
Doc: prepare_merkle_batches_external's cap doc said 2..=MAX_LEAVES;
the clamp (and its floor rationale) is 3..=MAX_LEAVES — aligned with
merkle_batch_sizes_with_cap and merkle_external_batch_cap.

Test: restore the #167-level regression at the seam the spill path
composes — one single-attempt store pass then the deferred rounds —
asserting an all-paid quorum shortfall survives every retry with
stored + failed == total and the exact shortfall set in
failed_addresses (the fold upload_merkle_from_spill turns into
Error::PartialUpload). The unpaid-batch E2E covers the other partial
path; this pins the paid-but-short one without a network.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jacderida
jacderida merged commit 527573b into main Aug 11, 2026
15 checks passed
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.

External-signer merkle finalize reports success on partial chunk storage

3 participants