From 5e18d0b06f2f6439f8ba2d3c52398f340bcff569 Mon Sep 17 00:00:00 2001 From: sstone Date: Thu, 8 Feb 2024 19:09:38 +0100 Subject: [PATCH] Rework recovery procedure The current recovery process needed to be updated to derive the correct master priv key from the seed by specifying our custom BIP32 path (m/52h/0h/2h/0) when we create the wallet. We also export 2 descriptor methods: one to get the private swap-in wallet descriptor, which can be used as-is, and the other to get the public swap-in wallet descriptor, which can be used to create a watch-only wallet to monitor swap-in funds and to recovery funds using our recovery procedure. Both descriptor use the refund master key, and not the master key itself because we use hardened paths to derive the refund key, which means that it is not possible to compute the refund master public key from the master public: importing the descriptor would fail. --- RECOVERY.md | 142 +++++++++++------- .../fr/acinq/lightning/crypto/KeyManager.kt | 12 +- .../lightning/transactions/SwapInProtocol.kt | 90 ++++++----- .../crypto/LocalKeyManagerTestsCommon.kt | 26 +++- 4 files changed, 163 insertions(+), 107 deletions(-) diff --git a/RECOVERY.md b/RECOVERY.md index 894f9e3a0..52eb9f0b6 100644 --- a/RECOVERY.md +++ b/RECOVERY.md @@ -1,8 +1,8 @@ # Funds recovery -:warning: to recover swap-in funds sent to older versions of Phoenix (up to and including version 2.1.2) pleaser refer to [this guide](https://github.com/ACINQ/lightning-kmp/blob/v1.5.15/RECOVERY.md) +:warning: to recover swap-in funds sent to older versions of Phoenix (up to and including version 2.1.2) please refer to [this guide](https://github.com/ACINQ/lightning-kmp/blob/v1.5.15/RECOVERY.md) -The following steps lets you recover on-chain funds managed by `lightning-kmp`. +The following steps let you recover on-chain funds managed by `lightning-kmp`. ## Closed channels @@ -36,52 +36,75 @@ The swap transaction's output can be spent using either: Funds can be recovered using the second option and [Bitcoin Core](https://github.com/bitcoin/bitcoin). This process needs at least Bitcoin Core 26.0. -This process will become simpler once popular on-chain wallets (such as [electrum](https://electrum.org/)) add supports for output script descriptors. +This process will become simpler once popular on-chain wallets (such as [electrum](https://electrum.org/)) add support for output script descriptors. -### Get your wallet descriptor +### Create recovery wallet -lightning-kmp provides both a public descriptor and private descriptor for your swap-in wallet. -The public descriptor can be used to create a watch-only wallet for your swap-in funds. -The private descriptor can be used to recover your swap-in funds, after the refund delay has passed. -:warning: Do not share this private descriptor with anyone ! +#### Compute your refund master private key -### Create recovery wallet +For security reasons, we don't directly export the refund master private key used for swap-ins, so you will need to manually insert it in the descriptor. +You can obtain your extended master private key in [electrum](https://electrum.org/). -Create a wallet to recover your funds using the following command: +1. Create a new wallet, and choose `Standard wallet` +2. Choose `I already have a seed` +3. Enter your 12-word seed, and in the `Options` menu select `BIP39 seed` +4. In the `Script type and Derivation path` dialog select `legacy(p2pkh)` and override the derivation path with `m/52h/0h/2h/0` +5. In the `Console` tab, enter `wallet.keystore.xprv`. This will give you your refund master private key -```sh -bitcoin-cli createwallet recovery -``` +#### Create your refund wallet descriptor -### Import descriptor into the recovery wallet +Copy the descriptor from the `SWAP_IN WALLET` section in the `Wallet Info` menu on your Phoenix wallet. It should look like this: -`lightning-kmp` provides a public and private descriptor for your swap-in wallet, which both use the following template: +```txt +tr(,and_v(v:pk(/),older())) +``` +For example: ```txt -tr(,and_v(v:pk(/),older())) +tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(xpub6EE2N7jrues5kfjrsyFA5f7hknixqqAEKs8vyMN4QW9vDmYnChzpeBPkBYduBobbe4miQ34xHG4Jpwuq5bHXLZY1xixoGynW31ySUqqVvcU/*),older(2590)))#sv8ug44m ``` -For example, your public descriptor will look like this: +You can check that the extended public key in this descriptor matches the extended public key of the wallet you created with Electrum to compute your refund master private key. +Replace the `refund master public key` with your refund master private key. For example: ```txt -tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(tpubDDqzCA42sbCmnGBcuuiAeLGqB9XHU5Gy1n68omeKf4pwFKe2padzkdXAPsDMWMdee879oPYrGrTS8sioqyjv8b6TztunE526eo4Au9kTef3/*),older(25920)))#z6mq2a3u +tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(xprvA1EfxcCy5HJnYBfPmwi9iXAyCktUSNSNxeDLAxxSrAcwLyDdfAga6P5GLHNdq7EiXe8Pzu6Py6xGwT7UTkw824FYf3v6fbRStvYsWqFTu29/*),older(2590)))#sv8ug44m ``` -And your private descriptor will look like this: +### Create a bitcoin core recovery wallet +Create a wallet to recover your funds using the following command: + +```shell +bitcoin-cli -named createwallet wallet_name=recovery ``` -tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(tprv8h9x3k1njDX6to9q2G3aEvcic81MJk64SUVMXFc2Eo2YQqPGCBpQa8uJDkTz3DMHVXEmvhuwf4ShjLQ7YaVr34x9DFT3y43cPzVKGB94r1n/*),older(25920)))#7dne06j5 -``` -We can import our private descriptor into our recovery wallet: +### Import your descriptor into the recovery wallet + +We can import our private descriptor into our recovery wallet. Since your replaced you refund master public key with your refund master private key, the descriptor checksum is no longer valid, but bitcoin core will give you the correct checksum: + +```shell +bitcoin-cli -rpcwallet=recovery importdescriptors '[{ "desc": "tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(xprvA1EfxcCy5HJnYBfPmwi9iXAyCktUSNSNxeDLAxxSrAcwLyDdfAga6P5GLHNdq7EiXe8Pzu6Py6xGwT7UTkw824FYf3v6fbRStvYsWqFTu29/*),older(2590)))#sv8ug44m", "timestamp":0}]' +[ + { + "success": false, + "error": { + "code": -5, + "message": "Provided checksum 'sv8ug44m' does not match computed checksum 'ksphr9r4'" + } + } +] +``` -```sh -bitcoin-cli -rpcwallet=recovery importdescriptors '[{ "desc": "tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(tprv8ZgxMBicQKsPdKRFLVct6VDpfmCxk6aC7iAF8tb6roQ7hv1zFCyGwDLBUUxMVJ95dTiQS5VvCbQ6J7CcGqguw5SbnDpNjbjpfVwcMwUtmjS/51h/0h/0h/*),older(25920)))#rn7cy7yr", "timestamp": 0 }]' +Update the checksum and try again: +```shell +bitcoin-cli -rpcwallet=recovery importdescriptors '[{ "desc": "tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(xprvA1EfxcCy5HJnYBfPmwi9iXAyCktUSNSNxeDLAxxSrAcwLyDdfAga6P5GLHNdq7EiXe8Pzu6Py6xGwT7UTkw824FYf3v6fbRStvYsWqFTu29/*),older(2590)))#ksphr9r4", "timestamp":0}]' [ { "success": true, "warnings": [ + "Range not given, using default keypool range", "Not all private keys provided. Some wallet functionality may return unexpected errors" ] } @@ -93,14 +116,14 @@ This is a slow process, which can be sped up by setting the `timestamp` field to Once Bitcoin Core is done with the scanning process, the `getwalletinfo` command will return `"scanning": false`: -```sh +```shell bitcoin-cli -rpcwallet=recovery getwalletinfo { "walletname": "recovery", "walletversion": 169900, "format": "sqlite", - "balance": 1.50000000, + "balance": 0.00003000, "unconfirmed_balance": 0.00000000, "immature_balance": 0.00000000, "txcount": 1, @@ -111,37 +134,39 @@ bitcoin-cli -rpcwallet=recovery getwalletinfo "avoid_reuse": false, "scanning": false, "descriptors": true, - "external_signer": false + "external_signer": false, + "blank": false, + "birthtime": 1707742312, + "lastprocessedblock": { + "hash": "00000000000000000001760b2e9b05c08275c664d78c1ae59093faa64b57b3b2", + "height": 830146 + } } ``` You can then find available funds matching the descriptor we imported: -```sh -bitcoin-cli -rpcwallet=recovery listtransactions - +```shell + bitcoin-cli -rpcwallet=recovery listtransactions [ { - "address": "bcrt1pzz7rudhpqyy6zdnuwrg3dpnethckfzncma2urxghuc62dz49zenqv0p0q6", + "address": "bc1p6pxx4mp43xkac222jmfy958gpxqn7duku6cka9ahdfmdp9aak74sza58es", "parent_descs": [ - "tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(tpubDDqzCA42sbCmnGBcuuiAeLGqB9XHU5Gy1n68omeKf4pwFKe2padzkdXAPsDMWMdee879oPYrGrTS8sioqyjv8b6TztunE526eo4Au9kTef3/*),older(144)))#zqam8e56" + "tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(xpub6EE2N7jrues5kfjrsyFA5f7hknixqqAEKs8vyMN4QW9vDmYnChzpeBPkBYduBobbe4miQ34xHG4Jpwuq5bHXLZY1xixoGynW31ySUqqVvcU/*),older(2590)))#sv8ug44m" ], "category": "receive", - "amount": 0.10000000, - "vout": 0, + "amount": 0.00003000, + "vout": 1, "abandoned": false, - "confirmations": 1, - "blockhash": "06361beb06e7d24bea80fc6800f4b5f374f09542a07fae77a7f8c26a9f7544b2", - "blockheight": 146, - "blockindex": 1, - "blocktime": 1700670588, - "txid": "4c3236b1fa1f3ed124ab83b1667be95f855952e68729eae54a9f511c8c8cb993", - "wtxid": "16ab0b31f680e5bd4f149527148b542e16de96ce2d14db9c41552752f3d8e655", + "confirmations": 0, + "trusted": false, + "txid": "a9e38fee226e3a598d035afdbecd99c5cb0a6039866cc29fd15d7b27c7d8dcff", + "wtxid": "701989d4f18951ae757409ea948e4a9bc3de9bf37dd14a4dcd21ba5355df2401", "walletconflicts": [ ], - "time": 1700670571, - "timereceived": 1700670571, - "bip125-replaceable": "no" + "time": 1707745877, + "timereceived": 1707745877, + "bip125-replaceable": "yes" } ] ``` @@ -149,25 +174,30 @@ bitcoin-cli -rpcwallet=recovery listtransactions ### Send funds to a different address Once those funds have been recovered and the refund delay has expired (the `confirmations` field of the previous command exceeds `25920`), you can send them to your normal on-chain wallet. -Compute the total amount received (in our example, 1.5 BTC), choose the address to send to (for example, `bcrt1q9ez7rt33wynwpah582lnqlj3u0tpzsrkj2flas`) and create a transaction using all of the received funds: +For now, this process involves selecting the inputs that you want to spend and creating the spending transaction manually, as documented below, but future versions of Bitcoin Core will probably make this easier. + +For example, if `listtransactions` lists a UTXO `5e9d2a387572fe0c8a4996c2f34373b3fbbdb19ff106b84fc91c2450eb27cbe7:0` of `0.002` Bitcoin, this is how you would send it to your on-chain address. -```sh -bitcoin-cli -rpcwallet=recovery walletcreatefundedpsbt '[{"txid":"4c3236b1fa1f3ed124ab83b1667be95f855952e68729eae54a9f511c8c8cb993", "vout":0, "sequence":144}]' '[{"bcrt1qzy4h8dux6pjl8ys979632uynqffd53vjkzffjl":0.09}]' +```shell +bitcoin-cli -rpcwallet=recovery -named walletcreatefundedpsbt inputs='[{"txid":"5e9d2a387572fe0c8a4996c2f34373b3fbbdb19ff106b84fc91c2450eb27cbe7", "vout":0, "sequence":2590}]' outputs='[{"bcrt1q9qt02fkc2rfpm3w37uvec62kd7yh688uyf8v4w":0.002}]' subtractFeeFromOutputs='[0]' { - "psbt": "cHNidP8BAHECAAAAAZO5jIwcUZ9K5eoph+ZSWYVf6XtmsYOrJNE+H/qxNjJMAAAAAACQAAAAAkBUiQAAAAAAFgAUEStzt4bQZfOSBfF1FXCTAlLaRZIEOA8AAAAAABYAFEx1yJgBL6kfpf2sybIL0WajM0rXAAAAAAABASuAlpgAAAAAACJRIBC8PjbhAQmhNnxw0RaGeV3xZIp431XBmRfmNKaKpRZmIhXBH8VZ2clsWVOJXTFQ5k6/PdaWoLCOdYZQtI/2JR1+YNEnIG0cAcgg4JAO3Y5ZtLOD5zp/WFAHJFWAT5z/6Z+k+FQbrQKQALLAIRYfxVnZyWxZU4ldMVDmTr891pagsI51hlC0j/YlHX5g0QUA0EkObyEWbRwByCDgkA7djlm0s4PnOn9YUAckVYBPnP/pn6T4VBspAWR9Jdbf5zHI25Gs69RTMJILBCLUX82cmJj59Bk4SZKgTHUGgQAAAAABFyAfxVnZyWxZU4ldMVDmTr891pagsI51hlC0j/YlHX5g0QEYIGR9Jdbf5zHI25Gs69RTMJILBCLUX82cmJj59Bk4SZKgAAAiAgMhzD3XSvW4p+oRyBAvB6rUHaOCIyjVxJV9tEin3sUiqxjpcZ0vVAAAgAEAAIAAAACAAQAAAAEAAAAA", - "fee": 0.00002620, - "changepos": 1 + "psbt": "cHNidP8BAFICAAAAAefLJ+tQJBzJT7gG8Z+xvfuzc0PzwpZJigz+cnU4Kp1eAAAAAAAeCgAAAXAFAwAAAAAAFgAUKBb1JthQ0h3F0fcZnGlWb4l9HPwAAAAAAAEBK0ANAwAAAAAAIlEgRsEcQhkAfS6VDjLeJZ2NJRqKVgaibPLHI6oN28AfRBwiFcAfxVnZyWxZU4ldMVDmTr891pagsI51hlC0j/YlHX5g0Scg6oyRJt9CuIzhFceFlxIolSJ/PTrrUxnVV4zHUivjR7WtAh4KssAhFh/FWdnJbFlTiV0xUOZOvz3WlqCwjnWGULSP9iUdfmDRBQDQSQ5vIRbqjJEm30K4jOEVx4WXEiiVIn89OutTGdVXjMdSK+NHtSkBorv4wrBqM9DCSo9s2yigvE2CbsIJiMCn0WW9crBila72QOqbAAAAAAEXIB/FWdnJbFlTiV0xUOZOvz3WlqCwjnWGULSP9iUdfmDRARggorv4wrBqM9DCSo9s2yigvE2CbsIJiMCn0WW9crBila4AAA==", + "fee": 0.00002000, + "changepos": -1 } +``` -bitcoin-cli -rpcwallet=recovery walletprocesspsbt "cHNidP8BAHECAAAAAZO5jIwcUZ9K5eoph+ZSWYVf6XtmsYOrJNE+H/qxNjJMAAAAAACQAAAAAkBUiQAAAAAAFgAUEStzt4bQZfOSBfF1FXCTAlLaRZIEOA8AAAAAABYAFEx1yJgBL6kfpf2sybIL0WajM0rXAAAAAAABASuAlpgAAAAAACJRIBC8PjbhAQmhNnxw0RaGeV3xZIp431XBmRfmNKaKpRZmIhXBH8VZ2clsWVOJXTFQ5k6/PdaWoLCOdYZQtI/2JR1+YNEnIG0cAcgg4JAO3Y5ZtLOD5zp/WFAHJFWAT5z/6Z+k+FQbrQKQALLAIRYfxVnZyWxZU4ldMVDmTr891pagsI51hlC0j/YlHX5g0QUA0EkObyEWbRwByCDgkA7djlm0s4PnOn9YUAckVYBPnP/pn6T4VBspAWR9Jdbf5zHI25Gs69RTMJILBCLUX82cmJj59Bk4SZKgTHUGgQAAAAABFyAfxVnZyWxZU4ldMVDmTr891pagsI51hlC0j/YlHX5g0QEYIGR9Jdbf5zHI25Gs69RTMJILBCLUX82cmJj59Bk4SZKgAAAiAgMhzD3XSvW4p+oRyBAvB6rUHaOCIyjVxJV9tEin3sUiqxjpcZ0vVAAAgAEAAIAAAACAAQAAAAEAAAAA" +```shell +bitcoin-cli -rpcwallet=recovery walletprocesspsbt cHNidP8BAFICAAAAAefLJ+tQJBzJT7gG8Z+xvfuzc0PzwpZJigz+cnU4Kp1eAAAAAAAeCgAAAXAFAwAAAAAAFgAUKBb1JthQ0h3F0fcZnGlWb4l9HPwAAAAAAAEBK0ANAwAAAAAAIlEgRsEcQhkAfS6VDjLeJZ2NJRqKVgaibPLHI6oN28AfRBwiFcAfxVnZyWxZU4ldMVDmTr891pagsI51hlC0j/YlHX5g0Scg6oyRJt9CuIzhFceFlxIolSJ/PTrrUxnVV4zHUivjR7WtAh4KssAhFh/FWdnJbFlTiV0xUOZOvz3WlqCwjnWGULSP9iUdfmDRBQDQSQ5vIRbqjJEm30K4jOEVx4WXEiiVIn89OutTGdVXjMdSK+NHtSkBorv4wrBqM9DCSo9s2yigvE2CbsIJiMCn0WW9crBila72QOqbAAAAAAEXIB/FWdnJbFlTiV0xUOZOvz3WlqCwjnWGULSP9iUdfmDRARggorv4wrBqM9DCSo9s2yigvE2CbsIJiMCn0WW9crBila4AAA== { - "psbt": "cHNidP8BAHECAAAAAZO5jIwcUZ9K5eoph+ZSWYVf6XtmsYOrJNE+H/qxNjJMAAAAAACQAAAAAkBUiQAAAAAAFgAUEStzt4bQZfOSBfF1FXCTAlLaRZIEOA8AAAAAABYAFEx1yJgBL6kfpf2sybIL0WajM0rXAAAAAAABASuAlpgAAAAAACJRIBC8PjbhAQmhNnxw0RaGeV3xZIp431XBmRfmNKaKpRZmAQiLA0D59zl6TLlwXk2oCio3Ffff8dpRQmpYWs7MaY+cUk1Zfl03hzxj1vwIAHBQQbyh33PCX7JoDrlXxlo/Le86jMjQJiBtHAHIIOCQDt2OWbSzg+c6f1hQByRVgE+c/+mfpPhUG60CkACyIcEfxVnZyWxZU4ldMVDmTr891pagsI51hlC0j/YlHX5g0QAAIgIDIcw910r1uKfqEcgQLweq1B2jgiMo1cSVfbRIp97FIqsY6XGdL1QAAIABAACAAAAAgAEAAAABAAAAAA==", + "psbt": "cHNidP8BAFICAAAAAefLJ+tQJBzJT7gG8Z+xvfuzc0PzwpZJigz+cnU4Kp1eAAAAAAAeCgAAAXAFAwAAAAAAFgAUKBb1JthQ0h3F0fcZnGlWb4l9HPwAAAAAAAEBK0ANAwAAAAAAIlEgRsEcQhkAfS6VDjLeJZ2NJRqKVgaibPLHI6oN28AfRBwBCIsDQEstkcuMh1AB1Nf1XkhBUuFT6WfeWmx+7VWOaUNW1t56AFz7d+QI1v+Xz7dyQTw8YuzvdoWXajAFzyYwluHc2ysmIOqMkSbfQriM4RXHhZcSKJUifz0661MZ1VeMx1Ir40e1rQIeCrIhwB/FWdnJbFlTiV0xUOZOvz3WlqCwjnWGULSP9iUdfmDRAAA=", "complete": true, - "hex": "0200000000010193b98c8c1c519f4ae5ea2987e65259855fe97b66b183ab24d13e1ffab136324c000000000090000000024054890000000000160014112b73b786d065f39205f1751570930252da459204380f00000000001600144c75c898012fa91fa5fdacc9b20bd166a3334ad70340f9f7397a4cb9705e4da80a2a3715f7dff1da51426a585acecc698f9c524d597e5d37873c63d6fc0800705041bca1df73c25fb2680eb957c65a3f2def3a8cc8d026206d1c01c820e0900edd8e59b4b383e73a7f5850072455804f9cffe99fa4f8541bad029000b221c11fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d100000000" + "hex": "02000000000101e7cb27eb50241cc94fb806f19fb1bdfbb37343f3c296498a0cfe7275382a9d5e00000000001e0a00000170050300000000001600142816f526d850d21dc5d1f7199c69566f897d1cfc03404b2d91cb8c875001d4d7f55e484152e153e967de5a6c7eed558e694356d6de7a005cfb77e408d6ff97cfb772413c3c62ecef7685976a3005cf263096e1dcdb2b2620ea8c9126df42b88ce115c78597122895227f3d3aeb5319d5578cc7522be347b5ad021e0ab221c01fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d100000000" } - -bitcoin-cli sendrawtransaction 0200000000010193b98c8c1c519f4ae5ea2987e65259855fe97b66b183ab24d13e1ffab136324c000000000090000000024054890000000000160014112b73b786d065f39205f1751570930252da459204380f00000000001600144c75c898012fa91fa5fdacc9b20bd166a3334ad70340f9f7397a4cb9705e4da80a2a3715f7dff1da51426a585acecc698f9c524d597e5d37873c63d6fc0800705041bca1df73c25fb2680eb957c65a3f2def3a8cc8d026206d1c01c820e0900edd8e59b4b383e73a7f5850072455804f9cffe99fa4f8541bad029000b221c11fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d100000000 -09efe025805b2db8ae845a94639e5ad415756fb0d010aad54bf3f74ae71e015d +``` +```shell +bitcoin-cli sendrawtransaction 02000000000101e7cb27eb50241cc94fb806f19fb1bdfbb37343f3c296498a0cfe7275382a9d5e00000000001e0a00000170050300000000001600142816f526d850d21dc5d1f7199c69566f897d1cfc03404b2d91cb8c875001d4d7f55e484152e153e967de5a6c7eed558e694356d6de7a005cfb77e408d6ff97cfb772413c3c62ecef7685976a3005cf263096e1dcdb2b2620ea8c9126df42b88ce115c78597122895227f3d3aeb5319d5578cc7522be347b5ad021e0ab221c01fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d100000000 +16d5a43fe6260b1a5993d97d711cfb4323fb27b44c9d34c547fb1693bf1c8900 ``` Wait for that transaction to confirm, and your funds will have been successfully recovered! diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index 99ca939d9..33716dede 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt @@ -134,11 +134,19 @@ interface KeyManager { fun localServerPrivateKey(remoteNodeId: PublicKey): PrivateKey = DeterministicWallet.derivePrivateKey(localServerExtendedPrivateKey, perUserPath(remoteNodeId)).privateKey val swapInProtocol = SwapInProtocol(userPublicKey, remoteServerPublicKey, userRefundPublicKey, refundDelay) - val descriptor = swapInProtocol.descriptor(chain, userRefundExtendedPrivateKey) + + // this is a private descriptor that can be used as-is to recover swap-in funds once the refund delay has passed + // it is compatible with address rotation as long as refund keys are derived directly from userRefundExtendedPrivateKey + // README: it includes the user's master refund private key and is not safe to share !! + val privateDescriptor = SwapInProtocol.privateDescriptor(chain, userPublicKey, remoteServerPublicKey, refundDelay, userRefundExtendedPrivateKey) + + // this is the public version of the above descriptor. It can be used to monitor a user's swap-in transaction + // README: it cannot be used to derive private keys, but it can be used to derive swap-in addresses + val publicDescriptor = SwapInProtocol.publicDescriptor(chain, userPublicKey, remoteServerPublicKey, refundDelay, DeterministicWallet.publicKey(userRefundExtendedPrivateKey)) // legacy p2wsh-based swap-in protocol, with a fixed on-chain address val legacySwapInProtocol = SwapInProtocolLegacy(userPublicKey, remoteServerPublicKey, refundDelay) - val legacyDescriptor = legacySwapInProtocol.descriptor(chain, master, userExtendedPrivateKey) + val legacyDescriptor = SwapInProtocolLegacy.descriptor(chain, DeterministicWallet.publicKey(master), DeterministicWallet.publicKey(userExtendedPrivateKey), remoteServerPublicKey, refundDelay) fun signSwapInputUserLegacy(fundingTx: Transaction, index: Int, parentTxOuts: List): ByteVector64 { return legacySwapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts[fundingTx.txIn[index].outPoint.index.toInt()], userPrivateKey) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt index 64559036a..d55a2d380 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt @@ -6,7 +6,6 @@ import fr.acinq.bitcoin.crypto.musig2.Musig2 import fr.acinq.bitcoin.crypto.musig2.SecretNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.NodeParams -import fr.acinq.lightning.crypto.KeyManager /** * new swap-in protocol based on musig2 and taproot: (user key + server key) OR (user refund key + delay) @@ -18,6 +17,7 @@ import fr.acinq.lightning.crypto.KeyManager data class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val userRefundKey: PublicKey, val refundDelay: Int) { // The key path uses musig2 with the user and server keys. private val internalPublicKey = Musig2.aggregateKeys(listOf(userPublicKey, serverPublicKey)) + // The script path contains a refund script, generated from this policy: and_v(v:pk(user),older(refundDelay)). // It does not depend upon the user's or server's key, just the user's refund key and the refund delay. private val refundScript = listOf(OP_PUSHDATA(userRefundKey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) @@ -55,38 +55,30 @@ data class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: Pub return Musig2.signTaprootInput(serverPrivateKey, fundingTx, index, parentTxOuts, publicKeys, privateNonce, publicNonces, scriptTree) } - /** - * @param chain chain we're on. - * @param masterRefundKey master private key for the refund keys: we assume that there is a single level of derivation to compute the refund keys. - * @return a taproot descriptor that can be imported in bitcoin core (from version 26 on) to recover user funds once the funding delay has passed. - */ - fun descriptor(chain: NodeParams.Chain, masterRefundKey: DeterministicWallet.ExtendedPrivateKey): String { - val prefix = when (chain) { - NodeParams.Chain.Mainnet -> DeterministicWallet.xprv - else -> DeterministicWallet.tprv + companion object { + fun privateDescriptor(chain: NodeParams.Chain, userPublicKey: PublicKey, serverPublicKey: PublicKey, refundDelay: Int, masterRefundKey: DeterministicWallet.ExtendedPrivateKey): String { + val internalPubKey = Musig2.aggregateKeys(listOf(userPublicKey, serverPublicKey)) + val prefix = when (chain) { + NodeParams.Chain.Mainnet -> DeterministicWallet.xprv + else -> DeterministicWallet.tprv + } + val xpriv = DeterministicWallet.encode(masterRefundKey, prefix) + val desc = "tr(${internalPubKey.value},and_v(v:pk($xpriv/*),older($refundDelay)))" + val checksum = Descriptor.checksum(desc) + return "$desc#$checksum" } - val xpriv = DeterministicWallet.encode(masterRefundKey, prefix) - val desc = "tr(${internalPublicKey.value},and_v(v:pk($xpriv/*),older($refundDelay)))" - val checksum = Descriptor.checksum(desc) - return "$desc#$checksum" - } - /** - * @param chain chain we're on. - * @param masterRefundKey master public key for the refund keys: we assume that there is a single level of derivation to compute the refund keys. - * @return a taproot descriptor that can be imported in bitcoin core (from version 26 on) to create a watch-only wallet for your swap-in transactions. - */ - fun descriptor(chain: NodeParams.Chain, masterRefundKey: DeterministicWallet.ExtendedPublicKey): String { - // the internal pubkey is the musig2 aggregation of the user's and server's public keys: it does not depend upon the user's refund's key - val prefix = when (chain) { - NodeParams.Chain.Mainnet -> DeterministicWallet.xpub - else -> DeterministicWallet.tpub + fun publicDescriptor(chain: NodeParams.Chain, userPublicKey: PublicKey, serverPublicKey: PublicKey, refundDelay: Int, masterRefundKey: DeterministicWallet.ExtendedPublicKey): String { + val internalPubKey = Musig2.aggregateKeys(listOf(userPublicKey, serverPublicKey)) + val prefix = when (chain) { + NodeParams.Chain.Mainnet -> DeterministicWallet.xpub + else -> DeterministicWallet.tpub + } + val xpub = DeterministicWallet.encode(masterRefundKey, prefix) + val desc = "tr(${internalPubKey.value},and_v(v:pk($xpub/*),older($refundDelay)))" + val checksum = Descriptor.checksum(desc) + return "$desc#$checksum" } - val xpub = DeterministicWallet.encode(masterRefundKey, prefix) - val path = masterRefundKey.path.toString().replace('\'', 'h').removePrefix("m") - val desc = "tr(${internalPublicKey.value},and_v(v:pk($xpub$path/*),older($refundDelay)))" - val checksum = Descriptor.checksum(desc) - return "$desc#$checksum" } } @@ -126,22 +118,26 @@ data class SwapInProtocolLegacy(val userPublicKey: PublicKey, val serverPublicKe return Transactions.sign(fundingTx, index, Script.write(redeemScript), parentTxOut.amount, serverKey) } - /** - * The output script descriptor matching our legacy swap-in addresses. - * That descriptor can be imported in bitcoind to recover funds after the refund delay. - * - * @param chain chain we're on. - * @param masterRefundKey master private key for the swap-in wallet. - * @param userPrivateKey user refund private key, derived from the master private key. - * @return a p2wsh descriptor that can be imported in bitcoin core (from version 24 on) to recover user funds once the funding delay has passed. - */ - fun descriptor(chain: NodeParams.Chain, masterRefundKey: DeterministicWallet.ExtendedPrivateKey, userPrivateKey: DeterministicWallet.ExtendedPrivateKey): String { - // Since child public keys cannot be derived from a master xpub when hardened derivation is used, - // we need to provide the fingerprint of the master xpub and the hardened derivation path. - // This lets wallets that have access to the master xpriv derive the corresponding private and public keys. - val masterFingerprint = ByteVector(Crypto.hash160(DeterministicWallet.publicKey(masterRefundKey).publickeybytes).take(4).toByteArray()) - val encodedChildKey = DeterministicWallet.encode(DeterministicWallet.publicKey(userPrivateKey), testnet = chain != NodeParams.Chain.Mainnet) - val userKey = "[${masterFingerprint.toHex()}/${KeyManager.SwapInOnChainKeys.encodedSwapInUserKeyPath(chain)}]$encodedChildKey" - return "wsh(and_v(v:pk($userKey),or_d(pk(${serverPublicKey.toHex()}),older($refundDelay))))" + companion object { + /** + * The output script descriptor matching our legacy swap-in addresses. + * That descriptor can be imported in bitcoind to recover funds after the refund delay. + * + * @param chain chain we're on. + * @param masterPublicKey master public key for the swap-in wallet. + * @param userExtendedPublicKey user public key, derived from the master private key. + * @param remoteServerPublicKey server public key + * @param refundDelay refund delay + * @return a p2wsh descriptor that can be imported in bitcoin core (from version 24 on) to recover user funds once the funding delay has passed. + */ + fun descriptor(chain: NodeParams.Chain, masterPublicKey: DeterministicWallet.ExtendedPublicKey, userExtendedPublicKey: DeterministicWallet.ExtendedPublicKey, remoteServerPublicKey: PublicKey, refundDelay: Int): String { + // Since child public keys cannot be derived from a master xpub when hardened derivation is used, + // we need to provide the fingerprint of the master xpub and the hardened derivation path. + // This lets wallets that have access to the master xpriv derive the corresponding private and public keys. + val masterFingerprint = ByteVector(Crypto.hash160(masterPublicKey.publickeybytes).take(4).toByteArray()) + val encodedChildKey = DeterministicWallet.encode(userExtendedPublicKey, testnet = chain != NodeParams.Chain.Mainnet) + val userKey = "[${masterFingerprint.toHex()}/${userExtendedPublicKey.path.asString('h').removePrefix("m/")}]$encodedChildKey" + return "wsh(and_v(v:pk($userKey),or_d(pk(${remoteServerPublicKey.toHex()}),older($refundDelay))))" + } } } \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt index 1dc50342e..058dff5df 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt @@ -8,6 +8,7 @@ import fr.acinq.lightning.blockchain.fee.FeeratePerByte import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite +import fr.acinq.lightning.transactions.SwapInProtocol import fr.acinq.lightning.utils.toByteVector import kotlin.test.Test import kotlin.test.assertEquals @@ -193,7 +194,8 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { @Test fun `spend swap-in transactions`() { - val swapInTx = Transaction(version = 2, + val swapInTx = Transaction( + version = 2, txIn = listOf(), txOut = listOf( TxOut(Satoshi(100000), TestConstants.Alice.keyManager.swapInOnChainWallet.legacySwapInProtocol.pubkeyScript), @@ -203,12 +205,32 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { TxOut(Satoshi(150000), TestConstants.Alice.keyManager.swapInOnChainWallet.swapInProtocol.pubkeyScript), TxOut(Satoshi(150000), Script.pay2wpkh(randomKey().publicKey())) ), - lockTime = 0) + lockTime = 0 + ) val recoveryTx = TestConstants.Alice.keyManager.swapInOnChainWallet.createRecoveryTransaction(swapInTx, TestConstants.Alice.keyManager.finalOnChainWallet.address(0), FeeratePerKw(FeeratePerByte(Satoshi(5))))!! assertEquals(4, recoveryTx.txIn.size) Transaction.correctlySpends(recoveryTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + @Test + fun `compute descriptors to recover swap-in funds`() { + val seed = MnemonicCode.toSeed("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", "") + val master = DeterministicWallet.generate(seed) + val chain = NodeParams.Chain.Regtest + val userPublicKey = PrivateKey.fromHex("0101010101010101010101010101010101010101010101010101010101010101").publicKey() + val remoteServerPublicKey = PrivateKey.fromHex("0202020202020202020202020202020202020202020202020202020202020202").publicKey() + val userRefundExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, KeyManager.SwapInOnChainKeys.swapInUserRefundKeyPath(chain)) + val refundDelay = 2590 + assertEquals( + "tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(tprv8hWm2EfcAbMerYoXeHA9w6faUqXdiQeWfSxxWpzh3Yc1FAjB2vv1sbBNY1dX3HraotvBAEeY2hzz1X4vc3SC516K1ebBvLYrkA6LstQdbNX/*),older(2590)))#90ftphf9", + SwapInProtocol.privateDescriptor(chain, userPublicKey, remoteServerPublicKey, refundDelay, userRefundExtendedPrivateKey) + ) + assertEquals( + "tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(tpubDECoAehrJy3Kk1qKXvpkLWKh3s3ZsjqREkZjoM2zTpQQ5eywfKjc45oEi8GMq1mpWxM2kg79Lp5DzznQKGRE15btY327vgLcLbfZLrgAWrv/*),older(2590)))#xmhrglc6", + SwapInProtocol.publicDescriptor(chain, userPublicKey, remoteServerPublicKey, refundDelay, DeterministicWallet.publicKey(userRefundExtendedPrivateKey)) + ) + } + companion object { val dummyExtendedPubkey = DeterministicWallet.publicKey(DeterministicWallet.generate(ByteVector("deadbeef"))) }