Layered Key-Value Store for Wallets and Overlay Services
Applications constantly need somewhere to keep small pieces of state, and doing that on chain usually means inventing a scheme per project. This standardises a simple named-value store: a public one others can discover, and a private one held in the wallet.
Summary
- Why
- Wallets and overlay services needed a shared, testable definition of what a key-value entry is and how it evolves, instead of behavior that only existed as undocumented code and tests in one SDK.
- What
- BRC-35 is a standard for representing key-value store entries as spendable PushDrop token outputs, with a public overlay-tracked profile and a private wallet-local profile that share one lifecycle model.
- How
- A developer creates a KV entry as a PushDrop output identified by a tuple of protocol ID, key, and either a controller (global profile) or a wallet basket context (local profile), and updates it by spending the current live token into a new one, optionally retaining ancestry for history.
What this lets you do
- Store a key-value entry as a spendable PushDrop token
- Publish publicly discoverable state bound to a controller identity
- Persist private, wallet-scoped state using existing basket methods
- Reconstruct the history chain of prior values for a key
- Verify signatures on global entries instead of trusting the overlay blindly
Written by claude-sonnet-5 from the specification text. Where the two differ, the original is correct.
Reference for an AI
Everything an assistant needs to answer questions about BRC-35 accurately, including what it depends on.
The specification
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:
- Global KVStore: publicly discoverable, overlay-tracked, controller-bound KV tokens.
- 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 for wallet output baskets
- BRC-48 for PushDrop
- BRC-64 for overlay transaction history tracking
- BRC-87 for topic-manager and lookup-service naming conventions
- BRC-88 for overlay service synchronization architecture
- BRC-99 for future basket permission schemes
- BRC-100 for wallet methods and object shapes
- BRC-116 for wallet trust and permission context
The older BRC-22 and BRC-24 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:
protocolIDis encoded as the UTF-8 JSON serialization of the wallet protocol tuple, such as"[1,\"kvstore\"]".keyis encoded as UTF-8 bytes.valueis represented as UTF-8 bytes of an application string.controlleris 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:
protocolIDfrom field 1keyfrom field 2 as thekeyIDcontrollerfrom 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
tagQueryModeis 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 byset.
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:
getMUST return the newest live value represented by the current matching outputs,setMUST spend all currently matching live outputs and create one successor output,removeMUST 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
setoperation 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_kvstoreandtm_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:
overlayBroadcastacceptDelayedBroadcastoriginator- 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 + controllerqueries 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:
{
"key": "profile.displayName",
"controller": "02ab...",
"protocolID": [1, "kvstore"]
}
Example tag query:
{
"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
protocolIDandtagsfrom 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
controllerfield. - MUST sign the exact concatenated semantic field bytes.
- SHOULD use the canonical public names
ls_kvstoreandtm_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>]andkeyID = <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_kvstoreas the canonical lookup-service name. - MUST recognize
tm_kvstoreas the canonical topic-manager topic. - MUST filter by the query schema defined in Section 3.5.
- MUST treat omitted
tagQueryModeas"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 bothallandany - global rejection of malformed or forged tokens via read-side signature verification
- global history reconstruction across successive updates
- local encrypted
set,get, update, andremove - 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)
- BRC-48: Pay to Push Drop
- BRC-64: Overlay Network Transaction History Tracking
- BRC-87: Standardized Naming Conventions for BRC-22 Topic Managers and BRC-24 Lookup Services
- BRC-88: Overlay Services Synchronization Architecture
- BRC-99: P Baskets: Allowing Future Wallet Basket and Digital Asset Permission Schemes
- BRC-100: Unified, Vendor-Neutral, Unchanging, and Open BSV Blockchain Standard Wallet-to-Application Interface
- BRC-116: Wallet Permissions and Counterparty Trust
bsv-blockchain/ts-sdk