Skip to content

fix: reject partial merkle uploads instead of reporting success - #212

Merged
Nic-dorman merged 2 commits into
mainfrom
bug/merkle-partial-upload-false-success
Aug 12, 2026
Merged

fix: reject partial merkle uploads instead of reporting success#212
Nic-dorman merged 2 commits into
mainfrom
bug/merkle-partial-upload-false-success

Conversation

@Nic-dorman

@Nic-dorman Nic-dorman commented Aug 11, 2026

Copy link
Copy Markdown
Member

Problem

Merkle payments (auto-selected for uploads of >=64 chunks) could report success while not all chunks were actually stored. The user pays on-chain, the app marks the row complete, persists the DataMap, and shows a shareable address — for a file that cannot be fully downloaded.

Root cause is upstream: ant-core's external-signer merkle finalize returns Ok with chunks_failed > 0 when chunks stay short of quorum after all retries, instead of Err(PartialUpload) like the wave-batch path (WithAutonomi/ant-client#166, fixed by WithAutonomi/ant-client#167). The app then dropped chunks_failed/total_chunks on the floor and emitted complete unconditionally.

Fix

Backend — new ensure_all_chunks_stored() guard, called in all three upload commands (confirm_upload_merkle, confirm_upload, wallet_upload) after the DataMap is persisted:

  • rejects when chunks_failed > 0 or chunks_stored < total_chunks, so the invariant holds even if a core path miscounts
  • runs after write_datamap_for so the user keeps the DataMap handle; the error message includes the stored/total/failed counts and the DataMap path, and notes that re-uploading the same file skips already-stored chunks (prepare's preflight detects them)
  • the wave and wallet paths already get Err from core on partial failure — there the guard is stated defense in depth

Frontend — the merkle branch's try/catch wrapped both the payment and confirm_upload_merkle, so a storage failure surfaced as "Payment failed: …". The catch now wraps only the on-chain payment; confirm rejections fall through to the outer catch and surface as an upload failure, mirroring the wave-batch path.

ant-core 0.5.0 → 0.5.1 bump

Second commit bumps to the current ant-core. 0.5.1 is a merkle pricing release, so this is user-visible in the right direction:

  • merkle pool commitments now carry the 3× median multiplier the single-node path always applied — merkle payments and cost previews rise to the correct network price (previously merkle underpaid nodes 3×)
  • the estimator bills the padded tree (median16 × 2^depth; 65 chunks pay for 128 leaves), so cost previews match what the contract actually charges
  • an external-signer batch over MAX_LEAVES (256 chunks ≈ 1 GB) is now refused cleanly at quote time (MerkleBatchTooLarge) instead of attempting a split the one-signature contract cannot express

No frontend change needed — the app signs whatever pool commitments the backend hands it.

Tests

  • 3 Rust unit tests for the guard (complete passes; shortfall rejected with counts + DataMap path; chunks_failed == 0 shortfall still rejected)
  • New store test driving the merkle flow with a mocked payment: confirm_upload_merkle rejection marks the row failed with the storage error, not a payment failure
  • Full suite re-run against 0.5.1: 72/72 frontend tests, nuxi typecheck, cargo test/clippy/fmt all green

Once ant-client#167 ships in a release, finalize itself will error and the backend guard becomes a no-op safety net — no changes needed here.

🤖 Generated with Claude Code

ant-core's external-signer merkle finalize returns Ok with
chunks_failed > 0 when chunks stay short of quorum after retries
(WithAutonomi/ant-client#166), so a paid, partially stored — and
unretrievable — file was shown as complete with a shareable address.

Backend: ensure_all_chunks_stored() guards all three upload commands
after the DataMap is persisted, so a shortfall errors with the counts
and the saved DataMap path (re-uploading the same file skips chunks
already stored).

Frontend: the merkle branch's catch now wraps only the on-chain
payment; confirm_upload_merkle rejections fall through to the outer
catch and surface as an upload failure instead of "Payment failed",
mirroring the wave-batch path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0.5.1 is a merkle pricing release: the payable amounts in pool
commitments now carry the same 3x median multiplier as the
single-node path (merkle previously underpaid nodes 3x), the cost
estimator bills the padded tree (median16 x 2^depth — 65 chunks pay
for 128 leaves) instead of raw chunk count, and an external-signer
batch over MAX_LEAVES (256 chunks, ~1 GB) is now refused cleanly at
quote time with MerkleBatchTooLarge instead of attempting a split
the one-signature contract cannot express.

User-visible: merkle upload payments and cost previews rise to the
correct network price; the app signs what the backend hands it, so
no frontend change.

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

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

Review against head ed3d9c46f1e7b371223dca32ced1b04afee6314c.

Verdict: no material blocker found.

The incomplete-upload guard is applied consistently to external wave-batch, external Merkle, and wallet uploads, after the DataMap is persisted and before the UI emits completion. The frontend now correctly distinguishes a post-payment storage shortfall from a payment failure, while preserving retry guidance.

Verification:

  • independent six-seat review across correctness, payment/storage lifecycle, UI state, API compatibility, tests, and adversarial failure handling
  • local frontend suite: 74/74 passed; vue-tsc --noEmit passed
  • local Tauri library tests: 18/18 passed; Clippy with warnings denied and formatting passed
  • git diff --check passed
  • all current GitHub checks passed

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

Superseding review after a completed two-group, six-seat rerun against exact head ed3d9c46f1e7b371223dca32ced1b04afee6314c.

Final verdict: no material blocker in #212.

The panel independently confirmed that ensure_all_chunks_stored rejects either a non-zero failure count or a stored/total shortfall, and that all three successful upload-result paths pass through the guard before completion is persisted/emitted. The frontend also preserves post-payment storage failures as upload failures rather than mislabelling them as payment failures.

One reviewer challenged the 300-second withTimeout payment race. I reproduced that race, but it is inherited unchanged from base 9827d28 and is not introduced by #212. It should be fixed in the payment flow; it does not alter this PR's partial-upload correctness verdict.

Fresh verification on this exact head/descendant integration state:

  • review panel: 6/6 seats completed; no #212 blocker
  • frontend: 74/74 tests passed; vue-tsc --noEmit passed
  • Tauri: 18/18 library tests passed; Clippy (-D warnings), fmt and git diff --check passed
  • all current GitHub checks passed

@Nic-dorman
Nic-dorman merged commit 3f5b76d into main Aug 12, 2026
4 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.

2 participants