fix(core): surface partial external-signer merkle uploads as PartialUpload - #167
Conversation
…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>
|
Note on the Security Audit failure: it's pre-existing, not introduced here — this PR changes no dependencies. |
dirvine
left a comment
There was a problem hiding this comment.
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 --checkandcargo fmt --all -- --check: cleancargo 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.
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>
Linear issue
V2-944
Fixes #166.
Risk tier
Compatibility
Client::finalize_upload_merkle(_with_progress)now returnsErr(Error::PartialUpload)when chunks remain short of quorum after all retries, where it previously returnedOkwithchunks_failed > 0. This matches the wave-batch finalize's existing contract andFileUploadResult::chunks_failed's own documented contract ("Always 0 for a successful upload — partial-failure information is conveyed viaPartialUploadinstead"). Callers that inspectedchunks_failedkeep working (it is now always 0 onOk); callers that ignored it — every known one: desktop app, mobile FFI — stop reporting paid, partially stored, unretrievable files as complete.Semver impact
Test evidence
merkle_finalize_resultfold: quorum shortfall →PartialUploadwith correct stored/failed address sets (preflight carry-ins included instored, no address expected for them), and full success → identical previousOkshape withchunks_failed == 0.cargo test -p ant-core --lib: 432/432 pass.cargo clippy --all-targets --all-features -- -D warningsandcargo fmt --all -- --checkclean.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 theOkused to (plus the failed addresses).Notes
PartialUpload::spendis reported as 0 — the external signer pays on-chain out-of-band, so the spend is unknown to the library here (same as theOkarm'sstorage_cost_atto: "0").PartialUploadafter payment still cannot re-store without re-paying the missing chunks — strictly better than silent success, and re-preparing the same file skips chunks already stored.to_external_resultcurrently dropschunks_failed; error propagation makes that moot).🤖 Generated with Claude Code