|
| 1 | +<pre> |
| 2 | + BIP: ? |
| 3 | + Layer: Applications |
| 4 | + Title: Timelock-Recovery storage format |
| 5 | + |
| 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