Skip to content

feat: multi-batch merkle payments (one tx per sub-batch, single allowance) - #213

Merged
Nic-dorman merged 5 commits into
mainfrom
feat/merkle-multi-batch-payments
Aug 13, 2026
Merged

feat: multi-batch merkle payments (one tx per sub-batch, single allowance)#213
Nic-dorman merged 5 commits into
mainfrom
feat/merkle-multi-batch-payments

Conversation

@Nic-dorman

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

Copy link
Copy Markdown
Member

Summary

Consumer half of ADR-0003 (WithAutonomi/ant-client#168): uploads above the 256-chunk merkle tree limit (~1 GiB) quote as multiple sub-batches; the app pays one payForMerkleTree transaction per batch and finalizes with one winner hash per batch. Implements V2-948.

  • Backend: UploadQuoteEvent.merkle_batches (array) replaces the single-batch fields; confirm_upload_merkle(winnerPoolHashes: (string|null)[]) routes through finalize_upload_merkle_multi; the fix: reject partial merkle uploads instead of reporting success #212 completeness guard stays.
  • Frontend: single up-front ERC-20 allowance covering the summed worst-case (ensureAllowanceForMerkleBatches) → the wallet prompts once for approve + once per payment; "Open your wallet and confirm payment n of m" instruction while paying; running cost/gas totals across batches.
  • Partial abandonment: rejecting payment k of N still finalizes the paid batches — their chunks store on the network, and the row reports the incomplete upload (with the saved DataMap path), so a later retry only re-pays the remainder. Nothing already paid is wasted.

Review follow-ups (all resolved — ready for re-review)

  • ✅ Merkle payment timeout race: the 300s withTimeout(payForMerkleTree) is removed — an in-flight payment is never abandoned (fake-timer regression test included). Same fix for the WAVE path landed separately via fix: never time out an in-flight upload payment #214.
  • ant-core now pinned to the released 0.6.0 from crates.io (ant-client#168 shipped in ant-cli-v0.3.3); git pin and draft-pin comment removed, lockfile refreshed.
  • files.stage.approve_payment_n_of_m translated in all 18 locales.

Tests

  • Store tests: two-batch happy path (2 payment calls, both hashes passed to confirm, row completes); abandon-second-payment path (confirm called with [hash, null], row fails with the incomplete-upload error, not "Payment failed"); storage-failure path retained; payment-timeout regression test.
  • Against released ant-core 0.6.0: 76/76 frontend tests, vue-tsc, cargo check/cargo test green.

🤖 Generated with Claude Code

…wance

ant-core 0.6.0 (ADR-0003, WithAutonomi/ant-client#168) lifts the
~1 GiB external-signer cap by preparing uploads above the 256-chunk
tree limit as multiple merkle sub-batches, one payForMerkleTree
transaction each.

Backend: UploadQuoteEvent carries merkle_batches (one depth/
commitments/timestamp entry per sub-batch) instead of the single
batch's fields; confirm_upload_merkle takes one winner hash per batch
(null = user never paid it) and finalizes through
finalize_upload_merkle_multi, keeping the ensure_all_chunks_stored
guard.

Frontend: one up-front allowance approval covering the summed
worst-case charge (ensureAllowanceForMerkleBatches), then one payment
tx per batch with a "confirm payment n of m" wallet instruction
(stageDone/stageTotal). Abandoning a later payment still finalizes
the paid batches — their chunks store and the row reports the
incomplete upload with the DataMap path, so a retry only re-pays the
remainder.

DRAFT: pins ant-core to the ant-client#168 branch; flip to the 0.6.0
release before merge. approve_payment_n_of_m locale key is en-only —
run the locale sweep before merge.

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.

Paired review against ant-ui head a5e45eaf5d2591ffb610c1751d740989bdae361f and the merged ant-client #168 head 300479257927ddb15a49e8203dd75af43b6e9245.

Verdict: implementation is coherent with #168; one dependency-pinning item should be resolved before this leaves draft.

The cross-repo contract matches: the UI serialises each prepared Merkle sub-batch, pays in order, preserves null for unpaid batches, and passes the ordered vector to finalize_upload_merkle_multi_with_progress. Partial payment and post-payment quorum shortfall are both surfaced as incomplete uploads rather than false success or payment failure.

Before merge, replace the branch dependency in src-tauri/Cargo.toml with the released ant-core version (the comment already anticipates 0.6.0) and refresh Cargo.lock. The current lock resolves #168 at dbc9ca0, not its merged head; the production difference is not material to this UI contract, but a mutable feature-branch dependency is not a durable release input and may disappear after merge.

Verification:

  • independent six-seat paired review across both repositories
  • local frontend suite: 74/74 passed; vue-tsc --noEmit passed
  • local Tauri library tests: 18/18 passed; Clippy with warnings denied and formatting passed
  • #168 CI is fully green, including macOS/Linux builds, unit/E2E/Merkle E2E, Clippy, format, docs, security and ADR validation
  • ant-ui #213 GitHub checks all passed; git diff --check 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 the earlier paired review after a completed two-group, six-seat rerun against ant-ui a5e45eaf5d2591ffb610c1751d740989bdae361f and merged ant-client #168 head 300479257927ddb15a49e8203dd75af43b6e9245.

Final verdict: changes requested before merge.

The cross-repository multi-batch contract is coherent: prepared batches serialize compatibly, winner hashes retain batch order, null represents unpaid batches, and #168's finalizer/store lifecycle is sound. However, the payment timeout has an ambiguous-execution failure boundary: withTimeout(payForMerkleTree(...)) can reject after writeContract has broadcast but before receipt polling returns. The underlying promise continues and may complete on-chain, but its winner hash is discarded. If this happens on the first batch, paidCount === 0 reports a clean payment failure and never calls confirm_upload_merkle; retrying obtains a fresh quote and can pay again. In a multi-batch flow this boundary is crossed once per payment, so #213 should not ship relying on the timeout result as proof that nothing was spent.

I reproduced the promise-race mechanics deterministically: the outer flow failed while broadcast=true; the receipt arrived later, after the settled outer promise could no longer recover its result.

Required before merge:

  1. Remove the outer timeout around the broadcast/receipt operation, or preserve the transaction hash and reconcile its receipt/result before classifying the batch as unpaid. Add a fake-timer test for timeout-after-broadcast-before-receipt and assert that retry cannot repay an ambiguously executed batch.
  2. Replace the mutable ant-core feature-branch dependency with a released version and refresh Cargo.lock. cargo search ant-core currently reports only 0.5.1, so this may mean waiting for the release anticipated by the Cargo comment. The current lock resolves commit dbc9ca0, not merged #168 head 3004792.

Verification:

  • 6/6 review seats completed; the payment finding was independently reproduced by the parent reviewer
  • frontend: 74/74 tests and typecheck passed
  • Tauri: 18/18 tests, Clippy (-D warnings), fmt and diff checks passed
  • current GitHub checks are green

Comment thread stores/files.ts Outdated
// stageDone/stageTotal drive the "payment i of N" wallet
// instruction while status is `paying` (see pages/files.vue).
this.updateEntry(id, { stageDone: i, stageTotal: batches.length })
const payResult = await withTimeout(

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.

Blocker — ambiguous transaction execution: this timeout races the full payForMerkleTree promise, which includes broadcast and receipt polling. If 300s expires after broadcast but before the receipt returns, the eventual winnerPoolHash is discarded. On the first batch paidCount remains zero, so the flow reports a clean payment failure and never finalises the paid batch; retry can pay a fresh quote again. Please remove this outer timeout or retain/reconcile the tx hash before treating the batch as unpaid, with a timeout-after-broadcast regression test.

Nic-dorman and others added 2 commits August 12, 2026 09:50
payForMerkleTree spans broadcast and receipt polling; racing it against a
300s timer could discard a winner hash the chain had already accepted,
report the batch as a clean payment failure, and let a retry pay it
again. Drop the outer timeout in the multi-batch loop — the wallet's own
reject is the cancel path — and pin the behaviour with a fake-timer
regression test (receipt at +400s must complete, not fail).

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

Copy link
Copy Markdown
Member Author

Review item 1 addressed in 9a8429e (option 1 — timeout removed): the merkle batch loop no longer races payForMerkleTree against a timer, so a winner hash can never be discarded between broadcast and receipt; the wallet's own reject remains the cancel path. Added the requested fake-timer regression test: receipt arriving at +400 s must leave the row paying at +300 s (not "Payment failed"), preserve the hash, and complete without ever re-paying the batch. Frontend 75/75 + vue-tsc clean; branch also merged with main (keeps the #212 guard).

Item 2 (replace the git pin with a released ant-core + lock refresh) is pending the ant-client release cut — the PR stays draft until then.

The same broadcast/receipt race in the wave path (inherited from base, reproduced in your #212 review) is tracked separately as a follow-up.

🤖 Generated with Claude Code

Nic-dorman and others added 2 commits August 12, 2026 10:49
…ch-payments

# Conflicts:
#	stores/files.ts
#	tests/stores/files.test.ts
…stage label

Replace the draft git-branch pin (ant-client#168 is released in
ant-cli-v0.3.3) with the crates.io ant-core 0.6.0 dependency, and add
the approve_payment_n_of_m stage string to all 17 non-English locales.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Nic-dorman
Nic-dorman marked this pull request as ready for review August 13, 2026 07:43
@Nic-dorman
Nic-dorman requested a review from dirvine August 13, 2026 07:44
@Nic-dorman

Copy link
Copy Markdown
Member Author

Both requested changes are in — ready for re-review:

  1. Payment timeout race: withTimeout around payForMerkleTree removed in 9a8429e (in-flight payments are never abandoned; fake-timer regression test added). The inherited WAVE-path instance was fixed separately in fix: never time out an in-flight upload payment #214 (merged).
  2. Release pin: ant-client#168 shipped yesterday in ant-cli-v0.3.3, publishing ant-core 0.6.0 to crates.io — 7395170 flips the git pin to ant-core = "0.6.0" and refreshes the lockfile. Same commit adds the approve_payment_n_of_m string to all 17 non-English locales.

Verified against the released crate: 76/76 frontend tests, vue-tsc, cargo check/cargo test all green.

@Nic-dorman
Nic-dorman merged commit 4a9697a into main Aug 13, 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