in reviewhumanPR #298Payments
[Fellowship Challenge] payments: hashlock.v1, sealed digital goods and payment change hands in one step
Submission for the Hexaware x Project NANDA Fellowship Technical Challenge
Author
@saisreekantam
github profile →- Status
- In review
- Opened on
- Oct 1
- Branch
- challenge/saisreekantam-sealed-exchange
Description
The pitch.
Submission for the Hexaware x Project NANDA Fellowship Technical Challenge
**Demo video:** https://www.youtube.com/watch?v=QVAOcTkux3Y
**Track:** Lab · **Layer:** payments. The PR adds `hashlock.v1`, and `deadline.v1` as its time half and negative control. `ledger.v1` and every existing scenario are unchanged; their event traces are identical to `main`.
## The gap
Escrow in the Lab can hold the money, but nothing ties the money to the goods. Open PR #291 (`leased.v1`) fixes one half of that: a hold that is never released is refunded at a deadline, so money is not stranded.
A deadline refund opens the other half. If the goods arrive after the refund, the buyer keeps the goods *and* the money. Payment and delivery are still separate events, and a delay can split them. #291 names this as its next step: "so a buyer cannot keep both the goods and the money."
For digital goods, it can be closed completely: make the seller's payment and the buyer's ability to use the goods the same event.
## The change: `hashlock.v1` (payments)
- **Listing.** The seller's registry card lists `content_digest = sha256(data)`.
- **Sealed box.** The seller sends the data sealed with AES-GCM under a fresh key, with `box_digest` and `key_digest`.
- **Pay only with the box in hand.** The buyer checks the box against its digest and the listing. `hold_locked` then binds the escrow to the payee, the key digest and the content digest.
- **Claim.** The seller is paid only through `claim(ref, key)`. The ledger checks, in order:
1. the claimant is the payee,
2. the claim is before the deadline,
3. `sha256(key)` matches the key digest,
4. the box decrypts (the AES-GCM tag rejects tampering),
5. the plaintext matches the listing.
If every check passes, it pays the seller and reveals the key to the buyer in the same step. Otherwise it emits `claim_refused {reason}` and reveals nothing, and the deadline refunds the buyer, who keeps a box it cannot open.
- **No bypass.** Plain `release` cannot bypass the key.
- **Cancelling.** The buyer may still refund before the seller claims, for instance while the claim is in transit. It gets its money back but never the key, so it cannot end up with both.
**`deadline.v1`** is the same ledger without the hashlock. It is included as the negative control, as `plain.v1` is for auth. It is the same mechanism as #291's `leased.v1`. If #291 lands first, `hashlock.v1` should subclass `leased.v1` and `deadline.v1` goes away.
**Other code.**
- `TownAPI` gains `hashlock_hold`, `hashlock_claim`, `revealed_key`, `escrow_state` and `supports_hashlock`. Intents record key digests, never keys.
- Two Lab roles, `data-seller` and `data-buyer`, speak the same protocol under either ledger. Under a ledger without the hashlock, the seller sends the key itself. That keeps the control fair: same agents, only the rules differ.
- Both plugins are drop-ins for every bundled scenario, with identical stage verdicts (tested).
## Fault and invariant: `sealed_delivery`
Four buyers each buy a dataset from a different seller. Staggered starts put one fault on each pair:
| Pair | Fault | Outcome |
|---|---|---|
| buyer-1 / seller-a | the `escrow_locked` notice is delayed 10 s, so the key claim arrives after the 5.0 s deadline | `claim_refused: deadline passed`; refunded; box stays sealed |
| buyer-2 / seller-b | the sealed box is dropped in transit | nothing paid; re-requested; paid and opened |
| buyer-3 / seller-c | the first box is damaged after sealing | `box_rejected: box digest mismatch` before payment; resent; paid and opened |
| buyer-4 / seller-d | the seller seals other bytes under its listing (the box itself is consistent) | `claim_refused: content does not match the listing`; refunded; key withheld |
**Invariant:** each seller is paid **if and only if** its buyer opened the listed data.
The `sealed_delivery` validator judges only `events.jsonl`:
- It re-hashes every revealed key against its locked digest, so `verify` re-check
…Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/298/head:pr-298
git checkout pr-298