# Layered Key-Value Store for Wallets and Overlay Services

Ty Everett (<ty@projectbabbage.com>)

## Abstract

This document defines a layered key-value store standard for BSV systems that represent state as spendable PushDrop token outputs.

It standardizes a shared lifecycle model and two interoperable profiles:

1. **Global KVStore**: publicly discoverable, overlay-tracked, controller-bound KV tokens.
2. **Local KVStore**: wallet-local, basket-backed KV persistence built entirely from existing BRC-100 wallet methods.

BRC-35 is based on the currently interoperable behavior of the `bsv-blockchain/ts-sdk` `GlobalKVStore`, `LocalKVStore`, `PushDrop`, `kvStoreInterpreter`, `Historian`, `LookupResolver`, and topic-broadcasting implementations, together with their tests and surrounding wallet architecture. It uses BRC-48 PushDrop tokens, BRC-64-style retained ancestry for history, BRC-87/BRC-88 naming and overlay-service conventions, and existing BRC-100 wallet methods. It does not define any new wallet RPC methods.

## Motivation

Applications increasingly need both:

* a **publicly discoverable** KV surface for shared or controller-addressable state, and
* a **wallet-local** KV surface for private or application-scoped persistence.

The existing `ts-sdk` implementations already provide both capabilities, but their behavior is currently defined by code and tests rather than by a single stable protocol document. Without a BRC:

* wallet authors cannot easily determine which behaviors are protocol-level and which are SDK convenience,
* overlay-service authors cannot reliably implement interoperable lookup behavior,
* application developers are left to infer lifecycle and history semantics from library wrappers, and
* PushDrop usage remains too generic to clearly describe what a KV token actually is.

BRC-35 addresses this by specifying the shared KV model once, then defining the global and local profiles separately but consistently.

## Background

Readers of this document should be familiar with:

* [BRC-46](/wallet/0046.md) for wallet output baskets
* [BRC-48](/scripts/0048.md) for PushDrop
* [BRC-64](/overlays/0064.md) for overlay transaction history tracking
* [BRC-87](/overlays/0087.md) for topic-manager and lookup-service naming conventions
* [BRC-88](/overlays/0088.md) for overlay service synchronization architecture
* [BRC-99](/wallet/0099.md) for future basket permission schemes
* [BRC-100](/wallet/0100.md) for wallet methods and object shapes
* [BRC-116](/wallet/0116.md) for wallet trust and permission context

The older [BRC-22](/overlays/0022.md) and [BRC-24](/overlays/0024.md) documents provide historical background for overlay submission and lookup. This document uses the newer **service** and **topic** terminology established by BRC-87 and BRC-88.

## Normative Language

The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this document are to be interpreted as described in RFC 2119 and RFC 8174 when, and only when, they appear in all capitals.

## Specification

### 1. Terminology

* **KV token**: A spendable PushDrop output whose semantic fields represent one KV entry.
* **Live token**: A KV token that is still unspent and therefore part of the current KV state.
* **Controller**: The compressed public identity key of the entity that controls a global KV entry. In the global profile, this is a semantic token field and not merely the PushDrop locking public key.
* **Context**: The local KV namespace. In the local profile, the context is the wallet basket name.
* **Tuple**: The identity of a KV entry. In the global profile the tuple is `(protocolID, key, controller)`. In the local profile the tuple is `(context, key)` within one wallet.
* **History chain**: The ancestry of prior KV tokens spent into the current live token.

### 2. Shared Model and Invariants

#### 2.1 State Model

A BRC-35 KV store is not a mutable document database. It is a set of spendable tokens whose current state is determined by **currently unspent outputs**.

For a single KV tuple:

* **Create** means minting a live token when no live token currently exists for that tuple.
* **Update** means spending the prior live token and creating a successor token for the same tuple.
* **Delete** means spending the prior live token without replacement.

The satoshi amount carried by a KV token is not semantic KV data. Implementations **SHOULD** use `1` satoshi unless another amount is needed for fee policy or wallet policy.

#### 2.2 Encoding Rules

Unless a profile says otherwise:

* `protocolID` is encoded as the UTF-8 JSON serialization of the wallet protocol tuple, such as `"[1,\"kvstore\"]"`.
* `key` is encoded as UTF-8 bytes.
* `value` is represented as UTF-8 bytes of an application string.
* `controller` is encoded as the raw bytes of the controller's compressed public key hex string.
* `tags`, when present, are encoded as the UTF-8 JSON serialization of an array of strings.

