in reviewhumanPR #296Coordination
[Fellowship Challenge] Coordination: one-shot ContractNet finalization under duplicate delivery
Submission for the Hexaware x Project NANDA Fellowship Technical Challenge
Author
@devdiv07
github profile →- Status
- In review
- Opened on
- Oct 1
- Branch
- challenge/devdiv07-contractnet-once
Description
The pitch.
Submission for the Hexaware x Project NANDA Fellowship Technical Challenge **Layer:** Coordination · **Track:** Lab · **Fault:** Town-native `duplicate` on the message that triggers a contract-net task ### Demo video https://youtu.be/Qe4oWpow3j0 ## Summary `contractnet.once.v1` is a new, swappable Coordination plugin. One `task_id` names one contract-net task lifecycle, and that task is finalized at most once. Stock `contractnet.v1` is unchanged. A new bundled Lab scenario delivers the auction's opening `consign` message twice through Town's own `duplicate` fault: - With `contractnet.once.v1`, the task is announced once, awarded once and paid once, and the run passes. - The bundled control differs only in `coordination: contractnet.v1`. It announces the task twice, awards it twice and charges the winner twice, so it fails on purpose. - Both bundles verify. ## Problem discovered An exploratory differential fault sweep compared stock `contractnet.v1` with a one-shot prototype. It ran each bundled scenario under drop, duplicate and delay faults on each message kind. Coordination changed the outcome in exactly one case: `supply_chain` with its `order` message duplicated. Under stock `contractnet.v1`, re-measured on this branch (E021, E032): 1. The manufacturer handles `order` twice, at t=0.6 and t=0.7. It announces `supply-axle` and `supply-wheel` again each time, and stock `announce()` replaces the task record. 2. Each handled `order` schedules its own close, so `award()` runs twice for `supply-axle` (t=1.6 and t=1.7). Stock `award()` returns a winner both times, so the second award is actionable. 3. The manufacturer acts on the second award: a second `escrow_hold(ref="supply-axle")` reuses the escrow reference. 4. `PaymentError: escrow ref supply-axle reused` ends the Lab run after 65 events, before settlement and without a verdict. No payment settles in this run. The observed failure is a crash caused by repeated finalization, not a double payment. ## Contribution `src/nandatown/layers/coordination_once.py` (51 lines) registers `contractnet.once.v1` as a subclass of `ContractNet`: - **First announcement.** The first `announce()` for a `task_id` creates the task exactly as `contractnet.v1` does. The task keeps its own copy of the spec. - **Identical re-announcement.** The same issuer, spec and rule is a replay. Task state, bids and winner are preserved, and the plugin records `announce_replayed` instead of a second `task_announced`. - **Conflicting re-announcement.** Different terms for an existing `task_id` are rejected without mutation. `announce_rejected` records the conflicting terms and the reason. - **Return contract.** `announce()` keeps the stock `None` return in all three cases. - **Repeated award.** Once a task is closed, `award()` records `award_rejected` and returns `None`, so a repeated close cannot produce another actionable award. - **Stock plugin.** `contractnet.v1` and the default plugin set are unchanged; `coordination.py` has no diff. ### Why a repeated `award()` returns `None` Both in-tree callers act on any non-`None` result: - `Auctioneer.close` sends `auction_won`, and the winning bidder pays on every `auction_won`. - `Manufacturer.award_task` holds escrow under `ref=task_id`. Returning the stored award again, as legacy #5's `resolve()` does, would trigger those effects again: a second payment in the auction and the reused escrow reference in the supply chain. This conclusion comes from reading the callers' code; a variant that returns the stored award was not run. ### Task identity Task identity uses Town's existing `canonical_json`. So `True`, `1` and `1.0` are different terms, which a unit test checks. The claim covers JSON-compatible task specs only: `canonical_json` treats a tuple like a list and an integer key like its string form, and it raises on a set. ## Scenario and fault `auction_duplicate_consign` is the bundled `auction`, with the same seed (42), bidders, valuations, de …
Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/296/head:pr-296
git checkout pr-296