feat(ffi): resumable external-signer finalize — retain the paid attempt on a partial store - #272
Nic-dorman wants to merge 1 commit into
Conversation
…pt on a partial store A post-payment store shortfall used to strand the on-chain payment: ant-core consumed the PreparedUpload by value, so the paid proofs were gone and a re-prepare produced different quote hashes. ant-core now offers a resumable finalize (ant-client #172), and this wires the FFI surface onto it. - New `session` module: the session map holds either a `PreparedUpload` or the `FinalizeResume` handle a partial finalize returns, under the same upload_id. Generic over the two payloads (neither is constructible outside ant-core) so the state machine is unit-tested offline. - Wave finalize drives `finalize_upload_resumable_with_progress`; merkle drives `finalize_upload_merkle_multi_resumable_with_progress` with the single batch this surface supports. A session already in resume state routes to `finalize_resume_with_progress`. - `FinalizeOutcome::Partial` re-inserts the resume handle and surfaces the existing `ClientError::PartialUpload` with a reason that says to call the same finalize again. `Complete` behaves as before. Public FFI signatures are unchanged, so all language bindings pick this up with doc changes only. - Lossless pre-check: a wave tx map missing the receipt for a paid quote is refused before ant-core can consume the prepared upload. - `payment_transactions` rejects a resume-state session as already paid; `cancel_upload` also abandons a retained partial attempt. - Docs: finalize/cancel/PartialUpload contracts and the mobile external-signer guide now describe the resumable behaviour and a bounded retry loop. - Devnet-gated `devnet_tests` (ignored by default) proves the real prepare → pay → finalize → download path plus the routing error paths. Closes #201 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Live devnet run (dev2, Covers, against a real prepared session: wrong-method finalize and incomplete tx map both refused without consuming the session → |
dirvine
left a comment
There was a problem hiding this comment.
APPROVE — no blockers. Reviewed the full diff at head a31125e, verified against the pinned ant-core (ant-cli-v0.3.6 at dbc01ce), and ran the test suite. This closes #201 correctly.
Verified
- The FFI now uses ant-core's resumable finalisation. A store shortfall returns
FinalizeOutcome::Partial; paid proofs and unstored chunks are retained as aFinalizeResumehandle under the sameupload_id; a repeat finalisation uses the same on-chain payment without re-preparing or re-signing. check_tx_hashes_completevalidates the wave transaction map before consuming the session, so invalid input is lossless.- Re-checked the previously outstanding edge case: its zero-amount exemption exactly mirrors ant-core's
build_paid_chunks. Both require a transaction hash only when!amount.is_zero()(client.rs:1555; ant-corebatch.rs:299-307). - Invalid merkle winner input is decoded before
take, so malformed input does not consume the prepared upload. - Wrong-method and wrong-shape routing failures remove nothing;
payment_transactionscorrectly refuses already-paid resume state.
Tests
cargo test -p ant-ffi --locked passed: 23 passed, 1 ignored (devnet-gated). CI is green across the current matrix.
The session unit tests cover prepared → partial → resume → complete, wrong-method losslessness in both states, cancellation in both states, and refusal of unroutable shapes without removal. The healthy-devnet test cannot naturally force the Partial → resume → Complete leg; that path is covered by the offline session tests and ant-core's upstream coverage.
Non-blocking follow-ups
- A stuck partial retains paid chunk bodies in memory (wave) or the spill directory (merkle) until finalisation succeeds or
cancel_uploadis called. This is documented, but bounded caching remains worthwhile for long-lived mobile clients. peekandtakeuse separate lock acquisitions. Concurrent finalisation of the same ID can give the losing caller a benignunknownerror, buttakeremains atomic and there is no double-payment risk.
Why
External-signer uploads had a "pay once, maybe upload once" footgun (#201).
finalize_upload*handed thePreparedUploadto ant-core by value; if chunk storage fell short after the wallet had paid, the paid proofs were dropped and the payment was stranded — a re-preparecollects new quotes with different quote hashes that can never match the paid tx map. The FFI docs said so honestly ("NOT retryable after payment") as a stopgap.ant-core now has a resumable finalize (WithAutonomi/ant-client#172, in the
ant-cli-v0.3.6tag ant-ffi already pins): a store shortfall comes back asFinalizeOutcome::Partialcarrying aFinalizeResumehandle that owns the paid proofs and the still-unstored chunks. This PR wires the FFI onto it.What
sessionmodule (new). The session map now holds either aPreparedUploador theFinalizeResumehandle a partial finalize returned, under the sameupload_id. It is generic over the two payloads — neither can be constructed outside ant-core — so the state machine has offline unit tests with stand-ins.payment_kind()returnsOptionso an unknown future variant of ant-core's#[non_exhaustive]enums is refused rather than mis-routed.finalize_upload_resumable_with_progress; merkle →finalize_upload_merkle_multi_resumable_with_progresswith the one batch this surface supports (multi-batch prepares are already refused). A session in resume state routes tofinalize_resume_with_progressand ignores the tx map / winner hash.Completeconverts as before.Partialre-inserts the resume handle under the sameupload_idand returns the existingClientError::PartialUpload(counts + spend) with a reason telling the caller to call the same finalize again. Other errors propagate; they are payment-side and consume the session as before.PaymentErrorthat lost the session).payment_transactionsrejects a resume-state session as already paid;cancel_uploadalso abandons a retained partial attempt (dropping the handle frees the chunks).cargo checks clean against it.finalize_upload*,cancel_upload, thePartialUploadvariant, anddocs/mobile-external-signer.mdnow describe the resumable contract, including ant-core's guidance to bound the retry loop and treat a non-shrinkingchunks_failedas stuck (with a Swift loop example).Tests
sessionunit tests: prepared → partial retains a resume entry under the same id; resume → complete removes it; wrong-method finalize on either state is lossless; cancel drops both;payment_transactionson a resume state is refused; an unroutable shape is refused without removal.devnet_tests::devnet_external_signer_wave_round_trip(#[ignore], gated onANT_FFI_DEVNET_MANIFEST): real prepare → wrong-method + incomplete-map refusals leave the session intact → pay the quotes with the devnet wallet exactly as an external signer would → finalize completes through the new path → repeat finalize is "unknown" → bytes round-trip by data map.cargo fmt --check,cargo clippy --all-targets --all-features --locked -D warnings,cargo test --locked→ 23 passed, 1 ignored.Not covered live: a forced mid-finalize store shortfall is not reproducible on a healthy devnet, so the
Partial → retained → resume → Completetransition is proven by the offlinesessiontests only; the ant-core side of that transition has its own unit coverage upstream.Notes
ant-cli-v0.3.6already carries the resumable API.Generated/dir is gitignored; regenerating locally confirms the new doc strings flow through.Closes #201
🤖 Generated with Claude Code