diff --git a/README.md b/README.md index 97ae38d..79146bd 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/SUMMARY.md b/SUMMARY.md index f73f594..8c63002 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -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 diff --git a/wallet/0230.md b/wallet/0230.md new file mode 100644 index 0000000..6d3fe9c --- /dev/null +++ b/wallet/0230.md @@ -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 diff --git a/wallet/README.md b/wallet/README.md index ecebc41..8a169c1 100644 --- a/wallet/README.md +++ b/wallet/README.md @@ -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)