# KVStore Topic Manager (`tm_kvstore`)

Admits `@bsv/sdk` `GlobalKVStore` tokens: PushDrop outputs that store one
key/value pair for one wallet protocol, signed by the controller that owns it.

## Token shape

| # | Field        | Encoding |
|---|---|---|
| 0 | `protocolID` | UTF-8 JSON of the wallet protocol, e.g. `[0,"Microblog Posts v1"]` |
| 1 | `key`        | UTF-8 wallet key id, 1..=256 bytes |
| 2 | `value`      | UTF-8 |
| 3 | `controller` | 33-byte compressed identity public key (raw bytes) |
| 4 | `tags`       | OPTIONAL UTF-8 JSON array of strings, at most 32 tags of 128 bytes |
| n | `signature`  | DER ECDSA over the concatenation of fields `0..n-1` |

Two shapes are admitted, because the SDK writes both:

- **five fields** — `protocolID, key, value, controller, signature` (old format)
- **six fields** — the same with `tags` before the signature (new format)

Any other field count is not a KVStore token and is skipped.

## Admission rules

All of these must hold, or the output is not admitted:

1. The locking script decodes as PushDrop.
2. `protocolID` is a JSON `[securityLevel, protocolName]` array: exactly two
   items, level `0`, `1` or `2`, and a printable non-empty name of at most 100
   bytes.
3. `key` is non-empty and at most 256 bytes; `value` is valid UTF-8.
4. `controller` is a valid 33-byte compressed secp256k1 public key.
5. `tags`, when present, is a JSON array of 1..=128-byte strings.
6. The signature verifies over the concatenated data fields under the token's
   own protocol, with `keyID = key` and `counterparty = controller`.
7. The locking key is the controller's own child derivation
   (`forSelf = true`, `counterparty = 'anyone'`) — the identity→locking-key link
   that makes the `controller` column mean "the signer".

Rule 6 and 7 are the reason this topic exists: a record that names a controller
without that controller's signature is worthless to readers, and admitting it
would let anyone publish rows in someone else's name.

## Supported protocol IDs

The topic is generic: one topic serves many applications, distinguished by the
protocol ID inside each token. By default every well-formed protocol ID is
admitted. A deployment can restrict admission with `KVSTORE_PROTOCOLS`, a
comma-separated list of canonical protocol texts, for example:

```
KVSTORE_PROTOCOLS=[0,"Microblog Posts v1"],[0,"Microblog Likes v1"]
```

An output whose protocol is not on the list is skipped (its submit still
succeeds for the other topics in the request).

## Lifecycle

- **Admitted** — indexed by outpoint, with the protocol, key, controller, tags
  and an admission sequence.
- **Spent** — a KVStore update or removal spends the previous token. The row is
  flagged `spent` and stops being served as a current value; `history: true`
  still serves it.
- **Evicted** — hidden from every answer, history included; the row is kept so
  a page cursor naming its outpoint still resolves.

A replay of the same outpoint is a no-op for ordering and spend state: the
admission sequence never moves, and a spent row is not resurrected.
