Repository navigation
feat(browser): export manifests and recover pointer payments - #230
mickvandijke wants to merge 9 commits into
Conversation
dirvine
left a comment
There was a problem hiding this comment.
Review — manifest export + pointer payment recovery (T2)
Reviewed at head 8316468 (base 225a6768; 4 files, +350/−5) via the standing review panel (GLM 5.3, Qwen 3.8 27B, DS4 Flash): a correctness round completed 3/3 with substantive reviews, plus a dedicated adversarial round that completed 1/3 (Qwen only; GLM and DS4 seats returned incomplete terminations after three attempts, so that round is provisional — noted at the end). Outcome: no severe findings; approve with caveats. Severe bar applied: silent data loss, stuck node, registry corruption, backwards-compat break, silent failure misclassification.
Verified against source on the exact head:
- No double payment.
recoverPointerPayment(ant-core/src/browser/wasm_transport/pointer.rs:583+) is offline: it re-derives the paid proof from the checkpoint's plan plus an observed receipt via the existingwallet_quotes→paid_transactions→plan.proofhelpers, then hands{record, proof}back for the unchangedpointer_put_paidpath. It never quotes, signs, or submits a transaction; the checkpoint contains no owner seed. - Receipt trust is not new surface. The adversarial seat flagged that a forged receipt could mint a proof for unpaid state. Adjudicated: the proof only carries the tx-hash claim — storers verify it on-chain before accepting the record (
ant-protocol payment::single_node::verify_paid_quote: on-chain amount ≥ quote price, correct rewards address). A fabricated tx hash fails at store time, and this is the same wallet/receipt trust boundary the upload adapter already uses. Not severe; no change needed here. - Checkpoint validation. Version 1 only,
deny_unknown_fields, 4 MiB cap,Pointer::from_bytessignature check, and plan/record consistency (plan.address == record.state_id(),plan.data_size == record.len()). - ABI/back-compat.
createPointer/updatePointer/transferPointergain an optional trailingonPreparedcallback; omitted →Nonefor existing JS callers. New exports are additive. NoCargo.toml/Cargo.lockchanges. - Manifest. Embedded DataMaps are capped at
MAX_MANIFEST_BYTESand must use canonical MessagePack (re-encoded and byte-compared); public entries require a valid file link. Minor note:name/paths have no per-field size bound — application-supplied, non-severe. - Hygiene.
cargo fmt --all --checkclean on head; no whitespace errors; no dependency changes.
Caveats (non-blocking):
- Security Audit fails — inherited, not introduced by this PR. Four rustls 0.23.45 advisories (RUSTSEC-2026-0346…0349, published 2026-10-14; fix: rustls ≥ 0.23.46). This PR changes no Cargo files, and main's last CI run (2026-09-28) predates the advisories — main would fail the same audit today. Suggest a separate lockfile bump on main.
- E2E Tests (macos-latest) pending at review time. Everything else green on this exact head (Real Chromium + local nodes, unit tests on both OSes, Merkle E2E, Browser WASM, clippy, fmt, docs).
Panel participation, for the record: correctness round — GLM 5.3, Qwen 3.8, DS4 all returned substantive approve-with-caveats reviews, no severe findings (3/3). Adversarial round — Qwen only (1/3, provisional); the GLM and DS4 seats did not terminate within limits after retries. Qwen's single candidate severe finding is the receipt-forgery item adjudicated above.
Verdict: no blocking findings. Merge after the macOS E2E check completes; treat the rustls bump as a main-branch follow-up, not a gate for this PR.
Rebuild the bundled WASM from WithAutonomi/ant-client#230 at bab6beb with the same rustc 1.99.0, notices and std copyright. - The core's pointer checkpoint now holds its record in base64url and its plan in base64url MessagePack. Re-encode the devnet fixture's core checkpoint into that layout from the same signed record and plan; the receipt and expected paid state are as captured. Rewrite the tamper test for the new layout, and check that the untouched checkpoint recovers, so each refusal is caused by its edit. - recoverPointerPayment now takes a single transactionHash for the plan's quotes, as the write does, so pass receipts through instead of keying them in the SDK. Cover both forms against the real devnet checkpoint. - Document that storers refuse a paid proof once its quotes are a day old. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Manifests:
- The shared encoder refuses an embedded DataMap without chunks, which
the decoder already refused, so no writer can produce an unreadable
.ant file or link.
- Browser input follows native writer policy: a root DataMap is embedded
only up to MAX_EMBEDDED_ROOT_MAP_BYTES, and a size hint must match a
root map's exact size.
- Torrent hashes use the shared parser; the duplicate entry-count check
is gone.
- canonicalize() computes effective names once, and the browser view
derives each address once. Path-less embedded entries self-encrypt
their DataMap far fewer times.
- CreatedManifest and the manifest views derive Deserialize.
Pointers:
- create/update/transfer take a trailing { onPrepared, onPaid } options
object instead of a seventh positional callback. A bare function is
still taken as onPaid, and unknown options are refused before paying.
- The settlement-refusal check runs again just before the wallet is
invoked, after onPrepared may have waited.
- recoverPointerPayment requires the receipt's transactionHashes to name
the checkpoint's quote, no longer needs totalAmount, and reports a
newer checkpoint as an unsupported version.
- A checkpoint recovery could not read is refused before payment, and
the plan is no longer cloned to serialize it.
- Documented that onSubmission records nothing for pointer writes, so
the wallet must keep the broadcast hash; restored the pay-twice
warning.
- WASM tests cover checkpoint ordering, callback failure, option
validation, the refusal re-check and recovery.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pointers:
- Pointer writes take `options?: { onPrepared, onSubmission, onPaid }`
(`Option<JsValue>`), so the TypeScript parameter is optional again.
- New `onSubmission` option: the core forwards every transaction the
wallet reports broadcasting, and the wallet waits on it. A page lost
between broadcast and receipt can therefore recover without each
wallet keeping the hash itself.
- recoverPointerPayment accepts `{ transactionHash }` as well as keyed
`{ transactionHashes }`, exactly as the write does, so a reported
submission or a single-hash receipt recovers. A keyed receipt must
still name the checkpoint's quote.
- onPrepared is bounded by the 30 s callback timeout (shared with
onPaid). Every failure before the wallet is invoked, including the
re-checked settlement refusal, ends "payment was not submitted";
nothing is paid unless the wallet is called.
- The checkpoint stays a versioned JSON string, but the record is
base64 and the plan base64 MessagePack rather than JSON number arrays:
87,297 instead of 156,250 characters against the mock close group.
- storePaidPointer says a proof expired when a refused proof's quotes
are past the storers' age limit, rather than a generic store error.
- A failure building onPaid's argument can no longer abort a paid write.
Manifests:
- ManifestInput::create returns a typed thiserror ManifestInputError
instead of String.
- The entry count is checked again before any entry is parsed.
- createManifest checks top-level and torrent keys itself:
serde_wasm_bindgen reads only declared struct fields, so
deny_unknown_fields never refused unknown keys from JavaScript.
- Validation and sorting run once: the bytes and link form are encoded
through Manifest::encode_canonical.
- ManifestEntry::known_size sums chunk sizes as u64, so it no longer
overflows past 4 GiB on wasm32. The browser view names entries
through ManifestEntry::effective_name_with instead of a copy of the
rule.
- Documented that an embedded root DataMap must be one an upload shrank,
since the link carries its shrunk form.
WASM tests cover createManifest/createFileLink input from JavaScript,
onSubmission ordering and recovery from it, single-hash recovery, and
the onPrepared timeout. ADR-0007 is updated to match.
BREAKING CHANGE: createPointer, updatePointer and transferPointer no
longer accept a bare function as their last argument; pass
`{ onPaid }` instead. ManifestInput::create now returns
`Result<CreatedManifest, ManifestInputError>` instead of
`Result<CreatedManifest, String>`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pointers: - onSubmission is bounded by the callback timeout and never rejects to the wallet: the transaction is already broadcast, so a page that cannot keep it must not hold up or fail the payment, as for onPaid. - The write waits for every reported broadcast to be kept before it goes on or fails, whether or not the wallet waited, as the upload adapter does. - Write failures are a typed PointerWriteError split at the wallet call: every failure before it, including invalid input, an existing pointer and a refused settlement, ends "payment was not submitted"; later failures are reported as they are. - write_pointer takes the write with its target, dropping the too_many_arguments exemption. - storePaidPointer notes a proof past the storers' real 24 h quote age (no safety margin), by this device's clock, alongside the storer's own error, instead of replacing it with a pay-again instruction. batch::paid_proof_has_expired is public and ungated, so no build config leaves its proof import unused. - Restored the warning that a write with neither onPrepared nor onSubmission keeps nothing to recover from. Manifests: - One embedding policy, data::manifest::is_embeddable, serves native and browser writers: a small root map, or a published map of at most MAX_PUBLISHED_MAP_CHUNKS. Browser input refuses an intermediate shrink level. - createManifest reads accepted keys from each input struct's serde derive (struct_fields) instead of hand-kept lists. - The created view takes addresses from the link form, whose maps are already shrunk, instead of shrinking each embedded map again. ADR-0007 is updated and listed in the ADR index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A real-Chromium test against the pinned devnet nodes and Anvil. It pays once for a pointer, loses the page after the payment confirms and before the state is stored, and rebuilds the paid state with recoverPointerPayment from the onPrepared checkpoint and the transaction onSubmission kept. It then stores that state with storePaidPointer and reads it back, with no second payment. A write refused before the wallet is checked to say "payment was not submitted". The test also builds a manifest with createManifest and writes the .ant file and link to its output directory for the native CLI. The two pointer tests now share one local-chain wallet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main now records the creator's SHA-256 on manifest entries (#229, V2-1564), and decodeManifest already shows it. createManifest entries take an optional hex `sha256` as well, so a page that hashes a file can record it just as a native writer does. It is the creator's claim and must be 32 bytes of hex; the browser computes none itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4d27758 to
404fa34
Compare
Rebuild the bundled WASM from WithAutonomi/ant-client#230 at 4d27758 with the same rustc 1.99.0, notices and std copyright. The JS API is unchanged; only doc comments and export order differ. - storePaidPointer no longer replaces a storer's refusal of a day-old proof with "paid proof expired": it reports the storer's error, noting when this device's clock puts a quote past the age storers accept. Document that instead. - The core's onSubmission bounds and pointer write errors need nothing here: the SDK's pay function does not call the core's onSubmission, and nothing matches "payment was not submitted". createManifest now refuses an intermediate shrunk DataMap, which no upload returns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reject_unknown_keys took its object with dyn_ref, which tests `instanceof Object`. An object without Object.prototype, such as one from Object.create(null), or an object from another frame fails that test, so its keys went unchecked. For pointer write options that meant a misspelt `onPrepared` was ignored and the write paid with no checkpoint kept; createManifest likewise accepted unknown top-level and torrent keys. Any value JavaScript reports as an object is now checked. Tests cover both kinds of object for createManifest and the pointer options. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t contents The pointer recovery paragraph in the browser-tests README had split the paid upload paragraph from its last sentence. ADR-0007 now says what a checkpoint holds, a signed record and a quoted payment plan, and that the transaction evidence is in the kept submissions and receipts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebuild the bundled WASM from WithAutonomi/ant-client#230 at 404fa34, now rebased onto main, with the same rustc 1.99.0 and std copyright. The rebase brings ant-protocol 5.0.0 from crates.io instead of the pinned 4.0.0 revision, saorsa-core 0.29.1 and saorsa-transport 0.38.1; the notices follow. The JS bindings change only in export order. Manifest entries may now record the creator's SHA-256 of the plaintext (ant-client #229, ADR-0006), and createManifest accepts one as hex. - ManifestEntry carries it as optional hex `sha256`; the SDK dropped the field the core returns, so CLI manifests lost it when read here. - ManifestEntryInput takes an optional `sha256`, which Rust validates. - The SDK surfaces the hash and does not check it: the CLI checks it on extraction, which the browser has none of, and downloads are verified by content address. ADR-0007 and the README say so. - Tests decode ant-client's golden fixture with a sha256, carry one through createManifest, decode and link, and refuse malformed ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Browser applications can read manifests but cannot export them through the shared WASM API. Pointer writes also lose recoverable state if a page disappears between payment submission and its receipt. This change exports canonical manifest creation and file links. It adds an awaited checkpoint of the signed pointer state and verified plan before payment, forwards each transaction the wallet broadcasts to the page, and rebuilds the paid proof from the original plan and an observed transaction without submitting another one.
The companion SDK PR #7 consumes these exports for typed pointer recovery, manifest export and streaming saves. Merge this PR first; the SDK must then rebuild WASM from ant-client main before release. Since
b44ebfbits manifest entries and inputs carry the newsha256.Rebased onto
main(e47ea7a) on 2026-10-19 for #229, which addssha256to manifest entries; each rebased commit compiles on its own. Hashes below are the rebased commits; evidence at earlier commits names their pre-rebase hashes.Second review follow-up (
b97d4aa, breaking):createPointer/updatePointer/transferPointertakeoptions?: { onPrepared, onSubmission, onPaid }(Option<JsValue>), so the TypeScript parameter is optional again. A bareonPaidfunction is no longer accepted. Unknown options are still refused before anything is paid.onSubmission: the core forwards each transaction the wallet reports broadcasting, and the wallet waits on it. A page lost between broadcast and receipt can recover without every wallet keeping the hash itself.recoverPointerPaymenttakes{ transactionHash }as well as keyed{ transactionHashes }, exactly as the write does. A reported submission or a single-hash receipt therefore recovers; a keyed receipt must still name the checkpoint's quote.onPreparedis bounded by the 30-second callback timeout it now shares withonPaid. Nothing is paid unless the wallet is invoked, and every failure before that, including the re-checked settlement refusal, ends "payment was not submitted".storePaidPointersays a proof expired when its quotes are past the storers' age limit.ManifestInput::createreturns a typedManifestInputError. The entry count is checked before any entry is parsed.createManifestchecks top-level and torrent keys itself, becauseserde_wasm_bindgenreads only declared struct fields, sodeny_unknown_fieldsnever refused unknown keys from JavaScript. Validation and sorting run once.known_sizesums asu64, so it no longer overflows past 4 GiB on wasm32.Third review follow-up (
5839239):onSubmissioncannot block a paid write: it is bounded by the callback timeout and never rejects to the wallet. The write waits for every reported broadcast to be kept, whether or not the wallet waited, as the upload adapter does.PointerWriteErrorsplits at the wallet call. Every earlier failure, including invalid input, an existing pointer and a refused settlement, ends "payment was not submitted".write_pointerno longer needs atoo_many_argumentsexemption.storePaidPointernotes a proof past the storers' real 24-hour limit, by the device's clock, alongside the storer's own error, rather than replacing it.is_embeddableserves native and browser writers. Browser input refuses an intermediate shrink level, so it accepts only a small root map or the published map of at most three chunks.createManifestreads accepted keys from each input struct's serde derive, and the view takes addresses from the already-shrunk link form. ADR-0007 is added to the ADR index.Real-devnet test (
cb38005): a real-Chromium test that loses a pointer payment's page after the payment confirms, recovers the paid state from theonPreparedcheckpoint and theonSubmissionhash, and builds a manifest for the native CLI. CI's real-browser job runs it.SHA-256 on created entries (
404fa34):createManifestentries take an optional hexsha256, recorded as the creator's claim just as a native writer records it. It must be 32 bytes of hex; the browser computes none itself. The manifest view already shows it since #229.Fourth review follow-up (
93f3bd2,f96c2fb): unknown keys are checked on any JavaScript object. The check testedinstanceof Object, which an object fromObject.create(null)or from another frame fails, so such an object's keys went unchecked. A misspeltonPreparedin one was ignored, and the write paid with no checkpoint kept. The browser-tests README and ADR-0007's account of what a checkpoint holds are also corrected.The first review follow-up (
36b0162) addressed 15 earlier findings: the encoder refuses an embedded DataMap with no chunks, browser input follows native embedding and size-hint policy, torrent hashes use the shared parser, the settlement refusal is re-checked before the wallet, and the "retry pays again" warning was restored.Linear issue
Closes V2-1478
Closes V2-1354
Browser follow-ups to the manifest work in #219 and pointer transfer work in #210, using their existing Linear references.
Risk tier
Proposed for human confirmation: browser checkpoint and recovery behavior changes; existing quote verification, payment amounts, signed records and storage rules remain in the shared core.
Compatibility
onPaidfunction as their last argument; pass{ onPaid }.ManifestInput::createreturnsResult<CreatedManifest, ManifestInputError>instead ofResult<_, String>. Additive: public Rust manifest-input types, WASMcreateManifest(with an optional per-entrysha256),createFileLinkandrecoverPointerPaymentexports, theonPreparedandonSubmissionoptions, andManifestEntry::effective_name_with. A suppliedonPreparedmust settle successfully within 30 seconds before payment. Once paid, anonPaidfailure still cannot prevent storage. Recovery takes the application's original checkpoint and a receipt or reported submission; it does not authorize fresh payment.BrowserManifestViewand its entries also deriveDeserialize, withkindas aString.Semver impact
Proposed for human review: the bare-function
onPaidform and theStringerror ofManifestInput::createare removed.Test evidence
At
f96c2fb:cargo test -p ant-core --lib: 854 passed, including tests for the entry-count order, serde-derived input field names, published-map embedding, link-form addresses,u64known_sizeoverflow, proof expiry and thesha256input.ant-core/wasm-tests,browser-wasm,test-utilsrelease build): 237 passed. Tests cover:createManifest/createFileLinkwith JavaScript input: unknown top-level, torrent and entry keys, including on objects without a prototype or from another realm, tag casing,Uint8ArrayDataMaps, number and BigInt sizes, a recorded and a malformedsha256.onSubmission: ordering, recovery from a reported submission, a hanging or failing callback not stopping a paid write, and a wallet that does not wait on it.onPreparedthat never settles, the bare-function form being refused, and unknown options on objects without a prototype or from another realm.cargo fmt --all -- --check, workspace clippy (--all-targets --all-features) and wasm32 clippy for bothbrowser-wasmandbrowser-wasm,test-utils, with-D warnings: passed.browser-wasm(neither native nor wasm32) still fails clippy on dead code inclient_engine/read_budget.rsanddata/client/mod.rs. Both predate this PR, and it adds nothing to that list.f96c2fb. At404fa34, these jobs passed: Real Chromium and local nodes, Browser WASM, unit tests, builds, clippy, format, documentation, Merkle E2E (macOS) and the PR checks. E2E Tests (macOS) failed intest_estimate_all_stored_partial_sample_is_incomplete, because the native 10-node test network's close-group lookup found 6 of the 7 peers it needed. That is upload code this PR does not touch; the Ubuntu E2E jobs had not finished. Security Audit fails on threezip2.4.2 advisories (RUSTSEC-2026-0367, -0368, -0369, published 2026-10-09 and -10) inmain's lockfile; this PR changes no Cargo files. (chore(deps): move rustls to 0.23.46 for four RustSec advisories #232 fixed the rustls advisories it failed on at the pre-rebase973ae93.)Real devnet at
404fa34(the later commits change only the unknown-key check, tests and docs): Chromium 151.0.7922.34 (Playwright 1.62.1), seven isolated nodes at the pinned26129a2611cac08f9f53a557f91fb8806b40a1cc, and local Anvil 1.7.1. It used the production WASM (browser-wasmonly) built from404fa34, sha25627719b21bc2136bf59e18365a31a5e5364735ce95f098f508380459ca0fc8223. The same suite also passed at the pre-rebase4d27758.final; and the new recovery test.recoverPointerPaymentrebuilt the paid state from the checkpoint and the reported transaction hash,storePaidPointerstored it, and a fresh client read it back at counter0with the expected target. A second create was refused with "already exists … payment was not submitted".ant --json manifest showbuilt from404fa34decoded the browser-created.antfile (sha25627f1d667bec142416240b2894732591b36f0c1c4895d3adc9207f88ab79468e4) and its link identically: name, torrent v1 info hash, entry path, public address and size.SDK PR #7 adopted
404fa34inb44ebfb, with WASM rebuilt from it: SDK build, TypeScript, 250 tests in 23 files (including the real devnet checkpoint, re-encoded for the new checkpoint layout, recovered and resumed through the core), all five example builds,npm pack --dry-runand ADR governance passed; itsverify:wasmfails only because404fa34is not onmain. It passes receipts torecoverPointerPaymentunkeyed, and its manifest entries and inputs carrysha256. It keeps its own submission journal: its payment function is the wallet the core calls, so the core'sonSubmissionwould only hand its broadcasts back, and the journal also holds what recovery observes.93f3bd2andf96c2fbneed no SDK change. Its real-browser devnet run has not been repeated against this WASM.Before the review follow-ups, at
8316468b7f5f813457f195bccb16a452426d5f35(exact revisions, commands, limitations and artifacts):26129a2611cac08f9f53a557f91fb8806b40a1cc, with local Anvil: paid pointer create/update/transfer, all-seven finality, paid replay, and recovery after deliberately losing confirmation with exactly one payment submission. Browser-created public/private manifests decode in the unchanged native CLI. This run predates the options object,onSubmissionand the current checkpoint encoding. The run at404fa34repeats the recovery and native decoding at the current head, but not the private manifest.New dependency
None. Cargo manifests and lockfile are unchanged.
ADR
Proposed ADR-0007: browser manifest creation and pointer payment recovery, updated for
onSubmission, single-hash recovery, callback timeouts, typed write failures, the shared embedding policy, the removed bare-function form and thesha256input, and listed in the ADR index. No Accepted ADR was edited; acceptance requires human engineering review.Mitigation / rollback
Revert the new exports and regenerate consuming WASM artifacts if needed; retain all pointer checkpoints, submitted-transaction evidence and paid proofs until reconciled. Never treat rollback or a missing receipt as authorization to pay again. Existing stored files and pointer states remain readable by compatible clients.
🤖 Generated with Claude Code