Applications that need binary values **SHOULD** encode them into a string representation at the application layer before using the interoperable profiles defined here.

#### 2.3 Operational Invariants

Compliant implementations:

* **MUST** treat state as live-output state, not as the last record observed in a scan.
* **MUST** preserve tuple identity across updates.
* **SHOULD** serialize writes per tuple to avoid concurrent mutation races.
* **MUST NOT** treat malformed or failed-verification outputs as valid state.

### 3. Global KVStore Profile

#### 3.1 Overview

The global profile defines a publicly discoverable KV store in which each live KV entry is represented by a PushDrop token tracked by overlay services.

Canonical public names are:

* **Lookup service**: `ls_kvstore`
* **Topic manager topic**: `tm_kvstore`

Implementations that claim public BRC-35 interoperability **MUST** understand these names. SDK options that allow overriding them are deployment conveniences and are not the canonical public profile.

#### 3.2 Canonical Token Format

Global KV tokens **MUST** use a BRC-48 PushDrop locking script with lock position `before`.

The canonical semantic field order is:

| Field | Meaning      | Encoding                                       |
| ----- | ------------ | ---------------------------------------------- |
| 1     | `protocolID` | UTF-8 JSON serialization of the protocol tuple |
| 2     | `key`        | UTF-8                                          |
| 3     | `value`      | UTF-8                                          |
| 4     | `controller` | bytes of compressed public key hex             |
| 5     | `tags`       | UTF-8 JSON serialization of string array       |
| 6     | `signature`  | PushDrop signature                             |

For backwards compatibility, the legacy no-tags form is also valid:

| Field | Meaning      |
| ----- | ------------ |
| 1     | `protocolID` |
| 2     | `key`        |
| 3     | `value`      |
| 4     | `controller` |
| 5     | `signature`  |

Emitters **MAY** omit the `tags` field only when emitting the legacy format for compatibility. New interoperable emitters **SHOULD** emit the tagged form and **SHOULD** encode an empty tag set as absence of field 5 rather than as an empty JSON array.

The `controller` field **MUST** contain the emitter's compressed identity public key. Implementations **MUST NOT** treat the PushDrop locking public key as the controller identity.

The PushDrop lock for a global token **MUST** be derived under the same `protocolID` and `key` scope represented in fields 1 and 2. Current interoperable implementations do this using the public-counterparty mode associated with globally verifiable KV tokens.

#### 3.3 Signature Rules

The global signature field **MUST** be computed over the byte-concatenation of every preceding semantic field in emitted order:

* legacy form: `protocolID || key || value || controller`
* tagged form: `protocolID || key || value || controller || tags`

For interoperability with current wallet behavior, the signature is created under the same `protocolID` and `key` scope as the token, using the public-signing behavior currently reached by omitting an explicit signature counterparty in BRC-100 signature creation.

Readers of global KV tokens **MUST** verify the appended signature before accepting the token as valid state.

Verification **MUST** use:

* `protocolID` from field 1
* `key` from field 2 as the `keyID`
* `controller` from field 4 as the counterparty identity used for verification
* the exact concatenated payload defined above

Any output that fails signature verification **MUST** be ignored.

#### 3.4 Lifecycle Semantics

For a given `(protocolID, key, controller)` tuple:

* create **MUST** mint one new live token,
* update **MUST** spend the prior live token and create a successor token for the same tuple,
* delete **MUST** spend the prior live token without replacement.

Public overlay services **SHOULD** expose at most one live token per tuple. If multiple live tokens are observed for the same tuple, clients **SHOULD** treat the state as ambiguous or corrupted rather than silently merging values.

#### 3.5 Lookup Query Schema

The canonical lookup query object is:

| Field          | Type               | Meaning                                |
| -------------- | ------------------ | -------------------------------------- |
| `key`          | string             | Match a specific key                   |
| `controller`   | string             | Match a specific controller public key |
| `protocolID`   | `[number, string]` | Match a specific wallet protocol tuple |
| `tags`         | `string[]`         | Match tags                             |
| `tagQueryMode` | `'all' \| 'any'`   | Tag matching mode                      |
| `limit`        | integer            | Page size                              |
| `skip`         | integer            | Result offset                          |
| `sortOrder`    | `'asc' \| 'desc'`  | Ordering hint                          |

