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 →
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

Author

Status
Merged
Merged on
Oct 12
Branch
challenge/alzz-26-registry-owned

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 diff

Checkout locally

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