Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ BRC | Standard
227 | [Frictionless On-Chain Onboarding via Pre-Funded Claimable Tokens](./apps/0227.md)
228 | [Unlinkable Payments under the Identity Paradigm](./payments/0228.md)
229 | [Wallet-Native Elliptic Curve Point Multiplication as a BRC-98 Module](./wallet/0229.md)
230 | [Wallet Contact Book Basket Profile](./wallet/0230.md)
369 | [Keyed Content and Conditional Key Release](./peer-to-peer/0369.md)

## License
Expand Down
1 change: 1 addition & 0 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
* [Wallet-Enforced Expiry for `noSend` Actions](./wallet/0177.md)
* [Wallet Permission Prompt Liveness Contract](./wallet/0219.md)
* [Wallet-Native Elliptic Curve Point Multiplication as a BRC-98 Module](./wallet/0229.md)
* [Wallet Contact Book Basket Profile](./wallet/0230.md)

## Transactions

Expand Down
135 changes: 135 additions & 0 deletions wallet/0230.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# BRC-230: Wallet Contact Book Basket Profile

Luke Rohenaz (rohenaz@gmail.com)

## Abstract

This BRC defines a **private contact-book profile** over [BRC-35](../overlays/0035.md) local key-value tokens and [BRC-46](./0046.md) / [BRC-100](./0100.md) baskets. It reserves the basket name `contacts`, specifies the KV key / tag, the encrypted JSON value, and the create / update / delete lifecycle. It adds no wallet RPC methods.

The book is presentation metadata the holder uses to recognize payees. It is not an identity certificate, not a MessageBox, and not a public overlay record.

## Motivation

BRC-100 wallets already replicate outputs, baskets, tags, and certificates through storage providers ([BRC-40](../outpoints/0040.md)). Application contact books have instead been kept in device-local preferences, so a name and avatar saved on one installation do not appear on another installation of the same wallet identity.

[BRC-169](../peer-to-peer/0169.md) section 10 says a client should keep an address book and what an entry must eventually record. It does not say how that list replicates between the user's own devices. [BRC-35](../overlays/0035.md) already defines wallet-local KV tokens as spendable PushDrop outputs in a basket. Wallet settings in the reference toolbox already use that local profile. Contacts need the same treatment, with a shared basket name and value schema so independent wallets can read each other's books after a storage sync.

## Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as described in RFC 2119.

### 1. Basket identifier

- The basket name is the UTF-8 string `contacts`.
- Per BRC-46, wallet implementations normalize basket identifiers by trimming whitespace and lowercasing. After normalization, conforming use of this profile MUST use exactly `contacts`.
- This identifier does not use the [BRC-99](./0099.md) reserved `p ` prefix. It is a standard application basket under BRC-46 permissioning.

### 2. Local KV profile

Conforming writers MUST use the [BRC-35](../overlays/0035.md) **local** profile:

| Local KV field | This profile |
|----------------|--------------|
| Context (basket) | `contacts` |
| Key (output tag) | Canonical destination, section 3 |
| Protocol ID | `[2, "contacts"]` |
| Key ID | The same string as the output tag |
| Counterparty | `self` |
| Encryption | Enabled. UTF-8 JSON is encrypted with BRC-100 `encrypt` under that protocol, key ID, and counterparty. Ciphertext is PushDrop field 1. |
| Lock position | `before` (BRC-35 local / current `@bsv/sdk` `PushDrop.lock` default) |
| Satoshis | `1` unless fee policy requires otherwise |

Readers MUST accept both one-field and two-field local tokens, ignoring a trailing PushDrop signature field, per BRC-35 section 4.4.

Plaintext local storage MUST NOT be used for this profile. A contact book is private presentation data.

### 3. Canonical destination and tag

Each live token represents one destination the holder can pay.

The **canonical destination** is one of:

- a paymail (`local@domain`),
- a Bitcoin address, or
- a 33-byte compressed identity public key as 66 lowercase hex characters.

The KV key and the output tag are the canonical destination, trimmed and lowercased. Current BRC-100 storage commonly lowercases tags before store and match ([BRC-164](./0164.md)); writers MUST assume that folding.

The encrypted JSON `destination` field carries the form the wallet will actually pay (original address case, validated paymail, or lowercase identity key). Readers MUST use the JSON `destination` for payment and display, not the tag.

If two destinations collide after lowercasing, the later `updatedAt` wins. Writers SHOULD avoid that collision; it is a consequence of tag folding, not of this profile inventing a hash key.