Queries **MUST** include at least one selector from `key`, `controller`, `protocolID`, or `tags`.

When `tags` is present:

* `tagQueryMode: "all"` means every supplied tag **MUST** be present on the result token.
* `tagQueryMode: "any"` means at least one supplied tag **MUST** be present on the result token.
* if `tagQueryMode` is omitted, services **MUST** treat it as `"all"`.

This BRC standardizes the query object and matching semantics. It does not redefine the generic BRC-24/BRC-88 transport envelope, `output-list` response encoding, or BEEF packaging used by the overlay stack.

#### 3.6 History

History is a core part of the global profile.

A BRC-35-compliant global implementation that exposes current KV state **MUST** preserve or reconstruct enough transaction ancestry to support BRC-64-style chronological history reconstruction for a live token's tuple.

History:

* **MUST** be reconstructable from retained ancestry rather than from a mutable service-side document record,
* **MUST** represent prior values in chronological order from oldest to newest, and
* **MUST** only include values from valid KV tokens for the same `(protocolID, key, controller)` tuple.

### 4. Local KVStore Profile

#### 4.1 Overview

The local profile defines wallet-local KV persistence without introducing any new wallet RPC methods.

It is a profile over existing BRC-100 capabilities. The wallet basket is the KV **context**, and the wallet output tag is the KV **key** index.

#### 4.2 Required Wallet Method Mapping

Local KV implementations **MUST** be expressible using the following existing wallet methods:

| Capability                           | Existing wallet method(s)    |
| ------------------------------------ | ---------------------------- |
| create or update local KV tokens     | `createAction`, `signAction` |
| enumerate current local KV tokens    | `listOutputs`                |
| encrypt values                       | `encrypt`                    |
| decrypt values                       | `decrypt`                    |
| manual recovery of corrupted outputs | `relinquishOutput`           |

This BRC does not define any new wallet interface surface beyond those methods.

#### 4.3 Namespace and Indexing

For the local profile:

* the wallet basket **MUST** be the KV context,
* the wallet output tag **MUST** be the KV key,
* reads **MUST** query by basket plus tag,
* duplicate live outputs for the same `(context, key)` tuple **MUST** be collapsible by `set`.

This BRC relies on existing wallet basket and tag semantics from BRC-46, BRC-99, BRC-100, and related wallet specifications. It does not redefine wallet normalization rules for basket or tag identifiers. Applications **SHOULD** therefore use stable canonical strings for contexts and keys.

#### 4.4 Canonical Token Format

Local KV tokens **MUST** use a BRC-48 PushDrop locking script with lock position `before`.

The PushDrop lock for a local token **MUST** be derived under:

* `protocolID = [2, <context>]`
* `keyID = <key>`
* `counterparty = "self"`

The local semantic payload is intentionally minimal:

| Field | Meaning                     |
| ----- | --------------------------- |
| 1     | value bytes                 |
| 2     | optional PushDrop signature |

Local parsers **MUST** accept both:

* one-field payloads, and
* two-field payloads in which only field 1 is semantic and field 2 is ignored for state purposes.

Payloads with zero fields or more than two fields **MUST** be treated as invalid local KV tokens.

#### 4.5 Encryption and Plaintext Profiles

The default local profile is **encrypted**.

When encryption is enabled:

* the wallet **MUST** encrypt the UTF-8 bytes of the value using `protocolID = [2, <context>]`,
* the wallet **MUST** use `keyID = <key>`,
* the resulting ciphertext **MUST** be stored as field 1 of the PushDrop payload.

When plaintext storage is intentionally selected:

* the UTF-8 bytes of the value **MUST** be stored directly as field 1.

Plaintext local storage is allowed for compatibility and explicit application choice, but encrypted local storage is the canonical default profile.

#### 4.6 Local Lifecycle Semantics

For a given `(context, key)` tuple within one wallet:

* `get` **MUST** return the newest live value represented by the current matching outputs,
* `set` **MUST** spend all currently matching live outputs and create one successor output,
* `remove` **MUST** continue spending matching live outputs until none remain.

If more than one live output exists for the same local tuple, that is duplicate live state. A compliant `set` implementation **MUST** collapse such duplicates into one successor token.

#### 4.7 Corrupted State Handling

