Beersy
BRC-35

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.

Ty Everettchanged 1 May 202615 min read
kv tokenpushdrop outpu

Summary

Why
Wallets and 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 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 of , key, and either a (global profile) or a wallet 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 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 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 for wallet
  • BRC-48 for PushDrop
  • BRC-64 for 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.
  • : The compressed public 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 is (protocolID, key, controller). In the local profile the tuple is (context, key) within one wallet.
  • : 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 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:

  • : 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 with lock position before.

The canonical semantic field order is:

FieldMeaningEncoding
1protocolIDUTF-8 JSON serialization of the protocol tuple
2keyUTF-8
3valueUTF-8
4controllerbytes of compressed public key hex
5tagsUTF-8 JSON serialization of string array
6signaturePushDrop signature

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

FieldMeaning
1protocolID
2key
3value
4controller
5signature

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

FieldTypeMeaning
keystringMatch a specific key
controllerstringMatch a specific controller public key
protocolID[number, string]Match a specific wallet protocol tuple
tagsstring[]Match tags
tagQueryMode'all' | 'any'Tag matching mode
limitintegerPage size
skipintegerResult 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 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:

CapabilityExisting wallet method(s)
create or update local KV tokenscreateAction, signAction
enumerate current local KV tokenslistOutputs
encrypt valuesencrypt
decrypt valuesdecrypt
manual recovery of corrupted outputsrelinquishOutput

This BRC does not define any new 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:

FieldMeaning
1value bytes
2optional 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 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 artifactStatus under BRC-35
LocalKVStore comments describing automatic relinquish-on-failureNon-normative stale commentaryCurrent interoperable behavior does not automatically call relinquishOutput during normal failure handling
overlayHost config surface in current SDK typesNon-normativePresent as a configuration artifact, but not part of the current interoperable protocol behavior
Legacy global no-tags tokensNormatively valid compatibility inputParsers 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:

FieldExample value
protocolID[1, "kvstore"]
keyprofile.displayName
valueTy
controller02ab...
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 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.

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

Was this helpful?

Search Beersy

Search standards by number, title, author or topic