-
Notifications
You must be signed in to change notification settings - Fork 5.9k
BIP Draft: Timelock-Recovery Storage Format #2068
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
oren-z0
wants to merge
13
commits into
bitcoin:master
Choose a base branch
from
oren-z0:new-bip-timelock-recovery-storage-format
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+266
−0
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
b76f390
new bip: timelock recovery storage format
oren-z0 ccb0dd8
Comparison with Script-Based Wallets
oren-z0 f1fe10e
Type is Specification
oren-z0 a581d40
Change Authors to a single Author
oren-z0 fe83fc5
Replace OP_VAULT mention with OP_CHECKCONTRACTVERIFY
oren-z0 f5743f5
Only the Alert Transaction needs to be non-malleable
oren-z0 76bca31
Adding discussion link
oren-z0 4cbfb1e
limiting the transactions weight
oren-z0 a18dd73
Explain anchor-addresses
oren-z0 2396392
fix typo
oren-z0 5538fb4
add surname initial to author name
oren-z0 38acc9d
Explain unintentional initiation of rrecovery-plan.
oren-z0 dc80bae
limit alert_inputs length to 2439
oren-z0 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,266 @@ | ||
| <pre> | ||
| BIP: ? | ||
| Layer: Applications | ||
| Title: Timelock-Recovery storage format | ||
| Authors: Oren Z <orenz0@protonmail.com> | ||
| Status: Draft | ||
| Type: Specification | ||
| Discussion: https://groups.google.com/g/bitcoindev/c/K1NpJp9_BYk | ||
| Assigned: ? | ||
| License: BSD-2-Clause | ||
| </pre> | ||
|
|
||
| == Abstract == | ||
|
|
||
| This document proposes a standard format for saving timelock-recovery plans, to allow different | ||
| wallets to generate them, and different services to monitor/execute them. | ||
|
|
||
| == Motivation == | ||
|
|
||
| Pre-signed transactions are one way to create a recovery-plan, for use in case of seed loss or | ||
| inheritance. | ||
| The most common example is a single pre-signed transaction with an <code>nLocktime</code> set to a | ||
| future date, as explained in [[bip-0065.mediawiki|BIP-65]]. | ||
| One limitation of this approach is that in the happy-flow scenario, when the seed is not lost, | ||
| and the <code>nLocktime</code> is about to be reached, the user must access their wallet and spend | ||
| one of its UTXOs - in order to revoke the pre-signed transaction and prevent it from being able to | ||
| move the funds with no cancellation period. | ||
| This could be frustrating, for example, for users that split their seed over multiple geographic | ||
| locations. | ||
|
|
||
| ''Timelock-Recovery plans'' are a way to pre-sign a pair of transactions that eventually move the | ||
| funds to one or more secondary wallets - with a special <code>nSequence</code> relative-locktime | ||
| in the second transaction, so that the user always has a cancellation-period. | ||
|
|
||
| Executing and monitoring a ''Timelock-Recovery plan'' thus requires more than broadcasting and | ||
| monitoring a single transaction. It also requires mechanisms for accelerating the first | ||
| transaction (which does not move most funds to the secondary wallet), for checking whether | ||
| the relative-timelock has passed, and a more nuanced handling of reorgs. | ||
|
|
||
| This BIP proposes a standard format for exporting ''Timelock-Recovery plans'' from the wallet that | ||
| generated them, and importing them into apps/services for monitoring/execution. | ||
|
|
||
| === Comparison with Script-Based Wallets === | ||
|
|
||
| Script-based wallets are another way to create recovery mechanisms, and can use absolute and | ||
| relative locktimes using OP_CHECKLOCKTIMEVERIFY ([[bip-0065.mediawiki|BIP-65]]) and | ||
| OP_CHECKSEQUENCEVERIFY ([[bip-0112.mediawiki|BIP-112]]). | ||
| For example, we can build a script that allows one main key to spend the funds at any time, | ||
| and a secondary key to spend the funds only in transactions with nLocktime above a certain | ||
| date/block-height, or only in transactions with nSequence above a certain relative | ||
| time-gap/number-of-blocks. | ||
| This makes the secondary key useful only after an absolute date/block-height, or after | ||
| a relative time since the funds were received (each UTXO independently). | ||
| This approach does have some advantages over pre-signed transactions, for example the | ||
| recovery-mechanism automatically applies to new funds received into the wallet. | ||
|
|
||
| However, script-based wallets have some disadvantages over a sequence of | ||
| pre-signed transactions: | ||
|
|
||
| * Script-based wallets are harder to implement correctly by hardware wallets, and harder to backup properly (i.e. users may forget to backup wallet-descriptors even for basic multisig wallets). | ||
| * As of the time of writing, scripts can limit when secondary-keys can be used, but not how they can be used: if the user doesn't touch the wallets' UTXOs for long-enough time, the secondary key will eventually become useable and could move the funds anywhere. This is true whether we measure the time in absolute terms (OP_CHECKLOCKTIMEVERIFY) or relative terms compared to when the wallets' UTXOs were created (OP_CHECKSEQUENCEVERIFY). This means that even in the happy-flow scenario of an untouched wallet, where no recovery is needed, the user must periodically "renew" the recovery-mechanism by spending the UTXO to a new wallet/address. This may be inconvenient in ultra-cold-storage scenarios (i.e. multisig with main keys hidden in different geographic locations). New opcode suggestions, such as OP_CHECKTEMPLATEVERIFY ([[bip-0119.mediawiki|BIP-119]]) and OP_CHECKCONTRACTVERIFY ([[bip-0443.mediawiki|BIP-443]]), discuss possible recovery-mechanisms in which in order for a secondary key to have full control over the funds, some onchain operations must be performed, with a required time-gap between them - giving the user enough time to revoke the whole process and move the funds elsewhere (assuming they still have the main key and the recovery-mechanism was triggered unintentionally). However, these suggestions are still in the discussion phase and even if ever implemented, their adoption may be slow. | ||
| * New Bitcoiners today typically don't think of such recovery-mechanisms in advance, and start with a P2WPKH wallet. They can pre-sign transactions with this wallet, but to utilize script-based features they would need to create a new wallet and move the funds there - an operation that might seem intimidating for large amounts. | ||
|
|
||
| == Specification == | ||
|
|
||
| A ''Timelock-Recovery plan'' consists of two transactions: | ||
|
|
||
| * ''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 monitoring the blockchain and looking for it should alert the user that the recovery-plan has been initiated (intentionally, unintentionally or maliciously). | ||
| * ''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]]. | ||
|
|
||
| With a reliable tool to monitor the blockchain for the ''Alert Transaction'' | ||
| or the ''Alert Address'', the user can safely store online backups of the recovery plan's | ||
| JSON file (or, even without a tool, by checking the blockchain manually from time to time). | ||
| If the presigned transactions leak and the ''Alert Transaction'' is broadcast | ||
| unintentionally, the user has the cancellation period (expected to be at least a | ||
| few days) to prevent most funds from moving by sending them to a new address, thereby | ||
| invalidating the ''Recovery Transaction''. | ||
|
|
||
| It is important that the ''Alert Transaction'' will be non-malleable (e.g. by using | ||
| [[bip-0140.mediawiki|BIP-140]]). | ||
| If a malleable ''Alert Transaction'' is used, a malicious miner could replace the | ||
| ''Alert Transaction'' with a similar transaction with a different txid, | ||
| making the ''Recovery Transaction'' invalid (pointing to a non-existent UTXO). | ||
|
|
||
| The <code>nLocktime</code> of both transactions should not be higher than the current | ||
| block height. | ||
|
|
||
| The ''anchor-addresses'' mentioned above, which are used for CPFP acceleration, could possibly | ||
| be P2A addresses (described in [[bip-0433.mediawiki|BIP-433]]), or other addresses under the | ||
| participants' control (i.e. addresses from the secondary wallets). | ||
| As of the time of writing, P2A is not widely adopted, and less-technical users may | ||
| struggle using them for CPFP acceleration - so we currently recommend using regular addresses. | ||
|
|
||
| === nSequence calculation === | ||
|
|
||
| Users will specify the cancellation-period in whole days between 2-388. | ||
|
|
||
| Following [[bip-0068.mediawiki|BIP-68]], the <code>nSequence</code> can represent a timespan in | ||
| units of 512 seconds, when bit (1 << 22) is set. An example calculation is provided below: | ||
|
|
||
| <source lang="python"> | ||
| n_sequence = (1 << 22) | round(cancellation_period_days * 24 * 60 * 60 / 512) | ||
| </source> | ||
|
|
||
| Users should be notified that the cancellation-period is not guaranteed to be exact (due to miners' | ||
| manipulation of block-timestamps). | ||
|
|
||
| Less than 2 days of cancellation-period and partial-days are not supported, as they are not useful. | ||
|
|
||
| More than 388 days of cancellation-period will overflow the <code>nSequence</code> field bits | ||
| allocated for the relative-locktime, and is not supported. | ||
|
|
||
| === JSON format === | ||
|
|
||
| For simplicity, this BIP proposes that a ''Timelock-Recovery plan'' will be saved as a JSON | ||
| object. | ||
|
|
||
| The JSON object will have the following fields: | ||
|
|
||
| * kind (mandatory): must be "timelock-recovery-plan". | ||
| * 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). | ||
| * name (optional): a name for the plan, decided by the user. A string of up to 200 characters. | ||
| * description (optional): a description for the plan, decided by the user. A string of up to 10,000 characters. | ||
| * created_at (mandatory): an ISO 8601 timestamp of the plan creation time, including timezone offset ('Z' if the timezone is UTC). | ||
| * plugin_version (mandatory): The version of the plugin that generated the plan. A string of up to 100 characters. | ||
| * wallet_version (mandatory): The version of the wallet that generated the plan. A string of up to 100 characters. | ||
| * wallet_name (mandatory): The human-readable name of the wallet app that generated the plan. A string of up to 100 characters. | ||
| * wallet_kind (mandatory): The internal name of the wallet app that generated the plan. A string of up to 100 characters. | ||
| * timelock_days (mandatory): The cancellation period in whole days. A number between 2 and 388. | ||
| * 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. | ||
| * 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. | ||
|
Comment on lines
+130
to
+131
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would recommend checking out P2A as mentioned above.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See previous comments. |
||
| * 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. | ||
| * alert_inputs (mandatory): An array of up to 2439 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. The maximal length of 2439 is calculated from a standard transaction of 400,000 wu where each input contains at least 41 bytes. | ||
| * alert_tx (mandatory): The raw <code>Alert Transaction</code> in uppercase hexadecimal format. A string of up to 800,000 characters. | ||
| * alert_txid (mandatory): The transaction ID of the <code>Alert Transaction</code>. A 64-character lowercase hexadecimal string. | ||
| * alert_fee (mandatory): The total fee paid by the <code>Alert Transaction</code> in satoshis. A non-negative integer. | ||
| * alert_weight (mandatory): The weight of the <code>Alert Transaction</code>. A positive integer, not higher than 400,000. | ||
| * recovery_tx (mandatory): The raw <code>Recovery Transaction</code> in uppercase hexadecimal format. A string of up to 800,000 characters. | ||
| * recovery_txid (mandatory): The transaction ID of the <code>Recovery Transaction</code>. A 64-character lowercase hexadecimal string. | ||
| * recovery_fee (mandatory): The total fee paid by the <code>Recovery Transaction</code> in satoshis. A non-negative integer. | ||
| * recovery_weight (mandatory): The weight of the <code>Recovery Transaction</code>. A positive integer, not higher than 400,000. | ||
| * 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: | ||
| ** address is a mandatory Bitcoin address string (up to 100 characters). | ||
| ** amount_sats is a mandatory positive integer representing the amount in satoshis. | ||
| ** label is an optional string of up to 200 characters. | ||
| * metadata (optional): A string of up to 10,000 characters for additional metadata, for example a digital-signature. | ||
| * checksum (mandatory): A checksum for verifying the integrity of the plan. A string of 8 to 64 characters. | ||
|
|
||
| === Checksum Calculation === | ||
| Notice that besides the top-level JSON object, all the internal values are either primitive or | ||
| arrays. | ||
| This is intentional, so a conversion of the values to JSON strings will be deterministic. | ||
|
|
||
| The checksum is calculated by converting the top-level JSON object to an array of | ||
| <code>[key, value]</code> pairs, sorting the array, stringifying, calculating the | ||
| SHA256 hash of the result in lowercase hexadecimal format, and taking a prefix of at least 8 | ||
| characters. | ||
|
|
||
| For example: | ||
| <source lang="javascript"> | ||
| const checksumData = new TextEncoder().encode( | ||
| JSON.stringify(Object.entries(recoveryPlanJson).sort()), | ||
| ); | ||
| const checksum = new Uint8Array(await crypto.subtle.digest('SHA-256', checksumData)); | ||
| const checksumHex = Array.from(checksum).map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 8); | ||
| </source> | ||
|
|
||
| Checksum hex string should be at least 8 characters long. Wallets may choose to use a longer | ||
| checksum. | ||
|
|
||
| == Rationale == | ||
|
|
||
| The JSON object will contain the raw transactions, in addition to other information - some of | ||
| which could technically be extracted from the raw transactions. This is intentional, to let | ||
| frontend UIs display the plan before uploading it to any service, without the need for | ||
| complicated parsing in the frontend. | ||
|
|
||
| Backend services that receive the JSON object for monitoring/execution are expected to validate | ||
| that the information is consistent with the raw transactions. | ||
|
|
||
| Also, if some wallet apps did not implement the specifications correctly, the services could | ||
| write custom code based on the <code>wallet_kind</code>, <code>wallet_version</code> and | ||
| <code>plugin_version</code> fields. | ||
|
|
||
| Servers may decide to put more restrictions on JSON objects, for example to refuse | ||
| storing very large transactions. | ||
|
|
||
| Notice that the raw transactions (<code>alert_tx</code> and <code>recovery_tx</code>) are expected | ||
| to be in uppercase hexadecimal format. | ||
| This is useful for frontend UIs to display them as QR codes, which are more compact when using | ||
| uppercase-only alphanumeric characters. | ||
|
|
||
| === Monitoring Timelock-Recovery Plans === | ||
|
|
||
| Checking whether the <code>Alert Transaction</code> is valid is trivial, via the | ||
| <code>testmempoolaccept</code> RPC call in bitcoin core 0.17+. | ||
|
|
||
| However, checking whether the <code>Recovery Transaction</code> is valid is more complex, | ||
| since it depends on a UTXO created by the <code>Alert Transaction</code>. | ||
|
|
||
| The <code>testmempoolaccept</code> RPC can receive a list of transactions in which the later | ||
| transactions may depend on earlier transactions - however in our case the | ||
| <code>Recovery Transaction</code> has an <code>nSequence</code> relative-locktime, and therefore | ||
| calling <code>testmempoolaccept 'alert-tx' 'recovery-tx'</code> will fail, claiming that the | ||
| <code>Alert Transaction</code> UTXO is not confirmed (and the required time window has not passed). | ||
|
|
||
| We recommend services that want to verify the entire <code>Timelock-Recovery plan</code> to parse | ||
| the <code>Recovery Transaction</code> and check its signatures manually, and reject complicated | ||
| spending scripts. Discovering that the <code>Recovery Transaction</code> is invalid only at the | ||
| time of execution, could lead to funds being locked forever. | ||
|
|
||
| == Reference Implementation == | ||
|
|
||
| JSON files can be generated using the Timelock Recovery plugin on | ||
| [https://electrum.org Electrum Wallet]: | ||
|
|
||
| https://github.com/spesmilo/electrum/tree/master/electrum/plugins/timelock_recovery | ||
|
|
||
| Demo Video: https://drive.google.com/file/d/10uXRouQbH1kz_HC14WnmRnYHa3gPZY8l/preview | ||
|
|
||
| Example JSON file: | ||
|
|
||
| <source lang="json"> | ||
| { | ||
| "kind": "timelock-recovery-plan", | ||
| "id": "exported-692452189b301b561ed57cbe", | ||
| "name": "Recovery Plan ac300e72-7612-497e-96b0-df2fdeda59ea", | ||
| "description": "RITREK APP 1.1.0: Trezor Account #1", | ||
| "created_at": "2025-11-24T12:39:53.532Z", | ||
| "plugin_version": "1.0.1", | ||
| "wallet_version": "1.0.1", | ||
| "wallet_name": "RITREK Service", | ||
| "wallet_kind": "RITREK BACKEND", | ||
| "timelock_days": 2, | ||
| "anchor_amount_sats": 600, | ||
| "anchor_addresses": [ | ||
| "bc1qnda6x2gxdh3yujd2zjpsd7qzx3awxmlaf9wwlk" | ||
| ], | ||
| "alert_address": "bc1qj0f9sjenwyjs0u7mlgvptjp05z3syzq7mru3ep", | ||
| "alert_inputs": [ | ||
| "a265a485df4c6417019b91379257eb387bceeda96f7bb6311794b8ed358cf104:0", | ||
| "2f621c2151f33173983133cbc1000e3b603b8a18423b0379feffe8513171d5d3:0" | ||
| ], | ||
| "alert_tx": "0200000000010204F18C35EDB8941731B67B6FA9EDCE7B38EB579237919B0117644CDF85A465A20000000000FDFFFFFFD3D5713151E8FFFE79033B42188A3B603B0E00C1CB3331987331F351211C622F0000000000FDFFFFFF0258020000000000001600149B7BA329066DE24E49AA148306F802347AE36FFD205600000000000016001493D2584B33712507F3DBFA1815C82FA0A302081E02483045022100DCDBAE77C35EB4A0B3ED0DE5484206AB6B07041BE99B2BBAF0243C125916523C0220396959C3C52B2B1F9E472AEEE7C5D9540531B131C3221DE942754C6D0941397D012103C08FF3ADBA14B742646572BCA6F07AEB910666FB28E4DDDC40E33755E7C869D30248304502210089084472FDA3CF82D6ABC11BF1A5E77C9B423617C8B840F58C02746035B3BA6302203942AA1FA13F952F49FB114D48130A9AAF70151E7D09036D15734DB1F41A8B6001210397064EDED7DAD7D662290DC2847E87C5C27DA8865B89DDB58FDE9A006BA7DB3900000000", | ||
| "alert_txid": "f1413fedadaf30697820bcd8f6a393fcc73ea00a15bea3253f89d5658690d2f7", | ||
| "alert_fee": 231, | ||
| "alert_weight": 834, | ||
| "recovery_tx": "02000000000101F7D2908665D5893F25A3BE150AA03EC7FC93A3F6D8BC20786930AFADED3F41F101000000005201400001A6550000000000001600149B7BA329066DE24E49AA148306F802347AE36FFD0247304402204AFF87C2127F5697F300C6522067A8D5E5290CA8D140D2E5BCEF4A36606C5FE5022056673BEC5BB459DFFBD4D266EE95AEF0D701383ED80BD433A02C3C486A826D76012102774DBCD59F2D08EFF718BC09972ADC609FBC31C26B551B3E4EA30A1D43EEDB9700000000", | ||
| "recovery_txid": "bc304610e8f282036345e87163d4cba5b16488a3bf2e4d738379d7bda3a0bca3", | ||
| "recovery_fee": 122, | ||
| "recovery_weight": 437, | ||
| "recovery_outputs": [ | ||
| [ | ||
| "bc1qnda6x2gxdh3yujd2zjpsd7qzx3awxmlaf9wwlk", | ||
| 21926, | ||
| "My Backup Wallet" | ||
| ] | ||
| ], | ||
| "metadata": "sig:825d6b3858c175c7fc16da3134030e095c4f9089c3c89722247eeedc08a7ef4f", | ||
| "checksum": "92f8b3da" | ||
| } | ||
| </source> | ||
|
|
||
| == Copyright == | ||
|
|
||
| This document is licensed under the 2-clause BSD license. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Has this been submitted to the mailing list? If so, could you please add a Discussion header with the URL to the discussion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, here: https://groups.google.com/g/bitcoindev/c/K1NpJp9_BYk
Added the link to a "Discussion" header (even though I did not see a similar Discussion header in other bips).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While it’s only present on some 30ish BIPs, it’s a header that we have been pushing more for lately.