If a local token cannot be decoded as a valid one-field or two-field local KV token, or if the encrypted value cannot be decrypted, the implementation **MUST NOT** return it as valid state.

A compliant implementation **SHOULD** support one or both of these recovery paths:

* overwrite the tuple with a new `set` operation that collapses the bad state, or
* allow manual administrative recovery using `relinquishOutput`.

Automatic relinquish-on-failure is not required by this BRC.

### 5. KV-Specific PushDrop Clarification

BRC-48 defines the generic PushDrop construction. BRC-35 defines the KV-specific profile needed for interoperable KVStore systems.

For BRC-35:

* emitters **MUST** use PushDrop lock position `before`,
* the signature field, when present, **MUST** be the final field,
* signature payload bytes **MUST** be formed by concatenating all preceding fields exactly as emitted,
* global readers **MUST** verify the signature before accepting state,
* local readers **MAY** ignore the signature field because trust is anchored in wallet-managed local storage rather than in public overlay validation.

The PushDrop locking public key and the semantic controller field serve different roles:

* the locking public key governs spendability of the output,
* the controller field identifies who the global KV entry belongs to and anchors public verification semantics.

### 6. Compatibility and Implementation Guidance

#### 6.1 Normative Versus SDK-Only Surface

The following are **normative** under BRC-35:

* the KV token lifecycle model,
* the global and local token formats,
* the global query schema,
* controller and signature verification semantics,
* the canonical public names `ls_kvstore` and `tm_kvstore`,
* BRC-64-style history expectations,
* local use of baskets, tags, and existing BRC-100 wallet methods.

The following are **not normative protocol surface** and are SDK or deployment conveniences:

* `overlayBroadcast`
* `acceptDelayedBroadcast`
* `originator`
* human-readable description strings supplied to wallet actions
* `overlayHost`
* tracker lists, host overrides, additional hosts, network presets, or host-reputation heuristics
* TypeScript wrapper return ergonomics such as returning a single object for `key + controller` queries and arrays for broader queries
* wrapper token objects included for local library convenience

#### 6.2 Known Divergences

| Current artifact                                                   | Status under BRC-35                   | Note                                                                                                         |
| ------------------------------------------------------------------ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `LocalKVStore` comments describing automatic relinquish-on-failure | Non-normative stale commentary        | Current interoperable behavior does not automatically call `relinquishOutput` during normal failure handling |
| `overlayHost` config surface in current SDK types                  | Non-normative                         | Present as a configuration artifact, but not part of the current interoperable protocol behavior             |
| Legacy global no-tags tokens                                       | Normatively valid compatibility input | Parsers **MUST** continue to accept them                                                                     |

#### 6.3 Multi-Protocol Guidance

Current global deployments commonly use a single default KV protocol such as `[1, "kvstore"]`. Implementations that support multiple global KV protocols **SHOULD** manage live state by the full `(protocolID, key, controller)` tuple and **SHOULD NOT** assume that `key + controller` alone is globally unique across protocols.

### 7. Examples

#### 7.1 Global Token Example

Example semantic token contents:

| Field        | Example value           |
| ------------ | ----------------------- |
| `protocolID` | `[1, "kvstore"]`        |
| `key`        | `profile.displayName`   |
| `value`      | `Ty`                    |
| `controller` | `02ab...`               |
| `tags`       | `["profile", "public"]` |

Example exact-match lookup query:

```json
{
  "key": "profile.displayName",
  "controller": "02ab...",
  "protocolID": [1, "kvstore"]
}
```

Example tag query:

```json
{
  "controller": "02ab...",
  "tags": ["profile", "public"],
  "tagQueryMode": "all",
  "limit": 50,
  "skip": 0,
  "sortOrder": "desc"
}
```

#### 7.2 Local Token Example

For a local tuple:

* `context = "notes"`
* `key = "welcome"`
* encryption protocol = `[2, "notes"]`
* encryption `keyID = "welcome"`
* basket = `notes`
* output tag = `welcome`

In the encrypted profile, field 1 contains ciphertext derived from the UTF-8 bytes of the value. In the plaintext profile, field 1 contains the raw UTF-8 value bytes.

## Security and Privacy Considerations

