githubEdit

Layered Key-Value Store for Wallets and Overlay Services

Ty Everett ([email protected])

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

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

Example tag query:

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

Last updated

Was this helpful?