in reviewhumanPR #305Payments
[Fellowship Challenge] payments: a duplicated award settles once and leaves a verifiable replay record
Submission for the Hexaware x Project NANDA Fellowship Technical Challenge
Author
@NITISH-R-G
github profile →- Status
- In review
- Opened on
- Oct 2
- Branch
- challenge/NITISH-R-G-idempotent-payments
Description
The pitch.
Submission for the Hexaware x Project NANDA Fellowship Technical Challenge **Surface:** Lab · **Layer:** payments · **Plugin:** `ledger.idempotent.v1` (opt-in; the default stays `ledger.v1`) ## Summary This adds a payments-layer plugin that handles a repeated direct transfer by settling it once and recording the repeat as evidence, bound to the original settlement. The mechanism is conventional idempotency; nothing here is a new algorithm. The part specific to Nanda Town is the second half of the requirement: in a testbed whose output is an evidence bundle, handling a duplicate safely is only half of the job. The handling must also leave a verifiable observation of the fault. ## The gap The Lab transport delivers at least once: `FaultRule` `duplicate` hands the same envelope, with the same message id, to the recipient twice, and the engine does not deduplicate deliveries. Every effectful handler therefore decides for itself what a repeat means. The Lab has three behaviours today, and this PR adds a fourth: | Handling of a repeat | Where | Safe? | Evidence? | |---|---|---|---| | Repeat the effect | `ledger.v1.transfer` | no | yes: a bundle in which money is conserved but paid twice | | Refuse by raising | escrow `hold` on a reused ref; negotiation turn and state checks | yes | the exception escapes the agent handler and the run can end before a bundle is written | | Absorb in the agent | most bundled handlers keep their own seen-set (`released`, `voted`, `done`, `parts_done`, `paid`) | yes | varies by agent | | Record a replay in the layer | `ledger.idempotent.v1` (this PR) | yes | yes: one settlement, plus a replay event that points at it | The auction bidder's payment handler has no guard of its own, so the bundled sealed-bid auction shows the first row. A duplicated `auction_won` makes `ledger.v1` record two `payment_settled` events for `auction-print-001`: bidder-y goes from 2000 to 200 cents, and the existing auction `settlement` stage fails with "expected exactly one payment_settled record". `ledger_conserved` still passes (6000 cents): the money moved; it just moved twice. The second row is why raising is not a complete answer for the Lab. In an exploratory scratch sweep, I duplicated each message kind once in each bundled scenario, giving 23 single duplicate-message experiments. 3 of them aborted before producing a bundle: - supply chain `order`: escrow refuses the reused ref `supply-axle`; - marketplace `nego_offer` and `nego_counter`: negotiation refuses an out-of-turn or closed session. This was not committed and is not a repository-wide benchmark. It only shows that refusing a repeat by raising can cost the run its evidence. ## The contribution `ledger.idempotent.v1` is a `Ledger` subclass, 41 lines in `layers/payments.py`, that overrides only `transfer`: - **First successful transfer** for a given payer and non-empty memo: settles exactly as in `ledger.v1`. - **Same payer and memo again, to the same payee for the same amount:** `payment_replay_ignored`. No money moves and no exception is raised. - **Same payer and memo, with another payee or amount:** `payment_reuse_rejected`. No money moves and no exception is raised. - **Both events** carry `settlement`, the event id of the `payment_settled` they repeat. Settlement → replay is an explicit, recorded causal pointer. - **Failures:** - Invalid amounts raise before any lookup, as in `ledger.v1`. - A transfer that fails (insufficient funds) records no identity, so a later attempt is a first attempt. - **Unchanged:** an empty memo, and every escrow operation, behave exactly as in `ledger.v1`. - **State:** the identity map belongs to the plugin instance, so it lasts one run. The code change is small because the missing behaviour is small. The testing is large because the invariant is easy to get subtly wrong. Examples from this work: - A mutant that dropped the check on the award's send record initially survived the whole module, and closing it took a …
Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/305/head:pr-305
git checkout pr-305