Skip to content

Commit cdcd267

Browse files
committed
new bip: timelock recovery storage format
1 parent 2017931 commit cdcd267

File tree

1 file changed

+225
-0
lines changed

1 file changed

+225
-0
lines changed
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
<pre>
2+
BIP: ?
3+
Layer: Applications
4+
Title: Timelock-Recovery storage format
5+
Author: Oren <[email protected]>
6+
Status: Draft
7+
Type: Process
8+
Assigned: ?
9+
License: BSD-2-Clause
10+
</pre>
11+
12+
== Abstract ==
13+
14+
This document proposes a standard format for saving timelock-recovery plans, to allow different
15+
wallets to generate them, and different services to monitor/execute them.
16+
17+
== Motivation ==
18+
19+
Pre-signed transactions are one way to create a recovery-plan, for use in case of seed loss or
20+
inheritance.
21+
The most common example is a single pre-signed transaction with an <code>nLocktime</code> set to a
22+
future date, as explained in [[bip-0065.mediawiki|BIP-65]].
23+
One limitation of this approach is that in the happy-flow scenario, when the seed is not lost,
24+
and the <code>nLocktime</code> is about to be reached, the user must access their wallet and spend
25+
one of its UTXOs - in order to revoke the pre-signed transaction and prevent it from being able to
26+
move the funds with no cancellation period.
27+
This could be frustrating, for example, for users that split their seed over multiple geographic
28+
locations.
29+
30+
''Timelock-Recovery plans'' are a way to pre-sign a pair of transactions that eventually move the
31+
funds to one or more secondary wallets - with a special <code>nSequence</code> relative-locktime
32+
in the second transaction, so that the user always has a cancellation-period.
33+
34+
Executing and monitoring a ''Timelock-Recovery plan'' thus requires more than broadcasting and
35+
monitoring a single transaction. It also requires mechanisms for accelerating the first
36+
transaction (which does not move most funds to the secondary wallet), for checking whether
37+
the relative-timelock has passed, and a more nuanced handling of reorgs.
38+
39+
This BIP proposes a standard format for exporting ''Timelock-Recovery plans'' from the wallet that
40+
generated them, and importing them into apps/services for monitoring/execution.
41+
42+
== Specification ==
43+
44+
A ''Timelock-Recovery plan'' consists of two transactions:
45+
46+
* ''Alert Transaction'': A mostly-consolidation transaction that keeps most funds in the original wallet, except for a fee and a small fixed amount that goes to ''anchor-addresses'' - addresses which can be used to accelerate the ''Alert Transaction'' via CPFP. The majority of funds should remain on the original wallet, in a new previously-unused address which we call the ''alert-address''. We use the term ''Alert Transaction'' because it should alert the user that the recovery-plan has been triggered, giving them a limited time to prevent the majority of the funds from moving to the secondary wallets.
47+
* ''Recovery Transaction'': The transaction that moves the funds from the alert-address UTXO from the ''Alert Transaction'' to one or more addresses of secondary wallets (each may receive a different amount). This transaction should have a special <code>nSequence</code> relative-locktime according to the size of cancellation-period requested by the user, following the rules of [[bip-0068.mediawiki|BIP-68]].
48+
49+
Both transactions are expected to have an <code>nVersion</code> of at least 2, and an
50+
<code>nLocktime</code> not higher than the current block height.
51+
Both transactions should be non-malleable, as defined in [[bip-0062.mediawiki|BIP-62]].
52+
53+
=== nSequence calculation ===
54+
55+
Users will specify the cancellation-period in whole days between 2-388.
56+
57+
Following [[bip-0068.mediawiki|BIP-68]], the <code>nSequence</code> can represent a timespan in
58+
units of 512 seconds, when bit (1 << 22) is set. An example calculation is provided below:
59+
60+
<source lang="python">
61+
n_sequence = (1 << 22) | round(cancellation_period_days * 24 * 60 * 60 / 512)
62+
</source>
63+
64+
Users should be notified that the cancellation-period is not guaranteed to be exact (due to miners'
65+
manipulation of block-timestamps).
66+
67+
Less than 2 days of cancellation-period and partial-days are not supported, as they are not useful.
68+
69+
More than 388 days of cancellation-period will overflow the <code>nSequence</code> field bits
70+
allocated for the relative-locktime, and is not supported.
71+
72+
=== JSON format ===
73+
74+
For simplicity, this BIP proposes that a ''Timelock-Recovery plan'' will be saved as a JSON
75+
object.
76+
77+
The JSON object will have the following fields:
78+
79+
* kind (mandatory): must be "timelock-recovery-plan".
80+
* id (mandatory): a non-empty string of up to 100 characters, to represent the plan uniquely (i.e. a UUID, or a server generated ID).
81+
* name (optional): a name for the plan, decided by the user. A string of up to 200 characters.
82+
* description (optional): a description for the plan, decided by the user. A string of up to 10,000 characters.
83+
* created_at (mandatory): an ISO 8601 timestamp of the plan creation time, including timezone offset ('Z' if the timezone is UTC).
84+
* plugin_version (mandatory): The version of the plugin that generated the plan. A string of up to 100 characters.
85+
* wallet_version (mandatory): The version of the wallet that generated the plan. A string of up to 100 characters.
86+
* wallet_name (mandatory): The human-readable name of the wallet app that generated the plan. A string of up to 100 characters.
87+
* wallet_kind (mandatory): The internal name of the wallet app that generated the plan. A string of up to 100 characters.
88+
* timelock_days (mandatory): The cancellation period in whole days. A number between 2 and 388.
89+
* anchor_amount_sats (mandatory): The amount in satoshis sent to each anchor address in the <code>Alert Transaction</code>. We recommend using 600 sats, which is above the dust limit.
90+
* anchor_addresses (mandatory): An array of up to 10,000 Bitcoin addresses that receive the anchor amount in the <code>Alert Transaction</code>. Each address is a string of up to 100 characters.
91+
* alert_address (mandatory): The Bitcoin address (mainnet) that receives the majority of funds in the <code>Alert Transaction</code>. A string of up to 100 characters.
92+
* alert_inputs (mandatory): An array of up to 10,000 inputs spent by the <code>Alert Transaction</code>. Each input is a string in the format "txid:vout" where txid is a 64-character lowercase hexadecimal string and vout is a decimal number of up to 6 digits.
93+
* alert_tx (mandatory): The raw <code>Alert Transaction</code> in uppercase hexadecimal format. A string of up to 800,000 characters.
94+
* alert_txid (mandatory): The transaction ID of the <code>Alert Transaction</code>. A 64-character lowercase hexadecimal string.
95+
* alert_fee (mandatory): The total fee paid by the <code>Alert Transaction</code> in satoshis. A non-negative integer.
96+
* alert_weight (mandatory): The weight of the <code>Alert Transaction</code>. A positive integer.
97+
* recovery_tx (mandatory): The raw <code>Recovery Transaction</code> in uppercase hexadecimal format. A string of up to 800,000 characters.
98+
* recovery_txid (mandatory): The transaction ID of the <code>Recovery Transaction</code>. A 64-character lowercase hexadecimal string.
99+
* recovery_fee (mandatory): The total fee paid by the <code>Recovery Transaction</code> in satoshis. A non-negative integer.
100+
* recovery_weight (mandatory): The weight of the <code>Recovery Transaction</code>. A positive integer.
101+
* recovery_outputs (mandatory): An array of up to 10,000 outputs from the <code>Recovery Transaction</code>. Each output is a tuple containing: <code>[address, amount_sats, label?]</code> where:
102+
** address is a mandatory Bitcoin address string (up to 100 characters).
103+
** amount_sats is a mandatory positive integer representing the amount in satoshis.
104+
** label is an optional string of up to 200 characters.
105+
* metadata (optional): A string of up to 10,000 characters for additional metadata, for example a digital-signature.
106+
* checksum (mandatory): A checksum for verifying the integrity of the plan. A string of 8 to 64 characters.
107+
108+
=== Checksum Calculation ===
109+
Notice that besides the top-level JSON object, all the internal values are either primitive or
110+
arrays.
111+
This is intentional, so a conversion of the values to JSON strings will be deterministic.
112+
113+
The checksum is calculated by converting the top-level JSON object to an array of
114+
<code>[key, value]</code> pairs, sorting the array, stringifying, calculating the
115+
SHA256 hash of the result in lowercase hexadecimal format, and taking a prefix of at least 8
116+
characters.
117+
118+
For example:
119+
<source lang="javascript">
120+
const checksumData = new TextEncoder().encode(
121+
JSON.stringify(Object.entries(recoveryPlanJson).sort()),
122+
);
123+
const checksum = new Uint8Array(await crypto.subtle.digest('SHA-256', checksumData));
124+
const checksumHex = Array.from(checksum).map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 8);
125+
</source>
126+
127+
Checksum hex string should be at least 8 characters long. Wallets may choose to use a longer
128+
checksum.
129+
130+
== Rationale ==
131+
132+
The JSON object will contain the raw transactions, in addition to other information - some of
133+
which could technically be extracted from the raw transactions. This is intentional, to let
134+
frontend UIs display the plan before uploading it to any service, without the need for
135+
complicated parsing in the frontend.
136+
137+
Backend services that receive the JSON object for monitoring/execution are expected to validate
138+
that the information is consistent with the raw transactions.
139+
140+
Also, if some wallet apps did not implement the specifications correctly, the services could
141+
write custom code based on the <code>wallet_kind</code>, <code>wallet_version</code> and
142+
<code>plugin_version</code> fields.
143+
144+
Servers may decide to put more restrictions on JSON objects, for example to refuse
145+
storing very large transactions.
146+
147+
Notice that the raw transactions (<code>alert_tx</code> and <code>recovery_tx</code>) are expected
148+
to be in uppercase hexadecimal format.
149+
This is useful for frontend UIs to display them as QR codes, which are more compact when using
150+
uppercase-only alphanumeric characters.
151+
152+
=== Monitoring Timelock-Recovery Plans ===
153+
154+
Checking whether the <code>Alert Transaction</code> is valid is trivial, via the
155+
<code>testmempoolaccept</code> RPC call in bitcoin core 0.17+.
156+
157+
However, checking whether the <code>Recovery Transaction</code> is valid is more complex,
158+
since it depends on a UTXO created by the <code>Alert Transaction</code>.
159+
160+
The <code>testmempoolaccept</code> RPC can receive a list of transactions in which the later
161+
transactions may depend on earlier transactions - however in our case the
162+
<code>Recovery Transaction</code> has an <code>nSequence</code> relative-locktime, and therefore
163+
calling <code>testmempoolaccept 'alert-tx' 'recovery-tx'</code> will fail, claiming that the
164+
<code>Alert Transaction</code> UTXO is not confirmed (and the required time window has not passed).
165+
166+
We recommend services that want to verify the entire <code>Timelock-Recovery plan</code> to parse
167+
the <code>Recovery Transaction</code> and check its signatures manually, and reject complicated
168+
spending scripts. Discovering that the <code>Recovery Transaction</code> is invalid only at the
169+
time of execution, could lead to funds being locked forever.
170+
171+
== Reference Implementation ==
172+
173+
JSON files can be generated using the Timelock Recovery plugin on
174+
[https://electrum.org Electrum Wallet]:
175+
176+
https://github.com/spesmilo/electrum/tree/master/electrum/plugins/timelock_recovery
177+
178+
Demo Video: https://drive.google.com/file/d/10uXRouQbH1kz_HC14WnmRnYHa3gPZY8l/preview
179+
180+
Example JSON file:
181+
182+
<source lang="json">
183+
{
184+
"kind": "timelock-recovery-plan",
185+
"id": "exported-692452189b301b561ed57cbe",
186+
"name": "Recovery Plan ac300e72-7612-497e-96b0-df2fdeda59ea",
187+
"description": "RITREK APP 1.1.0: Trezor Account #1",
188+
"created_at": "2025-11-24T12:39:53.532Z",
189+
"plugin_version": "1.0.1",
190+
"wallet_version": "1.0.1",
191+
"wallet_name": "RITREK Service",
192+
"wallet_kind": "RITREK BACKEND",
193+
"timelock_days": 2,
194+
"anchor_amount_sats": 600,
195+
"anchor_addresses": [
196+
"bc1qnda6x2gxdh3yujd2zjpsd7qzx3awxmlaf9wwlk"
197+
],
198+
"alert_address": "bc1qj0f9sjenwyjs0u7mlgvptjp05z3syzq7mru3ep",
199+
"alert_inputs": [
200+
"a265a485df4c6417019b91379257eb387bceeda96f7bb6311794b8ed358cf104:0",
201+
"2f621c2151f33173983133cbc1000e3b603b8a18423b0379feffe8513171d5d3:0"
202+
],
203+
"alert_tx": "0200000000010204F18C35EDB8941731B67B6FA9EDCE7B38EB579237919B0117644CDF85A465A20000000000FDFFFFFFD3D5713151E8FFFE79033B42188A3B603B0E00C1CB3331987331F351211C622F0000000000FDFFFFFF0258020000000000001600149B7BA329066DE24E49AA148306F802347AE36FFD205600000000000016001493D2584B33712507F3DBFA1815C82FA0A302081E02483045022100DCDBAE77C35EB4A0B3ED0DE5484206AB6B07041BE99B2BBAF0243C125916523C0220396959C3C52B2B1F9E472AEEE7C5D9540531B131C3221DE942754C6D0941397D012103C08FF3ADBA14B742646572BCA6F07AEB910666FB28E4DDDC40E33755E7C869D30248304502210089084472FDA3CF82D6ABC11BF1A5E77C9B423617C8B840F58C02746035B3BA6302203942AA1FA13F952F49FB114D48130A9AAF70151E7D09036D15734DB1F41A8B6001210397064EDED7DAD7D662290DC2847E87C5C27DA8865B89DDB58FDE9A006BA7DB3900000000",
204+
"alert_txid": "f1413fedadaf30697820bcd8f6a393fcc73ea00a15bea3253f89d5658690d2f7",
205+
"alert_fee": 231,
206+
"alert_weight": 834,
207+
"recovery_tx": "02000000000101F7D2908665D5893F25A3BE150AA03EC7FC93A3F6D8BC20786930AFADED3F41F101000000005201400001A6550000000000001600149B7BA329066DE24E49AA148306F802347AE36FFD0247304402204AFF87C2127F5697F300C6522067A8D5E5290CA8D140D2E5BCEF4A36606C5FE5022056673BEC5BB459DFFBD4D266EE95AEF0D701383ED80BD433A02C3C486A826D76012102774DBCD59F2D08EFF718BC09972ADC609FBC31C26B551B3E4EA30A1D43EEDB9700000000",
208+
"recovery_txid": "bc304610e8f282036345e87163d4cba5b16488a3bf2e4d738379d7bda3a0bca3",
209+
"recovery_fee": 122,
210+
"recovery_weight": 437,
211+
"recovery_outputs": [
212+
[
213+
"bc1qnda6x2gxdh3yujd2zjpsd7qzx3awxmlaf9wwlk",
214+
21926,
215+
"My Backup Wallet"
216+
]
217+
],
218+
"metadata": "sig:825d6b3858c175c7fc16da3134030e095c4f9089c3c89722247eeedc08a7ef4f",
219+
"checksum": "92f8b3da"
220+
}
221+
</source>
222+
223+
== Copyright ==
224+
225+
This document is licensed under the 2-clause BSD license.

0 commit comments

Comments
 (0)