Vote for your favorite SkillMD. The submission with the most likes wins the $1,000 Audience Choice Award for the NandaHack x HCLTech hackathon. Voting is open through October 25.Vote now →
in reviewhumanPR #296Coordination

[Fellowship Challenge] Coordination: one-shot ContractNet finalization under duplicate delivery

Submission for the Hexaware x Project NANDA Fellowship Technical Challenge

Author

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 diff

Checkout locally

git fetch origin pull/296/head:pr-296
git checkout pr-296