mirror of
https://github.com/bitcoin/bips.git
synced 2026-10-19 19:29:18 +00:00
Clarify that an empty payload contains zero content items. Add a vector that rejects a payload consisting only of the 0x00 terminator.
641 lines
28 KiB
Markdown
641 lines
28 KiB
Markdown
```
|
||
BIP: 138
|
||
Layer: Applications
|
||
Title: Compact Encryption Scheme for Non-seed Wallet Data
|
||
Authors: Pyth <pythcoiner@wizardsardine.com>
|
||
Status: Draft
|
||
Type: Specification
|
||
Assigned: 2026-05-11
|
||
License: BSD-2-Clause
|
||
Discussion: https://delvingbitcoin.org/t/a-simple-backup-scheme-for-wallet-accounts/1607
|
||
https://groups.google.com/g/bitcoindev/c/5NgJbpVDgEc/m/TtGK9sF9BgAJ
|
||
https://github.com/bitcoin/bips/pull/1951
|
||
Version: 0.1.1
|
||
Requires: 32, 340, 380, 388, 389
|
||
```
|
||
|
||
## Introduction
|
||
|
||
### Abstract
|
||
|
||
This BIP defines a compact encryption scheme for **output script descriptors** (BIP-0380),
|
||
**wallet policies** (BIP-0388), and other wallet metadata.
|
||
The payload must not contain any private key material.
|
||
|
||
Users can store encrypted backups on untrusted media or cloud services without leaking
|
||
addresses, script structures, or cosigner counts. The encryption key derives from the
|
||
lexicographically-sorted public keys in the descriptor, allowing any keyholder to decrypt
|
||
without additional secrets.
|
||
|
||
Though designed for descriptors and policies, the scheme works equally well for labels
|
||
and backup metadata.
|
||
|
||
### Copyright
|
||
|
||
This BIP is licensed under the BSD 2-Clause License.
|
||
Redistribution and use in source and binary forms, with or without modification, are
|
||
permitted provided that the above copyright notice and this permission notice appear
|
||
in all copies.
|
||
|
||
### Motivation
|
||
|
||
Losing the **wallet descriptor** (or **wallet policy**) is almost as catastrophic as
|
||
losing the seed itself. The seed lets you sign, but the descriptor maps you to your coins.
|
||
For multisig or miniscript wallets, keys alone are insufficient: without the descriptor, you
|
||
can't reconstruct the script.
|
||
|
||
Multisig setups can be designed to retain access to coins even if one of the
|
||
seeds is lost, for example a 3-of-3 MuSig2 keypath with a script path that lets
|
||
2-of-3 recover. But when one seed is lost, its public key may also be lost,
|
||
rendering the coins inaccessible. This risk is not intuitive.
|
||
|
||
Offline storage of descriptors has two practical obstacles:
|
||
|
||
1. **Descriptors are hard to store offline.**
|
||
Descriptors can be much longer than a 12/24-word seed. Paper and steel backups
|
||
become impractical or error-prone, while USB sticks and other offline digital
|
||
storage solutions degrade over time and are unlikely to survive flood or fire.
|
||
|
||
2. **Online redundancy carries privacy risk.**
|
||
Phones and cloud storage solve the length problem but expose your wallet
|
||
structure. Plaintext descriptors leak your pubkeys and script details.
|
||
Cloud storage is often unencrypted, and even cloud encryption could be compromised,
|
||
depending on (often opaque) implementation details. Its security also reduces to
|
||
that of the weakest device with cloud access. Each copy increases the attack surface.
|
||
|
||
This BIP therefore proposes an **encrypted**, compact backup format that:
|
||
|
||
* can be **safely stored in multiple places**, including untrusted online services,
|
||
* can be **decrypted only by intended holders** of specified _public_ keys,
|
||
|
||
See the original [Delving post](https://delvingbitcoin.org/t/a-simple-backup-scheme-for-wallet-accounts/1607/31)
|
||
for more background.
|
||
|
||
### Expected properties
|
||
|
||
* **Encrypted**: safe to store with untrusted cloud providers or backup services
|
||
* **Access controlled**:
|
||
* only designated cosigners can decrypt, using their xpub
|
||
* can't be decrypted using public keys revealed on chain
|
||
* **Easy to implement**:
|
||
* it should not require any sophisticated tools/libraries
|
||
* it should not require access to private key material, which secure signing systems may not yield
|
||
* **Vendor-neutral**: works with any hardware signer
|
||
|
||
### Scope
|
||
|
||
This proposal targets output script descriptors (BIP-0380) and policies (BIP-0388), but the
|
||
scheme also works for labels (BIP-0329) and other wallet metadata like
|
||
wallet backup metadata (BIP-0139).
|
||
|
||
Private key material MUST be removed before encrypting any payload.
|
||
This is because the use of _public key_ material for decryption makes the
|
||
scheme unsuitable for storing private key material.
|
||
|
||
## Specification
|
||
|
||
Note: in the following sections, the operator ⊕ refers to the bitwise XOR operation.
|
||
|
||
### Secret generation
|
||
|
||
- Let $p_1, p_2, \dots, p_n$, be the public keys in the descriptor/wallet policy, in
|
||
increasing lexicographical order. The scheme is defined for any $n \geq 1$,
|
||
in particular it supports single-signature descriptors ($n = 1$). Each $p_i$
|
||
is the x-only-normalized root public key of one *allowed* key expression
|
||
(see [Descriptor key requirements](#descriptor-key-requirements)).
|
||
- Let $s$ = tagged_hash("BIP138_DECRYPTION_SECRET", $p_1$ | $p_2$ | ... | $p_n$)
|
||
- Let $s_i$ = tagged_hash("BIP138_INDIVIDUAL_SECRET", $p_i$)
|
||
- Let $c_i$ = $s$ ⊕ $s_i$
|
||
|
||
where tagged_hash(tag, m) = sha256(sha256(tag) | sha256(tag) | m) is the tagged
|
||
hash construction from [BIP340](bip-0340.mediawiki#design).
|
||
|
||
Because $s$ and $s_i$ use distinct domain-separation tags, $s \neq s_i$ and
|
||
therefore $c_i$ is never the all-zero string.
|
||
|
||
Keys published on chain are already kept out of $\{p_1, \dots, p_n\}$ by the
|
||
[Descriptor key requirements](#descriptor-key-requirements).
|
||
Implementations MUST exclude the key with x coordinate
|
||
`50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0` (the BIP341 NUMS
|
||
point). Applications MAY exclude any further key they know to be public.
|
||
|
||
Applications that exclude keys beyond the NUMS point SHOULD document which keys they
|
||
exclude, so that users know which keys cannot open the backup. A cosigner holding an
|
||
excluded key will simply fail to decrypt, while the cosigners holding the keys that
|
||
remain in $\{p_1, \dots, p_n\}$ can still decrypt the backup.
|
||
|
||
**Note:** A key is *publicly known* when a third party can learn it without the wallet
|
||
owner disclosing it. There is no registry of such keys. In practice they come from two
|
||
sources: the blockchain, and well-known constants.
|
||
|
||
**Warning:** An xpub can be known to a third party without ever appearing on chain.
|
||
Some single-signature wallets, such as the Ledger and Trezor desktop apps, send the
|
||
BIP44/49/84/86 account xpub to a server, the server then derive every key from
|
||
that xpub. For a single-signature descriptor this leaks little the server does not
|
||
already know, but reusing such an xpub as a key of a multisig descriptor lets that
|
||
server decrypt every backup of the multisig. Users SHOULD build a multisig from
|
||
accounts whose xpub was never sent to such a server, such as BIP48 or BIP87 accounts.
|
||
|
||
### Descriptor key requirements
|
||
|
||
Only key expressions of the extended-public-key type with a trailing
|
||
derivation step or wildcard contribute to $\{p_1, \dots, p_n\}$. Without
|
||
that derivation, the xpub's root pubkey, used to seed $s$, would also be
|
||
the on-chain pubkey of every spend, so observing one spend would let
|
||
anyone recompute $s$ and decrypt the backup.[^trailing-derivation] The
|
||
rule generalizes to any network prefix (`xpub`/`tpub`/`Vpub`/...).
|
||
|
||
[^trailing-derivation]: **Why require trailing derivation?**
|
||
Any trailing derivation step, or the implicit child derivation forced by
|
||
a wildcard, breaks the identity between the xpub root pubkey and the
|
||
on-chain pubkey. Fixed-derivation expressions like `xpub.../0/5` are
|
||
allowed because the on-chain key (`xpub/0/5`) already differs from the
|
||
encryption seed (the xpub root). Literal pubkeys and bare xpubs are
|
||
disallowed for the same reason: the literal value is exactly what goes
|
||
on-chain, so $s$ would become recoverable from a single observed spend.
|
||
|
||
Allowed forms (origin information `[...]` is optional and orthogonal):
|
||
|
||
| Form | Example |
|
||
|-------------------------------------------------------------|-------------------|
|
||
| `<xpub>/<path>` (fixed derivation, no wildcard) | `xpub.../0/5` |
|
||
| `<xpub>/*` (wildcard only) | `xpub.../*` |
|
||
| `<xpub>/<path>/*` (fixed derivation followed by a wildcard) | `xpub.../0/*` |
|
||
| `<xpub>/<a;b;...>` (multipath, no wildcard) | `xpub.../<0;1>` |
|
||
| `<xpub>/<a;b;...>/*` (multipath followed by a wildcard) | `xpub.../<0;1>/*` |
|
||
|
||
Implementations:
|
||
|
||
- MUST exclude every other form (literal pubkeys, bare xpubs, ...) from
|
||
$\{p_1, \dots, p_n\}$.
|
||
- MUST refuse to encode a backup if the resulting set is empty.
|
||
- MUST make the user aware of each excluded expression, since the
|
||
cosigner holding that key will be unable to decrypt the backup with
|
||
their key.
|
||
|
||
### Key Normalization
|
||
|
||
For each allowed expression, take the root extended public key (ignoring
|
||
origin information, the trailing derivation path, the wildcard, and any
|
||
multipath specifiers) and extract its x-coordinate. The result is the
|
||
32-byte **x-only public key** $p_i$.[^x-only]
|
||
|
||
[^x-only]: **Why x-only keys?**
|
||
X-only public keys are 32 bytes, a natural size for cryptographic operations.
|
||
This format is also used in BIP340 (Schnorr signatures) and BIP341 (Taproot).
|
||
|
||
### Encryption
|
||
|
||
The format uses CHACHA20_POLY1305 (RFC 8439) as the encryption algorithm,
|
||
with a 96-bit random nonce and a 128-bit authentication tag to provide confidentiality
|
||
and integrity.[^chacha-default]
|
||
|
||
[^chacha-default]: **Why CHACHA20-POLY1305 ?**
|
||
ChaCha20-Poly1305 is already used in Bitcoin Core (e.g., BIP324) and is widely
|
||
available in cryptographic libraries. It performs well in software without
|
||
hardware acceleration, making it suitable for hardware wallets and embedded devices.
|
||
|
||
* let $nonce$ = random(96 bits)
|
||
* let $ciphertext$ = encrypt($payload$, $secret$, $nonce$, $aad$)
|
||
|
||
The additional authenticated data ($aad$) is empty.
|
||
|
||
The $nonce$ MUST NOT be all zero. If the random source yields an all-zero nonce,
|
||
the encoder MUST generate a new one.
|
||
|
||
### Decryption
|
||
|
||
In order to decrypt the payload of a backup, the owner of a certain public key p
|
||
computes:
|
||
|
||
* let $s_i$ = tagged_hash("BIP138_INDIVIDUAL_SECRET", $p$)
|
||
* for each `individual_secret_i` generate `reconstructed_secret_i` =
|
||
`individual_secret_i` ⊕ `si`
|
||
* for each `reconstructed_secret_i` process $payload$ =
|
||
decrypt($ciphertext$, $secret$, $nonce$, $aad$)
|
||
|
||
Decryption will succeed if and only if **p** was one of the keys in the
|
||
descriptor/wallet policy.
|
||
|
||
### Encoding
|
||
|
||
The encrypted backup must be encoded as follows:
|
||
|
||
`MAGIC` `VERSION` `DERIVATION_PATHS` `INDIVIDUAL_SECRETS` `ENCRYPTION`
|
||
`ENCRYPTED_PAYLOAD`
|
||
|
||
#### Magic
|
||
|
||
`MAGIC`: 6 bytes which are ASCII/UTF-8 representation of **BIP138**.
|
||
|
||
#### Version
|
||
|
||
`VERSION`: 1 byte unsigned integer representing the format version. The current
|
||
specification defines version `0x01`.
|
||
|
||
#### Derivation Paths
|
||
|
||
Derivation paths are optional; they can be useful to simplify the recovery process
|
||
if one has used a non-common derivation path to derive his key.[^derivation-optional]
|
||
|
||
All implementations that can fetch xpubs from a signing device SHOULD try the following
|
||
common paths automatically:
|
||
|
||
- `m/44h/<coin>h/{0..9}h`
|
||
- `m/49h/<coin>h/{0..9}h`
|
||
- `m/84h/<coin>h/{0..9}h`
|
||
- `m/86h/<coin>h/{0..9}h`
|
||
- `m/87h/<coin>h/{0..9}h`
|
||
- `m/48h/<coin>h/{0..9}h/1h`
|
||
- `m/48h/<coin>h/{0..9}h/2h`
|
||
|
||
`<coin>` is the BIP44 coin type of the network being recovered: `0` for mainnet
|
||
and `1` for test networks. `{0..9}h` covers accounts 0 through 9.
|
||
|
||
Some signing devices can fail or require confirmation when fetching xpubs for
|
||
some of these paths. This list is not exhaustive: an implementation MAY treat a
|
||
path as allowed to fail for any device it supports, in particular
|
||
`m/87h/<coin>h/{0..9}h`, which is widely under-supported. Implementations SHOULD
|
||
at least classify these:[^common-derivation-paths]
|
||
|
||
- BitBox02: `m/44h/<coin>h/{0..9}h`, `m/87h/<coin>h/{0..9}h`
|
||
- Ledger: `m/87h/<coin>h/{0..9}h`
|
||
|
||
If fetching an xpub for one of these common paths fails or times out and
|
||
decryption fails, implementations SHOULD warn the user.
|
||
|
||
[^derivation-optional]: **Why are derivation paths optional?**
|
||
When standard derivation paths are used, they are easily discoverable, making
|
||
them straightforward to brute-force. Omitting them enhances privacy by reducing
|
||
the information shared publicly about the descriptor scheme.
|
||
|
||
[^common-derivation-paths]: **Why this restricted list?**
|
||
The list covers common account paths for the network being recovered, accounts 0
|
||
through 9. Some signing devices reject or display some of these paths before
|
||
exporting an xpub, so implementations should treat the listed paths for that
|
||
device as optional rather than aborting recovery.
|
||
|
||
`DERIVATION_PATH` follows this format:
|
||
|
||
`COUNT`
|
||
`CHILD_COUNT` `CHILD` `...` `CHILD`
|
||
`...`
|
||
`CHILD_COUNT` `CHILD` `...` `CHILD`
|
||
|
||
`COUNT`: 1-byte unsigned integer (0–255) indicating how many derivation paths are
|
||
included.
|
||
`CHILD_COUNT`: 1-byte unsigned integer (1–255) indicating how many children are in
|
||
the current path.
|
||
`CHILD`: 4-byte big-endian unsigned integer representing a child index per BIP-32.
|
||
|
||
The derivation-path vector SHOULD be encoded in lexicographic order. This avoids
|
||
leaking information through implementation-specific ordering. The vector SHOULD NOT
|
||
contain duplicates. Implementations SHOULD deduplicate paths during encoding or
|
||
parsing.
|
||
|
||
#### Individual Secrets
|
||
|
||
At least one individual secret must be supplied.[^no-fingerprints]
|
||
|
||
[^no-fingerprints]: **Why no fingerprints in plaintext encoding?**
|
||
Including fingerprints would leak direct information about the descriptor
|
||
participants, which compromises privacy.
|
||
|
||
The `INDIVIDUAL_SECRETS` section follows this format:
|
||
|
||
`COUNT`
|
||
`INDIVIDUAL_SECRET`
|
||
`INDIVIDUAL_SECRET`
|
||
|
||
`COUNT`: 1-byte unsigned integer (1–255) indicating how many secrets are included.
|
||
`INDIVIDUAL_SECRET`: 32-byte serialization of the derived individual secret.
|
||
|
||
The individual secrets vector SHOULD be encoded in lexicographic order. This avoids
|
||
leaking information through implementation-specific ordering. The vector SHOULD NOT
|
||
contain duplicates. Implementations SHOULD deduplicate secrets during encoding or
|
||
parsing.
|
||
|
||
Encoders SHOULD append random decoy `INDIVIDUAL_SECRET` entries to hide the exact
|
||
number of real decryption keys. The total count SHOULD be rounded up to the
|
||
smallest bucket greater than or equal to the real count, starting at 5 and doubling
|
||
for each following bucket: 5, 10, 20, and so on. The bucket sequence saturates at
|
||
the 255 `COUNT` limit: when the smallest fitting bucket would exceed 255, encoders
|
||
SHOULD pad to 255 instead. Decoders do not distinguish real
|
||
entries from decoys; they try each entry until one decrypts the payload.
|
||
|
||
#### Encryption
|
||
|
||
`ENCRYPTION`: 1-byte unsigned integer identifying the encryption algorithm.
|
||
|
||
| Value | Definition |
|
||
|:-------|:---------------------------------------|
|
||
| 0x00 | Reserved |
|
||
| 0x01 | CHACHA20_POLY1305 |
|
||
|
||
#### Payload Size Limits
|
||
|
||
CHACHA20_POLY1305 (per RFC 8439) supports plaintext up to 2^38 - 64 bytes.
|
||
Implementations MAY impose stricter limits based on platform constraints
|
||
(e.g., limiting to 2^32 - 1 bytes on 32-bit architectures).
|
||
|
||
Implementations MUST reject empty payloads (i.e. zero content items).
|
||
|
||
#### Ciphertext
|
||
|
||
`CIPHERTEXT` is the encrypted data resulting from encryption of `PAYLOAD` with algorithm
|
||
defined in `ENCRYPTION` where `PAYLOAD` is encoded following this format:
|
||
|
||
`CONTENT_TYPE` `CONTENT_LENGTH` `CONTENT` (`CONTENT_TYPE` `CONTENT_LENGTH` `CONTENT` ...) (`PADDING`)
|
||
|
||
`CONTENT_LENGTH`: variable-length integer representing the length of `CONTENT` in bytes.
|
||
It MUST be present.
|
||
|
||
`CONTENT`: the `CONTENT_LENGTH` bytes of payload data.
|
||
|
||
`PADDING`: OPTIONAL bytes after the final `CONTENT`, up to the end of the decrypted
|
||
`PAYLOAD`. Parsers MUST consume exactly `CONTENT_LENGTH` bytes of each `CONTENT`. The padding
|
||
bytes are reserved for size padding (see Padding) and/or vendor-specific data, the same
|
||
way trailing bytes after `CIPHERTEXT` are reserved and ignored.
|
||
|
||
#### Padding
|
||
|
||
Backups that are updated over time can leak information through their size even when the
|
||
contents are encrypted. A wallet that uploads a new backup after every received transaction
|
||
can reveal activity to the storage provider through changing payload sizes. This is relevant
|
||
for mutable payloads such as BIP-329 or BIP-0139.[^padding-privacy]
|
||
|
||
Padding is OPTIONAL and is an encoder concern only: decoders ignore the `PADDING` bytes and
|
||
MUST NOT validate any bucket. Because the padding sits inside `CIPHERTEXT`, the
|
||
`ENCRYPTED_PAYLOAD` `LENGTH` reveals only the padded size, not the real `PAYLOAD` size.
|
||
|
||
Encoders that pad SHOULD zero-fill `PAYLOAD` up to the smallest bucket `S_n` that fits it,
|
||
with `S_0 = 10240` (10 KiB) and `S_n = floor(S_0 * (5 / 4)^n)`. Payloads of 10 KiB or less
|
||
are padded to 10 KiB. This keeps padding overhead under 25% above the smallest fitting bucket
|
||
while coarsening the observable size. Padding is RECOMMENDED for mutable payloads and
|
||
unnecessary for static ones such as BIP-380 descriptors and BIP-388 wallet policies.
|
||
|
||
[^padding-privacy]: **Why pad inside the ciphertext?**
|
||
Padding the encrypted payload, rather than appending bytes after the backup, hides the
|
||
real payload size from anyone who parses the backup, not just from someone who measures
|
||
the file size. The size field a parser sees is the padded `ENCRYPTED_PAYLOAD` `LENGTH`.
|
||
|
||
#### Integer Encodings
|
||
|
||
All variable-length integers are encoded as
|
||
[compact size](https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer).
|
||
|
||
#### Content Type
|
||
|
||
`CONTENT_TYPE` is a variable length field defining the type of the `CONTENT` that follows
|
||
it. It follows this format:
|
||
|
||
`TYPE` (`TYPE_LENGTH`) `TYPE_PARAMS`
|
||
|
||
A `PAYLOAD` carries one or more `CONTENT_TYPE CONTENT_LENGTH CONTENT` items, each `CONTENT_TYPE`
|
||
describing the `CONTENT` immediately following it. The sequence ends at the first
|
||
`TYPE` byte equal to `0x00`, or at the end of the `PAYLOAD`, whichever comes first; all
|
||
remaining bytes are `PADDING`. A `PAYLOAD` MUST contain at least one item (see
|
||
[Payload Size Limits](#payload-size-limits)).
|
||
|
||
`TYPE`: 1-byte unsigned integer identifying how to interpret `TYPE_PARAMS`.
|
||
|
||
| Value | Definition |
|
||
|:-------|:---------------------------------------|
|
||
| 0x00 | End of content items; padding follows |
|
||
| 0x01 | BIP Number (big-endian uint16) |
|
||
| 0x02 | Vendor-Specific Opaque Tag |
|
||
| 0x03 | String |
|
||
|
||
`TYPE_LENGTH`: variable-length integer representing the length of `TYPE_PARAMS` in bytes.
|
||
|
||
For all `TYPE` values except `0x00` and `0x01`, `TYPE_LENGTH` MUST be present.[^length-required]
|
||
|
||
[^length-required]: **Why is `TYPE_LENGTH` mandatory?**
|
||
`TYPE_LENGTH` is what lets a parser skip a content type it does not implement and still
|
||
read the items that follow; without it, the parser cannot know where `TYPE_PARAMS` ends.
|
||
A string note placed before a descriptor, for example, must not stop an
|
||
implementation that only handles descriptors from recovering that descriptor. A
|
||
type whose `TYPE_PARAMS` is always empty still carries a zero `TYPE_LENGTH` for this reason.
|
||
`0x01` is exempt because every implementation handles it, so it is never skipped
|
||
blindly. `0x00` is a terminator rather than a content item.
|
||
|
||
`TYPE_PARAMS`: variable-length field whose encoding depends on `TYPE`.
|
||
|
||
For `TYPE` values defined above:
|
||
- 0x00: parsers MUST stop immediately and treat the remaining bytes as padding.
|
||
- 0x01: `TYPE_LENGTH` MUST be omitted and `TYPE_PARAMS` is a 2-byte big-endian unsigned integer
|
||
representing the BIP number that defines it.
|
||
- 0x02: `TYPE_PARAMS` MUST be `TYPE_LENGTH` bytes of opaque, vendor-specific data.
|
||
- 0x03: `TYPE_PARAMS` MUST be empty, and the following `CONTENT` is the string itself,
|
||
which MUST be valid UTF-8.
|
||
|
||
For all `TYPE` values except `0x00` and `0x01`, parsers MUST reject `CONTENT_TYPE` if `TYPE_LENGTH` exceeds
|
||
the remaining payload bytes.
|
||
|
||
For an unknown `TYPE` less than `0x80`, parsers MUST consume its `TYPE_LENGTH` bytes of
|
||
`TYPE_PARAMS`, treat the content type as unknown, consume the following payload
|
||
`CONTENT_LENGTH` and `CONTENT`, and continue with the next item.
|
||
|
||
For an unknown `TYPE` greater than or equal to `0x80`, parsers MUST reject the
|
||
payload.[^type-upgrade]
|
||
|
||
[^type-upgrade]: **Why the 0x80 threshold?**
|
||
The `TYPE >= 0x80` rule keeps the encoding upgradeable. A future version can
|
||
define a new content type at or above `0x80` that older parsers reject rather
|
||
than misread, while types below `0x80` stay backward compatible.
|
||
|
||
#### BIP380 Descriptor Backup Content
|
||
|
||
When `CONTENT_TYPE` is `TYPE = 0x01` with `TYPE_PARAMS = 0x017c` (BIP380), `CONTENT` is a
|
||
UTF-8 BIP380 descriptor backup. It is either a single bare descriptor string, or
|
||
a JSON descriptor backup document (for several descriptors) when the first
|
||
character is `{`.
|
||
|
||
Test vectors are in
|
||
[`bip380_descriptor_backup.json`](./bip-0138/test_vectors/bip380_descriptor_backup.json).
|
||
|
||
Descriptor strings MUST NOT contain private key material and SHOULD include a
|
||
checksum.
|
||
|
||
##### JSON Descriptor Backup Documents
|
||
|
||
The descriptor backup document is a JSON object with the following fields:
|
||
|
||
- `version`: integer. This specification defines version `1`.
|
||
- `descriptor_sets`: array of descriptor set objects.
|
||
|
||
Each descriptor set describes BIP380 output script descriptors belonging to one
|
||
logical account or script family.
|
||
|
||
##### Descriptor Set Fields
|
||
|
||
`descriptor` is a required string containing a BIP380 output script descriptor.
|
||
|
||
For BIP389 multipath descriptors, `/<0;1>` means receive and change,
|
||
respectively. The `change_descriptor` field MUST NOT be used.
|
||
|
||
For descriptors without BIP389 multipath key expressions, `descriptor` is the
|
||
receive descriptor. The optional `change_descriptor` string can be omitted for a
|
||
receive-only set.
|
||
|
||
If optional boolean `archived` is `true`, importing wallets SHOULD NOT use the
|
||
descriptor set for new address generation.
|
||
|
||
`range` is an optional two-element array `[start, end]`, inclusive, describing
|
||
the [BIP32](bip-0032.mediawiki) non-hardened child indexes covered by the
|
||
descriptor set, with `start <= end`.
|
||
|
||
`birth_time` is an optional integer Unix timestamp in seconds, indicating a
|
||
lower bound for when the descriptor set may have received funds. Importing
|
||
wallets MAY use this value as a scanning hint.
|
||
If `0` or omitted, no lower bound is known. Importers SHOULD scan from the wallet
|
||
birthday if known, otherwise from genesis.
|
||
|
||
#### BIP388 Wallet Policy Backup Content
|
||
|
||
When `CONTENT_TYPE` is `TYPE = 0x01` with `TYPE_PARAMS = 0x0184` (BIP388), `CONTENT` is a
|
||
UTF-8 JSON wallet policy backup document. A BIP388 wallet policy is a descriptor
|
||
template plus a vector of key information items, as defined in BIP388.
|
||
|
||
Test vectors are in
|
||
[`bip388.json`](./bip-0138/test_vectors/bip388.json).
|
||
|
||
The document is one of two forms:
|
||
|
||
- A single policy, a JSON object `{ "keys": [...], "policy": "<template>" }`.
|
||
- Multiple policies, a JSON object `{ "version": 1, "policy_sets": [...] }`.
|
||
|
||
Parsers select the form by structure: an object with a `policy_sets` field is the
|
||
multiple form, an object with a `policy` field is the single form.
|
||
|
||
##### Policy Fields
|
||
|
||
`keys` is a required array of BIP388 key information items, each a serialized
|
||
extended public key with optional key origin (`[fingerprint/path]xpub`). It MUST
|
||
contain at least one key. Keys MUST NOT contain private key material.
|
||
|
||
`policy` is a required non-empty string containing a BIP388 wallet descriptor
|
||
template, with `@i` key placeholders referring to `keys`.
|
||
|
||
##### Policy Set Fields
|
||
|
||
A policy set has the same `keys` and `policy` fields as the single form, plus the
|
||
optional metadata fields:
|
||
|
||
If optional boolean `archived` is `true`, importing wallets SHOULD NOT use the
|
||
policy for new address generation.
|
||
|
||
`range` is an optional two-element array `[start, end]`, inclusive, describing
|
||
the [BIP32](bip-0032.mediawiki) non-hardened child indexes covered by the
|
||
policy, with `start <= end`.
|
||
|
||
`birth_time` is an optional integer Unix timestamp in seconds, indicating a lower
|
||
bound for when the policy may have received funds. Importing wallets MAY use this
|
||
value as a scanning hint.
|
||
If `0` or omitted, no lower bound is known. Importers SHOULD scan from the wallet
|
||
birthday if known, otherwise from genesis.
|
||
|
||
##### Multiple Policies Document
|
||
|
||
The multiple form is a JSON object with the following fields:
|
||
|
||
- `version`: integer. This specification defines version `1`.
|
||
- `policy_sets`: array of policy set objects.
|
||
|
||
#### Encrypted Payload
|
||
|
||
`ENCRYPTED_PAYLOAD` follows this format:
|
||
|
||
`NONCE` `LENGTH` `CIPHERTEXT`
|
||
|
||
`NONCE`: 12-byte (96-bit) nonce. Parsers MUST reject a backup whose `NONCE` is all zero.
|
||
`LENGTH`: variable-length integer representing ciphertext length.
|
||
`CIPHERTEXT`: variable-length ciphertext.
|
||
|
||
Note: `CIPHERTEXT` is followed by the end of the `ENCRYPTED_PAYLOAD` section.
|
||
Compliant parsers MUST stop reading after consuming `LENGTH` bytes of ciphertext;
|
||
additional trailing bytes are reserved for vendor-specific extensions and MUST
|
||
be ignored.
|
||
|
||
### Text Representation
|
||
|
||
Implementations SHOULD encode and decode the backup using Base64 (RFC 4648).[^psbt-base64]
|
||
|
||
[^psbt-base64]: **Why Base64?**
|
||
PSBT (BIP174) is commonly exchanged as a Base64 string, so wallet software
|
||
likely already supports this representation.
|
||
|
||
### File Extension
|
||
|
||
Backups saved to a file SHOULD use the `.bip138` file extension for the raw binary
|
||
encoding and the `.txt` extension for the Base64 text representation, so that the
|
||
extension tells implementations which encoding to expect.
|
||
|
||
## Rationale
|
||
|
||
See footnotes throughout the specification for design rationale.
|
||
|
||
### Future Extensions
|
||
|
||
The version field enables possible future enhancements:
|
||
|
||
- Additional encryption algorithms
|
||
- Support for threshold-based decryption
|
||
- Hiding number of participants
|
||
- bech32m export
|
||
|
||
### Implementation
|
||
|
||
- Rust [implementation](https://github.com/pythcoiner/bip138)
|
||
|
||
### Test Vectors
|
||
|
||
[key_types.json](./bip-0138/test_vectors/keys_types.json) contains test
|
||
vectors for key serialisations.
|
||
[content_type.json](./bip-0138/test_vectors/content_type.json) contains test
|
||
vectors for content types serialisations.
|
||
[payload.json](./bip-0138/test_vectors/payload.json) contains test vectors for payload parsing.
|
||
`payload` and each item's `content` are hexadecimal bytes; `type` is the numeric
|
||
type byte, and `items` lists the content items before the terminator.
|
||
|
||
[derivation_path.json](./bip-0138/test_vectors/derivation_path.json) contains
|
||
test vectors for derivation paths serialisations.
|
||
[individual_secrets.json](./bip-0138/test_vectors/individual_secrets.json)
|
||
contains test vectors for individual secrets serialization.
|
||
[encryption_secret.json](./bip-0138/test_vectors/encryption_secret.json)
|
||
contains test vectors for generation of encryption secret.
|
||
[chacha20poly1305_encryption.json](./bip-0138/test_vectors/chacha20poly1305_encryption.json)
|
||
contains test vectors for ciphertexts generated using CHACHA20-POLY1305.
|
||
[encrypted_backup.json](./bip-0138/test_vectors/encrypted_backup.json)
|
||
contains test vectors for generation of complete encrypted backups, including
|
||
BIP380 and BIP388 content, a multi-content payload, and an all-zero-nonce backup
|
||
that parsers MUST reject.
|
||
[bip380_descriptor_backup.json](./bip-0138/test_vectors/bip380_descriptor_backup.json)
|
||
contains test vectors for BIP380 descriptor backup documents.
|
||
[bip388.json](./bip-0138/test_vectors/bip388.json)
|
||
contains test vectors for BIP388 wallet policy backup documents.
|
||
|
||
## Backward Compatibility
|
||
|
||
This BIP defines a new format and changes no existing one. Liana wallet uses an early
|
||
version of this BIP, which is not compatible: its magic is `BEB` rather than `BIP138`.
|
||
|
||
## Acknowledgements
|
||
|
||
This BIP builds on the backup scheme proposed by @bigspider on
|
||
[Delving Bitcoin](https://delvingbitcoin.org/t/a-simple-backup-scheme-for-wallet-accounts/1607),
|
||
who also suggested the payload padding design.
|
||
|
||
Thanks to @Sjors for extensive reviews, many design contributions to the payload
|
||
encoding, and a proof-of-concept implementation on top of Bitcoin Core. Thanks
|
||
also to @murchandamus, @jonatack, @craigraw, @jp1ac4, and @torkelrogstad for
|
||
their reviews and suggestions.
|
||
|
||
## Changelog
|
||
|
||
* __0.1.1__ (2026-09-23):
|
||
* Clarify derivation range bounds and zero or omitted birth-time semantics for descriptors and policies.
|
||
* Clarify receive-only descriptor sets and add a corresponding test vector.
|
||
* __0.1.0__ (2026-09-21):
|
||
* Initial publication.
|