mergedhumanPR #303Registry
[Fellowship Challenge] registry: index.owned.v1, forbidding a forged card from erasing an honest listing
Submission for the Hexaware x Project NANDA Fellowship Technical Challenge
Description
The pitch.
Submission for the Hexaware x Project NANDA Fellowship Technical Challenge ## The gap In the default registry, `index.v1`, a listing has no owner. `publish()` stores whatever card arrives under `card["name"]`, even when the signature fails verification. So any agent can erase a competitor's verified listing by publishing a card in the competitor's name signed with its own key. The town correctly records the forged card as `card_unverified`, then keeps it and drops the real one, and `lookup()` hides unverified cards. The honest agent disappears from discovery. There is also no legitimate way for an agent to withdraw its own listing. Reproduced on `main` (`36e7f6c`): in `registry_eviction_unowned`, the honest seller registers (`ev-7`), the rival's forged card replaces it (`ev-15`), the buyer only asks the rival for a quote (`ev-16`), and the rival is paid 3780 cents (`ev-33`). The rival didn't just vandalize the listing, it took the sale. ## Layer `registry`. New plugin `index.owned.v1` in `src/nandatown/layers/registry_layer.py`. It subclasses `TownIndex`; `index.v1` itself is unchanged. | Situation | index.v1 | index.owned.v1 | |---|---|---| | Card for a name that already has a verified listing, signed by that name's key, with a higher `version` | overwrites | update accepted (`card_registered`, `update: true`) | | Old signed card replayed after an update or a withdrawal | restores the old card | refused (`card_publish_refused`, version not above the highest accepted) | | `version` present but not an integer of at least 1 | stored | refused (`card_publish_refused`) | | Same, signature fails | overwrites, erasing the verified listing | refused, listing untouched (`card_publish_refused`) | | Unverified card for an unclaimed name | stored unverified | stored unverified, does not claim the name | | Withdrawal signed by the listing's key | not possible | listing removed (`card_withdrawn`) | | Forged or replayed withdrawal | not possible | refused (`card_withdraw_refused`) | | Caller edits a card dict after `publish()` or a `lookup()` result | edits the stored listing | no effect: cards are deep-copied in and out | **Card lifetime: one use.** A card carries a `version`, an integer of at least 1; a card without one is version 1. The registry remembers the highest version it has accepted for each name, also after a withdrawal, and refuses any card at or below it. So replaying an older signed card cannot roll an update back or bring a withdrawn listing back; the owner updates or returns by publishing a higher version. A forged card is still refused for its signature first. Agents reach this through the normal API: `TownAPI.register(..., version=N)` puts the version on the card before signing (and records it in the intent); without `version` the card and intent are exactly as before, so existing scenarios are unchanged. **Withdrawal lifetime: one use.** Each name has a generation that moves on every change to its verified listing (registered, updated, withdrawn). The withdrawal payload binds the current card's fingerprint and that generation, so a signed withdrawal removes that listing once. It cannot be replayed later, even after the identical card is republished or after A, then B, then A. ## Fault and invariant - **Fault:** a forged card. A new `rival_forger` role (a `Seller` subclass) registers its own listing, then at t=0.1 republishes the honest seller's card under the honest seller's name, signed with its own key. - **Invariant:** a verified listing stays discoverable until its own key withdraws it. In the scenario: the buyer still reaches the honest seller after the forgery, and the honest seller is paid. New validator `registry_eviction`, judged only from events: - `forgery_detected`: the forged publish is recorded as `card_unverified` or `card_publish_refused` - `listing_intact`: a `quote_request` sent to the victim after the forgery was delivered (`message_delivered` for that me …
Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/303/head:pr-303
git checkout pr-303