### 4. JSON value

Field 1, after decryption, MUST be UTF-8 JSON of this object:

```json
{
"v": 1,
"kind": "paymail",
"destination": "satchmo@handcash.io",
"name": "Satchmo",
"avatar": "https://example.com/avatar.png",
"updatedAt": "2026-09-11T19:04:00.000Z",
"deleted": false
}
```

| Field | Required | Meaning |
|-------|----------|---------|
| `v` | yes | Schema version. This document defines `1`. |
| `kind` | yes | `paymail`, `address`, or `identityKey`. |
| `destination` | yes | The payable destination, section 3. |
| `name` | no | Holder-chosen label. Empty or omitted means the wallet may display the destination or a public profile name. |
| `avatar` | no | HTTPS URL of a display image. `http://` MUST be ignored. `ord://` and other schemes are out of scope for version 1. |
| `updatedAt` | yes | RFC 3339 / ISO 8601 UTC timestamp of this version. |
| `deleted` | no | If `true`, this is a tombstone. Readers MUST hide the contact. |

Unknown fields MUST be preserved when a writer updates a record it decoded. Readers MUST ignore unknown fields they do not understand.

`name` and `avatar` are holder annotations. They are not host attestations. A public paymail profile MAY be used to fill blank fields on a device; a later user edit MUST win.

### 5. Lifecycle

For a given canonical tag within one wallet identity:

- **Create** — mint one live token when none exists.
- **Update** — spend every live output with that tag and create one successor (BRC-35 local `set`, including duplicate collapse).
- **Delete** — either spend without replacement (BRC-35 local `remove`) or write a successor with `"deleted": true`. A tombstone is RECOMMENDED when two devices may still hold a cache of the row, so a later hydrate does not resurrect it. Tombstones MAY later be spent without replacement.

`updatedAt` MUST increase on every create, update, or tombstone. If two live tokens exist for one tag, readers MUST prefer the greater `updatedAt`, then spend-and-collapse on the next write.

Listing the book is `listOutputs` on basket `contacts` with `includeTags` and enough of the locking script or transaction to decode field 1.

### 6. Scope and non-goals

This profile:

- belongs to **one wallet identity** (the BRC-100 user whose storage holds the outputs);
- does **not** put names or avatars in certificate fields;
- does **not** deliver records through a MessageBox;
- does **not** publish the book to an overlay;
- does **not** replace [BRC-169](../peer-to-peer/0169.md) section 10's certificate, handle, domain, or last-verification fields. A later schema version MAY add those fields without changing the basket name.

A pairing or backup snapshot MAY copy the current decoded records so a new peer is not empty before storage catch-up. After catch-up, live tokens are authoritative.

### 7. Security considerations

- **Privacy.** Encryption-to-self is mandatory because a storage provider and anyone who can list the basket would otherwise read the address book. The locking public key and tag still reveal that a destination exists.
- **Not authentication.** A stored `name` or `avatar` is not proof of who controls the destination. Paymail host profiles remain unattested per BRC-169.
- **Key change.** Identity-key contacts are subject to the BRC-169 section 4.4 key-change rule when the wallet also stores a verified handle binding. This profile does not itself detect reassignment.
- **Fees.** Each edit is a wallet action. Implementations SHOULD skip a new transaction when the decoded value is unchanged.

## Implementations

- 1Sat Wallet (iOS / Mac Catalyst) uses this basket as the sync store for its contact book, with a device-local cache for display.

## References

1. [BRC-35](../overlays/0035.md) — Layered Key-Value Store for Wallets and Overlay Services
2. [BRC-46](./0046.md) — Wallet Transaction Output Tracking (Output Baskets)
3. [BRC-48](../scripts/0048.md) — Pay to Push Drop
4. [BRC-40](../outpoints/0040.md) — User Wallet Data Synchronization
5. [BRC-100](./0100.md) — Wallet-to-Application Interface
6. [BRC-164](./0164.md) — Output Identity Tags for BRC-100 Wallets
7. [BRC-169](../peer-to-peer/0169.md) — Universal Handle Addressing, section 10
1 change: 1 addition & 0 deletions wallet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ BRC | Standard
177 | [Wallet-Enforced Expiry for `noSend` Actions](./0177.md)
219 | [Wallet Permission Prompt Liveness Contract](./0219.md)
229 | [Wallet-Native Elliptic Curve Point Multiplication as a BRC-98 Module](./0229.md)
230 | [Wallet Contact Book Basket Profile](./0230.md)