Repository navigation
[Fellowship Challenge] payments: hashlock.v1, sealed digital goods and payment change hands in one step - #298
Conversation
|
Hi @saisreekantam, Built this fresh against I also disabled each ledger check and the validator's key rehash one at a time. Every one tripped a specific test. Swapping Watched the demo as well. What's on screen lines up with what's in the bundle, and you were straight about what the run does and doesn't prove. That matters more to us than polish. One blocker: the branch now conflicts with Minor, non-blocking:
One question: the ledger decrypts the box to check the digest, so the town sees the plaintext. Is there a step short of ZK that avoids that while keeping the binding re-checkable offline from |
A deadline refund keeps escrow from being stranded (open PR projnanda#291), but goods that arrive after the refund can still be used: the buyer keeps the goods and the money. hashlock.v1 makes payment and delivery one step. The seller lists a digest of the data and sends it sealed with AES-GCM under a fresh key. The buyer pays only while holding a box that matches its digest and the listing, and locks the escrow to the payee, the key digest and the content digest. The seller is paid only by handing the ledger the key. The ledger checks the payee, the deadline, the key digest, that the box decrypts and that the plaintext matches the listing, and reveals the key to the buyer in the same step. Otherwise the deadline refunds the buyer, who keeps a box it cannot open. Plain release cannot bypass the key. deadline.v1 is the same ledger without the hashlock: the time half, and the negative control, as plain.v1 is for auth. sealed_delivery injects a lost box, a damaged box, a seller sealing the wrong content and a key claim delayed past the deadline. Its validator checks that nothing is paid before a verified box, damaged boxes and bad claims earn nothing, and each seller is paid if and only if its buyer opened the goods, re-hashing every revealed key from the record. It counts ledger facts only when the town observed them. A fault stage is tested only when the scenario declares that fault. sealed_delivery_no_hashlock runs the same agents on deadline.v1 and fails atomic_exchange: a late key opens goods whose payment was refunded. TownAPI gains hashlock_hold, hashlock_claim, revealed_key, escrow_state and supports_hashlock; intents record key digests, never keys. Both plugins are drop-ins for every bundled scenario with identical stage verdicts, and existing scenario traces are identical to main.
The fellowship brief's optional extra: bring your own agents into a scenario you design. Two llm-data-buyer agents ask a model, by tool call, which of three sellers to buy a dataset from (seeing price and town reputation) and what to do after an order fails. The cheapest seller seals the wrong data and one key claim arrives late. The scenario loads the buyers with plugin_files and uses the bundled sealed_delivery validator, so verify replays it. Code checks every model call (a tool offered at that point, a listed seller, a price within the cap) and records each decision as a model_decision event. The sealed-box protocol itself is never left to the model. llm_market runs on hashlock.v1; llm_market_no_hashlock is the same file on deadline.v1 and fails atomic_exchange. Tests use the scripted:v1 brain, so CI needs no model. MANIFEST ships the example's YAML and README in the sdist.
Five runs of each scenario on a local Ollama: llm_market passed 5/5, llm_market_no_hashlock failed 5/5 on atomic_exchange, all ten bundles verify, and the guardrail rejected no calls. The model bought from a seller with negative reputation because it was cheapest; the hashlock refunded it.
The buyer may cancel a hashlocked escrow before the seller claims, for instance while the claim is in transit. It gets its money back but the key is never revealed, so the box it holds stays sealed; the seller's later claim is refused as "escrow refunded" and the deadline refunds nothing a second time. Neither side can end up with both.
603ba1e to
3f10183
Compare
…e event - escrow_state now answers only the escrow's parties: its payer, or the payee a hashlocked escrow names. Other agents get None, as with revealed_key. Every caller was already a buyer reading its own order. - _escrow_index no longer builds a deadlines map; nothing read it once the escrow_deadline validator was removed. _is_time goes with it. - New test: relabelling each escrow_deadline_set as projnanda#291's escrow_leased {from, cents, expires_at} leaves every stage verdict of both runs unchanged, and the same three stages still separate them. The validator never reads the deadline event, so building hashlock.v1 on leased.v1 would not change the judgment.
|
Thanks for reproducing all of this and for the careful review. Rebase. I've rebased onto The full suite passes: 1,915 tests on Python 3.11 and 3.12. Non-blocking items (b60d4e5):
Plaintext and confidentiality. One limitation I should own: the exposure extends beyond the ledger. The sealed box is recorded in message bodies, and A possible follow-up is optimistic verification in the style of FairSwap (Dziembowski, Eckey, and Faust, CCS 2018):
This would reduce routine plaintext exposure, but it would not by itself guarantee end-to-end confidentiality. Key visibility, ciphertext access, and the dispute protocol must be considered together. Offline verification can still check the key binding from the recorded key and digest; checking the ciphertext contents requires the ciphertext or appropriate dispute evidence. There is also an important distinction between integrity and correctness. A seller's plaintext root, committed before the buyer accepts, can establish consistency with the advertised data. It cannot establish that the data is what the buyer actually wanted. That stronger guarantee requires an independent reference, such as a buyer-approved digest, a publisher-signed I'd keep
Tests would cover honest delivery, corrupted ciphertext, false complaints, malformed Merkle proofs, and a seller who commits to incorrect data from the outset. In the last case, the protocol must distinguish consistency with the commitment from correctness of the underlying data. Thanks again for the thorough review. |
Awesome! Rebase: looks good to me, and the Follow-ups: All three done, no notes. The
Plaintext: Confirmed. From the original bundle alone, Before merge:
After that and the example split, I'm done here. Thanks for the quick turnaround. :) |
…et out - README and the HashlockLedger docstring now say hashlock.v1 verifies integrity, not confidentiality: the evidence bundle records the sealed box in the sealed_box message body (box_hex) and the key in key_revealed (key_hex), so anyone holding events.jsonl can decrypt every paid order. Keeping the ciphertext out of exported records is a separate redesign. - examples/llm_market, its test and its MANIFEST line move to their own pull request, as requested in review. MANIFEST.in matches main again.
|
Done:
The suite passes on 3.11 and 3.12 (1906 here, 1915 on #313). Thanks again! |
|
Hi @Redeem-Grimm-Satoshi, hope you're well. I wanted to check on next steps for this PR when you have a moment. The items from your last review are in: the confidentiality limit is documented in the README and the docstring, the example has moved to #313, and the redesign is tracked in #312. The branch is still mergeable against I'd also be grateful for any update you can share on the next steps of the Hexaware x Project NANDA Fellowship process, or a pointer to the right channel if this isn't the place to ask. If anything else is needed from my side, I'm happy to take care of it. Thank you for your time and the thoughtful review. |
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, anddeadline.v1as its time half and negative control.ledger.v1and every existing scenario are unchanged; their event traces are identical tomain.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_digestandkey_digest.Pay only with the box in hand. The buyer checks the box against its digest and the listing.
hold_lockedthen 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:sha256(key)matches the key digest,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
releasecannot 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.v1is the same ledger without the hashlock. It is included as the negative control, asplain.v1is for auth. It is the same mechanism as #291'sleased.v1. If #291 lands first,hashlock.v1should subclassleased.v1anddeadline.v1goes away.Other code.
TownAPIgainshashlock_hold,hashlock_claim,revealed_key,escrow_stateandsupports_hashlock. Intents record key digests, never keys.data-selleranddata-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.Fault and invariant:
sealed_deliveryFour buyers each buy a dataset from a different seller. Staggered starts put one fault on each pair:
escrow_lockednotice is delayed 10 s, so the key claim arrives after the 5.0 s deadlineclaim_refused: deadline passed; refunded; box stays sealedbox_rejected: box digest mismatchbefore payment; resent; paid and openedclaim_refused: content does not match the listing; refunded; key withheldInvariant: each seller is paid if and only if its buyer opened the listed data.
The
sealed_deliveryvalidator judges onlyevents.jsonl:verifyre-checks the cryptographic binding offline.api.observelets an agent record any event kind.goods_openedonly from the paying buyer.privacyis only tested withredact_fields. If the fault is declared but leaves no evidence, the stage is inconclusive, never passed.Negative control:
sealed_delivery_no_hashlockThis is the same file with
payments: deadline.v1. A test pins that nothing else differs. Predicted before running, and observed:sealed_delivery(hashlock.v1)order-buyer-1-1 goods opened without paymentIn the control, buyer-1 is refunded at 5.7, the late key reaches it at 10.9, and it opens data it no longer pays for.
nandatown compare sealed_delivery --swap payments=deadline.v1reports exactly the three differing stages.Bundle fingerprints
Both pass
nandatown verify("hashes match and the evaluator reproduces the recorded result"):sealed_delivery:sha256:8aeb9ab17b0051e0b14742bce337082c83a6e08ccef6156a4162d4595e0927c1sealed_delivery_no_hashlock:sha256:1e9aa8ca931bf2c38ee7bbfada93901567fdcacbc0b2d4cb92b6fcce52e670a4The Lab run id is random, so a rerun gives a new fingerprint with the same stage results.
Optional extra, moved to #313
At review, the model-driven buyers (
examples/llm_market/, with its test and MANIFEST line) moved into their own pull request, #313, which is stacked on this one. They are unchanged.Follow-up
The confidentiality redesign (optimistic, FairSwap-style verification) is tracked in #312, with its three invariants and its test cases. It stays out of this PR.
Tests
tests/test_sealed_delivery.py(40) fails onmainand passes here. It covers:payments;goods_openedasserted by the wrong party;escrow_statevisible only to the escrow's parties;escrow_leased, so building onleased.v1would not change the judgment.Full suite, rebased on
mainafter #290, #292, #300 and #303: 1906 passed on Python 3.11 and 3.12.hashlock.v1is a drop-in for all eight bundled scenarios, with unchanged verdicts.python -m buildandscripts/check_distributions.pypass, and the installed wheel runs both scenarios outside the checkout.Evaluator version
No existing validator rule changes, and existing scenarios produce identical traces, so the Lab evaluator stays
lab-0.2.6. Bumping it would make every existinglab-0.2.6bundle fail replay for no change in meaning. An older build withoutsealed_deliverywould report "no validator registered" for these bundles, which is a mismatch, not a false pass.What this does not prove
sealed_boxmessage body (box_hex) and the key inkey_revealed(key_hex), so anyone holdingevents.jsonlcan decrypt every paid order. Keeping the ciphertext out of exported records is the redesign tracked in hashlock.v1: keep sealed goods confidential in evidence bundles (optimistic, FairSwap-style verification) #312.goods_openedis the buyer's attributed assertion. In the Lab the buyer is reference code; an external buyer's report would be a claim, not independent observation.