* Global KV values are public. Their keys, values, controllers, and tags are visible to overlay operators and anyone who can inspect the chain or the overlay state.
* Tags can leak classification metadata even when values themselves are not sensitive.
* Global readers **MUST** verify signatures on read. Overlay services are indexing infrastructure, not trust anchors for token authenticity.
* Global history reconstruction reveals prior values. Applications **SHOULD** assume that a historical value chain is publicly linkable.
* Local encrypted storage is the canonical default because it protects the value payload from casual exposure outside the wallet's authorized decryption path.
* Even in the encrypted local profile, context names and key tags may remain visible to the wallet and to any local administrative interface. Applications **SHOULD** avoid sensitive naming if that metadata matters.
* Implementations **SHOULD** serialize writes per tuple to reduce races and duplicate live state.

## Conformance Appendix

### A. Parser Requirements

Global parsers:

* **MUST** accept the legacy no-tags and current tagged token forms.
* **MUST** reject or ignore malformed tokens.
* **MUST** verify the global signature before accepting state.
* **MUST** decode `protocolID` and `tags` from their JSON string forms.

Local parsers:

* **MUST** accept one-field and two-field payloads.
* **MUST** treat only field 1 as semantic state.
* **MUST** reject zero-field and greater-than-two-field payloads.
* **MUST NOT** return undecodable or undecryptable values as valid state.

### B. Emitter Requirements

Global emitters:

* **MUST** emit PushDrop with lock position `before`.
* **MUST** order fields exactly as defined in Section 3.2.
* **MUST** include the controller identity key in the `controller` field.
* **MUST** sign the exact concatenated semantic field bytes.
* **SHOULD** use the canonical public names `ls_kvstore` and `tm_kvstore`.

Local emitters:

* **MUST** store local KV outputs in the basket named by the context.
* **MUST** tag the output with the key.
* **MUST** use `[2, <context>]` and `keyID = <key>` for encrypted local storage.
* **MUST** collapse duplicate live outputs during `set`.

### C. Overlay Service Requirements

Overlay services implementing the public profile:

* **MUST** recognize `ls_kvstore` as the canonical lookup-service name.
* **MUST** recognize `tm_kvstore` as the canonical topic-manager topic.
* **MUST** filter by the query schema defined in Section 3.5.
* **MUST** treat omitted `tagQueryMode` as `"all"`.
* **MUST** return live outputs rather than spent historical records when serving current state.
* **MUST** preserve or reconstruct retained ancestry sufficient for BRC-64-style history reconstruction.

### D. Wallet-Local Behavior Requirements

Wallet-local implementations:

* **MUST NOT** require new wallet RPC methods beyond those listed in Section 4.2.
* **MUST** map local KV namespace to baskets and tags.
* **MUST** support encrypted local values by default.
* **MAY** offer plaintext local storage as an explicit option.
* **SHOULD** expose or permit manual recovery of corrupted outputs, such as through `relinquishOutput`.

### E. Recommended Conformance Scenarios

Implementations **SHOULD** be tested against at least the following scenarios:

* global create, lookup, update, and delete for one `(protocolID, key, controller)` tuple
* global queries by `key`, `controller`, `protocolID`, and tags with both `all` and `any`
* global rejection of malformed or forged tokens via read-side signature verification
* global history reconstruction across successive updates
* local encrypted `set`, `get`, update, and `remove`
* local plaintext profile operation
* local duplicate-output collapse and newest-live-value selection
* local corrupted-token handling and recovery guidance
* canonical service/topic naming and current overlay host-resolution expectations

## References

* [BRC-46: Wallet Transaction Output Tracking (Output Baskets)](/wallet/0046.md)
* [BRC-48: Pay to Push Drop](/scripts/0048.md)
* [BRC-64: Overlay Network Transaction History Tracking](/overlays/0064.md)
* [BRC-87: Standardized Naming Conventions for BRC-22 Topic Managers and BRC-24 Lookup Services](/overlays/0087.md)
* [BRC-88: Overlay Services Synchronization Architecture](/overlays/0088.md)
* [BRC-99: P Baskets: Allowing Future Wallet Basket and Digital Asset Permission Schemes](/wallet/0099.md)
* [BRC-100: Unified, Vendor-Neutral, Unchanging, and Open BSV Blockchain Standard Wallet-to-Application Interface](/wallet/0100.md)
* [BRC-116: Wallet Permissions and Counterparty Trust](/wallet/0116.md)
* [`bsv-blockchain/ts-sdk`](https://github.com/bsv-blockchain/ts-sdk)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://bsv.brc.dev/overlays/0035.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
