A paid download could redeem a bearer token before discovering that the seller could not read the file. Retrying the same request over another transport could then replay an already-spent token. This change prepares the requested file bytes before redeeming ecash and prevents automatic replay after possible delivery.
Changes
Bring the branch up to current main, preserving the 1.8.21 keyset-ID, mint-fee, refund-reporting, and purchased-content response fixes.
Complete file/range reads before payment verification. Read failures return 503 without redeeming ecash; empty, reversed and out-of-bounds ranges return 416 before payment.
Read restricted FileBrowser uploads through the rootless namespace without changing their permissions. Restrict this fallback to regular files within FileBrowser storage.
Make payment-bearing requests single-delivery: no replay after a timeout, disconnect, or HTTP response; no redirects. Refused connections can still retry, and normal browsing retains its fallback behavior.
Preserve locally verified refund status, bound and label seller error messages, and report interrupted response bodies without claiming an unconfirmed refund succeeded.
Validation
Full isolated backend suite: 1,576 passed, zero failed; four existing hardware/live tests ignored.
Production cargo check passed with the existing 16 warnings.
Root-independent regression cases cover read errors before redemption, deletion/change during payment, invalid ranges, authorization, timeout/disconnect behavior, and redirects.
Scratch-only testing on the development node reproduced a rootless FileBrowser 0640 upload: the host UID could not read it; namespace reading returned identical bytes while preserving 0640 permissions.
Combined testing with #162: 1,585 passed, zero failed; four existing tests ignored.
Limits and release scope
Target the next release after 1.8.21. These changes do not provide a durable payment receipt/recovery protocol: if a seller redeems payment and the network then loses the response, the buyer receives an explicit unconfirmed-refund warning. Live funded Tor-only acceptance remains a release-gate follow-up. Signed 1.8.21 artifacts are unchanged.
## Problem and behavior
A paid download could redeem a bearer token before discovering that the seller could not read the file. Retrying the same request over another transport could then replay an already-spent token. This change prepares the requested file bytes before redeeming ecash and prevents automatic replay after possible delivery.
## Changes
- Bring the branch up to current main, preserving the 1.8.21 keyset-ID, mint-fee, refund-reporting, and purchased-content response fixes.
- Complete file/range reads before payment verification. Read failures return 503 without redeeming ecash; empty, reversed and out-of-bounds ranges return 416 before payment.
- Read restricted FileBrowser uploads through the rootless namespace without changing their permissions. Restrict this fallback to regular files within FileBrowser storage.
- Make payment-bearing requests single-delivery: no replay after a timeout, disconnect, or HTTP response; no redirects. Refused connections can still retry, and normal browsing retains its fallback behavior.
- Preserve locally verified refund status, bound and label seller error messages, and report interrupted response bodies without claiming an unconfirmed refund succeeded.
## Validation
- Full isolated backend suite: 1,576 passed, zero failed; four existing hardware/live tests ignored.
- Production `cargo check` passed with the existing 16 warnings.
- Root-independent regression cases cover read errors before redemption, deletion/change during payment, invalid ranges, authorization, timeout/disconnect behavior, and redirects.
- Scratch-only testing on the development node reproduced a rootless FileBrowser 0640 upload: the host UID could not read it; namespace reading returned identical bytes while preserving 0640 permissions.
- Combined testing with #162: 1,585 passed, zero failed; four existing tests ignored.
## Limits and release scope
Target the next release after 1.8.21. These changes do not provide a durable payment receipt/recovery protocol: if a seller redeems payment and the network then loses the response, the buyer receives an explicit unconfirmed-refund warning. Live funded Tor-only acceptance remains a release-gate follow-up. Signed 1.8.21 artifacts are unchanged.
Review evidence: [next-release PR review](https://source.archipelago-foundation.org/lfg2025/archy/src/branch/main/docs/pr-review-20260930.md).
Paid cloud downloads paid with Minibits ecash were always rejected. The
buyer sends a cashuB token, which carries NUT-02 v2 keyset ids in their
8-byte short form. Minibits rotated its active keyset to a v2 id, and the
seller's verify_and_receive_payment called MintClient::swap directly,
skipping the short->full id repair that only receive_token applied. The
mint answered 422 ("ID length invalid"). The buyer then showed the
misleading "seller doesn't accept your Cashu mint" hint.
Move the repair into swap() so every caller is covered: payment verify,
streaming gate, send change, and cross-mint swaps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After the keyset-id fix, a Minibits paid download still failed and the
buyer lost the sats. What happened, 2026-09-29, amishparadise:
1. The seller redeemed the token, then failed to read the file. It was a
FileBrowser upload owned by the container subuid (100999) with mode
0640. The handler mapped that Err to 404.
2. The buyer's FIPS dial treats 404 as "fall back to Tor" and resent the
request with the same, now spent, token. The seller answered 402, and
the buyer showed "seller doesn't accept your Cashu mint".
Fixes:
- serve_content checks the file is readable before the paid gate. If it
isn't, it grants read with `podman unshare chmod a+r`, which matches
the other shared files. If that also fails it returns Unavailable (503)
without taking payment.
- The content handler returns 500 on internal errors and logs them,
instead of a silent 404.
- New PeerRequest::single_delivery(), used for the paid download: the
FIPS answer is final, FIPS retries only when it never connected, and
there's no Tor replay once the request may have been delivered.
- The buyer shows the seller's error text for non-402 failures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- mint_client: a stub mint shows swap() sends the full v2 keyset id when
given a cashuB short id, and leaves complete v1/v2 ids unchanged.
- fips::dial: the single-delivery decisions are now small functions
(fips_answer_is_final, fips_retryable). Tests cover them and, against a
silent local peer, check that a single-delivery request isn't resent
after a timeout while an ordinary one still is.
- content_server: an unreadable paid file returns Unavailable before the
payment gate runs, and a readable one still returns 402. Also covers
ensure_readable's grant/reopen behaviour. The podman grant is replaced
by a refusal under cfg(test) so results don't depend on the host.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chaum
changed title from fix(ecash): Minibits paid downloads rejected, and buyer charged without the file to fix(ecash): prevent paid-download replay and read failures after charging2026-09-30 11:34:42 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem and behavior
A paid download could redeem a bearer token before discovering that the seller could not read the file. Retrying the same request over another transport could then replay an already-spent token. This change prepares the requested file bytes before redeeming ecash and prevents automatic replay after possible delivery.
Changes
Validation
cargo checkpassed with the existing 16 warnings.Limits and release scope
Target the next release after 1.8.21. These changes do not provide a durable payment receipt/recovery protocol: if a seller redeems payment and the network then loses the response, the buyer receives an explicit unconfirmed-refund warning. Live funded Tor-only acceptance remains a release-gate follow-up. Signed 1.8.21 artifacts are unchanged.
Review evidence: next-release PR review.
Paid cloud downloads paid with Minibits ecash were always rejected. The buyer sends a cashuB token, which carries NUT-02 v2 keyset ids in their 8-byte short form. Minibits rotated its active keyset to a v2 id, and the seller's verify_and_receive_payment called MintClient::swap directly, skipping the short->full id repair that only receive_token applied. The mint answered 422 ("ID length invalid"). The buyer then showed the misleading "seller doesn't accept your Cashu mint" hint. Move the repair into swap() so every caller is covered: payment verify, streaming gate, send change, and cross-mint swaps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>fix(ecash): Minibits paid downloads rejected, and buyer charged without the fileto fix(ecash): prevent paid-download replay and read failures after charging971d477795to971d477795