diff --git a/RECOVERY.md b/RECOVERY.md index 081a05fe0..f1dbdcdd8 100644 --- a/RECOVERY.md +++ b/RECOVERY.md @@ -1,6 +1,8 @@ # Funds recovery -The following steps lets you recover on-chain funds managed by `lightning-kmp`. +: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 let you recover on-chain funds managed by `lightning-kmp`. ## Closed channels @@ -19,77 +21,90 @@ For example, when using [electrum](https://electrum.org/): When swapping funds to a `lightning-kmp` wallet, the following steps are performed: -- funds are sent to a swap-in address via a swap transaction +- funds are sent to a swap-in address via a swap transaction. - we wait for that transaction to have enough confirmations - then, if the fees don't exceed the user's liquidity policy, these funds are moved into a lightning channel +We use musig2 to aggregate user keys (user being the wallet) and server keys (server being the LSP: the ACINQ node): swap-in addresses are standard p2tr addresses, and +swap-in transactions to your wallet are indistinguishable from other p2tr transactions. + The swap transaction's output can be spent using either: -1. A signature from the user's wallet and a signature from the remote node +1. An aggregated musig2 signature built from a partial signature from the user's wallet and a partial signature from the remote node 2. A signature from the user's wallet after a refund delay Funds can be recovered using the second option and [Bitcoin Core](https://github.com/bitcoin/bitcoin). -This process needs at least Bitcoin Core 25.0. - -This process will become simpler once popular on-chain wallets (such as [electrum](https://electrum.org/)) add supports for output script descriptors. - -### Extract master keys +This process needs at least Bitcoin Core 26.0. -We don't directly export your extended master private key for security reasons, so you will need to manually insert it in the descriptor. -You can obtain your extended master private key in [electrum](https://electrum.org/). After restoring your seed, type `wallet.keystore.xprv` in the console to obtain your master `xprv`. +This process will become simpler once popular on-chain wallets (such as [electrum](https://electrum.org/)) add support for output script descriptors. ### Create recovery wallet -Create a wallet to recover your funds using the following command: +#### Compute your refund master private key -```sh -bitcoin-cli createwallet recovery -``` +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/). + +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 -### Import descriptor into the recovery wallet +#### Create your refund wallet descriptor -`lightning-kmp` provides the public descriptor for your swap-in address, which uses the following template: +Copy the descriptor from the `SWAP_IN WALLET` section in the `Wallet Info` menu on your Phoenix wallet. It should look like this: ```txt -wsh(and_v(v:pk([/]),or_d(pk(),older()))) +tr(,and_v(v:pk(/),older())) ``` -For example, it will look like this: - +For example: ```txt -wsh(and_v(v:pk([14620948/51h/0h/0h]tpubDCvYeHUZisCMV3h1zPevPWQmNPfA3g3vnu7gDqskXVCbJB1VKk2F7LApV6TTdm1sCyGout8ma27CCHvYTuMZxpwrcHnLwL4kaXW8z2KfFcW),or_d(pk(0256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fd),older(25920)))) +tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(xpub6EE2N7jrues5kfjrsyFA5f7hknixqqAEKs8vyMN4QW9vDmYnChzpeBPkBYduBobbe4miQ34xHG4Jpwuq5bHXLZY1xixoGynW31ySUqqVvcU/*),older(2590)))#sv8ug44m ``` -Replace the `extended_public_key` and the `derivation_path` with the extended private key obtained in the [first step](#extract-master-keys). -In our example, the extended private key matching our seed is `tprv8ZgxMBicQKsPdKRFLVct6VDpfmCxk6aC7iAF8tb6roQ7hv1zFCyGwDLBUUxMVJ95dTiQS5VvCbQ6J7CcGqguw5SbnDpNjbjpfVwcMwUtmjS`, so we create the following private descriptor: +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 -wsh(and_v(v:pk(tprv8ZgxMBicQKsPdKRFLVct6VDpfmCxk6aC7iAF8tb6roQ7hv1zFCyGwDLBUUxMVJ95dTiQS5VvCbQ6J7CcGqguw5SbnDpNjbjpfVwcMwUtmjS/51h/0h/0h),or_d(pk(0256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fd),older(25920)))) +tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(xprvA1EfxcCy5HJnYBfPmwi9iXAyCktUSNSNxeDLAxxSrAcwLyDdfAga6P5GLHNdq7EiXe8Pzu6Py6xGwT7UTkw824FYf3v6fbRStvYsWqFTu29/*),older(2590)))#sv8ug44m ``` -We need to obtain a checksum for this descriptor, which is provided by Bitcoin Core: +### Create a bitcoin core recovery wallet -```sh -bitcoin-cli getdescriptorinfo "wsh(and_v(v:pk(tprv8ZgxMBicQKsPdKRFLVct6VDpfmCxk6aC7iAF8tb6roQ7hv1zFCyGwDLBUUxMVJ95dTiQS5VvCbQ6J7CcGqguw5SbnDpNjbjpfVwcMwUtmjS/51h/0h/0h),or_d(pk(0256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fd),older(25920))))" +Create a wallet to recover your funds using the following command: -{ - "descriptor": "wsh(and_v(v:pk(tpubD6NzVbkrYhZ4WnT3E9HUVtswEnituRm6h1m2RQdQH5CWYQGksbns7hx3ediWHpFEkEQC4vPssnQN2gQpzkodRDuMA7nQtWiQ5EDzkGpGVNw/51'/0'/0'),or_d(pk(0256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fd),older(25920))))#m8v4e6vu", - "checksum": "dlcgkrnc", - "isrange": false, - "issolvable": true, - "hasprivatekeys": true -} +```shell +bitcoin-cli -named createwallet wallet_name=recovery ``` -We can the append this checksum to our private descriptor and import it into our recovery wallet: +### Import your descriptor into the recovery wallet + +We can import our private descriptor into our recovery wallet. Since you replaced your 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": "wsh(and_v(v:pk(tprv8ZgxMBicQKsPdKRFLVct6VDpfmCxk6aC7iAF8tb6roQ7hv1zFCyGwDLBUUxMVJ95dTiQS5VvCbQ6J7CcGqguw5SbnDpNjbjpfVwcMwUtmjS/51h/0h/0h),or_d(pk(0256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fd),older(25920))))#dlcgkrnc", "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" ] } @@ -101,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, @@ -119,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": "bcrt1qw78cdcsn55vwsvmwe9qgwnx0fwffzqej7keuqfjnwj5xm0f5u6js2hp66f", + "address": "bc1p6pxx4mp43xkac222jmfy958gpxqn7duku6cka9ahdfmdp9aak74sza58es", "parent_descs": [ - "wsh(and_v(v:pk(tpubD6NzVbkrYhZ4WnT3E9HUVtswEnituRm6h1m2RQdQH5CWYQGksbns7hx3ediWHpFEkEQC4vPssnQN2gQpzkodRDuMA7nQtWiQ5EDzkGpGVNw/51'/0'/0'),or_d(pk(0256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fd),older(25920))))#m8v4e6vu" + "tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(xpub6EE2N7jrues5kfjrsyFA5f7hknixqqAEKs8vyMN4QW9vDmYnChzpeBPkBYduBobbe4miQ34xHG4Jpwuq5bHXLZY1xixoGynW31ySUqqVvcU/*),older(2590)))#sv8ug44m" ], "category": "receive", - "amount": 1.50000000, - "label": "", + "amount": 0.00003000, "vout": 1, - "confirmations": 5, - "blockhash": "6e1048a8d7829d36a766188b499ddcc2e497193427678d115fd341b2b452c0bd", - "blockheight": 151, - "blockindex": 1, - "blocktime": 1687759025, - "txid": "d9940b7eb709ff8eaec307bdd6d20633e30a6eb1627d9ef8c8e03dfd28298c75", - "wtxid": "261492e5f930b82f65f269bb3006db9c3ef14423e5f52f2a185ace18704bb7b0", + "abandoned": false, + "confirmations": 0, + "trusted": false, + "txid": "a9e38fee226e3a598d035afdbecd99c5cb0a6039866cc29fd15d7b27c7d8dcff", + "wtxid": "701989d4f18951ae757409ea948e4a9bc3de9bf37dd14a4dcd21ba5355df2401", "walletconflicts": [ ], - "time": 1687759025, - "timereceived": 1687759181, - "bip125-replaceable": "no" + "time": 1707745877, + "timereceived": 1707745877, + "bip125-replaceable": "yes" } ] ``` @@ -157,32 +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. -```sh -bitcoin-cli -rpcwallet=recovery walletcreatefundedpsbt '[{"txid":"d9940b7eb709ff8eaec307bdd6d20633e30a6eb1627d9ef8c8e03dfd28298c75","vout":1,"sequence":25920}]' '[{"bcrt1q9ez7rt33wynwpah582lnqlj3u0tpzsrkj2flas":1.5}]' 0 '{"subtractFeeFromOutputs":[0]}' +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. +```shell +bitcoin-cli -rpcwallet=recovery -named walletcreatefundedpsbt inputs='[{"txid":"5e9d2a387572fe0c8a4996c2f34373b3fbbdb19ff106b84fc91c2450eb27cbe7", "vout":0, "sequence":2590}]' outputs='[{"bcrt1q9qt02fkc2rfpm3w37uvec62kd7yh688uyf8v4w":0.002}]' subtractFeeFromOutputs='[0]' { - "psbt": "cHNidP8BAFICAAAAAXWMKSj9PeDI+J59YrFuCuMzBtLWvQfDro7/Cbd+C5TZAQAAAABAZQAAAQzI8AgAAAAAFgAULkXhrjFxJuD29Dq/MH5R49YRQHYAAAAAAAEAiQIAAAABsDXoV21bcbM8ii+Nyo4r8ZWmMEJIiaYqYg6pKaXiOiMAAAAAAP3///8CnBMVIQEAAAAiUSA520UqAgN8jz9APGIBbNHksiweuAEnMZvjgpMKiRUKkoDR8AgAAAAAIgAgd4+G4hOlGOgzbslAh0zPS5KRAzL1s8AmU3Sobb005qWWAAAAAQErgNHwCAAAAAAiACB3j4biE6UY6DNuyUCHTM9LkpEDMvWzwCZTdKhtvTTmpQEFTSED1ZiIPcIwgcWSbaso29B11ULE+6VERxkh27lqMde8SRmtIQJW6UgYDzPwZyRnEKQWVghPwkW5ftoIHv4eSIshV31g/axzZAJAZbJoIgYCVulIGA8z8GckZxCkFlYIT8JFuX7aCB7+HkiLIVd9YP0EsxuziiIGA9WYiD3CMIHFkm2rKNvQddVCxPulREcZIdu5ajHXvEkZEBRiCUgzAACAAAAAgAAAAIAAAA==", - "fee": 0.00002420, + "psbt": "cHNidP8BAFICAAAAAefLJ+tQJBzJT7gG8Z+xvfuzc0PzwpZJigz+cnU4Kp1eAAAAAAAeCgAAAXAFAwAAAAAAFgAUKBb1JthQ0h3F0fcZnGlWb4l9HPwAAAAAAAEBK0ANAwAAAAAAIlEgRsEcQhkAfS6VDjLeJZ2NJRqKVgaibPLHI6oN28AfRBwiFcAfxVnZyWxZU4ldMVDmTr891pagsI51hlC0j/YlHX5g0Scg6oyRJt9CuIzhFceFlxIolSJ/PTrrUxnVV4zHUivjR7WtAh4KssAhFh/FWdnJbFlTiV0xUOZOvz3WlqCwjnWGULSP9iUdfmDRBQDQSQ5vIRbqjJEm30K4jOEVx4WXEiiVIn89OutTGdVXjMdSK+NHtSkBorv4wrBqM9DCSo9s2yigvE2CbsIJiMCn0WW9crBila72QOqbAAAAAAEXIB/FWdnJbFlTiV0xUOZOvz3WlqCwjnWGULSP9iUdfmDRARggorv4wrBqM9DCSo9s2yigvE2CbsIJiMCn0WW9crBila4AAA==", + "fee": 0.00002000, "changepos": -1 } +``` -bitcoin-cli -rpcwallet=recovery walletprocesspsbt "cHNidP8BAFICAAAAAXWMKSj9PeDI+J59YrFuCuMzBtLWvQfDro7/Cbd+C5TZAQAAAABAZQAAAQzI8AgAAAAAFgAULkXhrjFxJuD29Dq/MH5R49YRQHYAAAAAAAEAiQIAAAABsDXoV21bcbM8ii+Nyo4r8ZWmMEJIiaYqYg6pKaXiOiMAAAAAAP3///8CnBMVIQEAAAAiUSA520UqAgN8jz9APGIBbNHksiweuAEnMZvjgpMKiRUKkoDR8AgAAAAAIgAgd4+G4hOlGOgzbslAh0zPS5KRAzL1s8AmU3Sobb005qWWAAAAAQErgNHwCAAAAAAiACB3j4biE6UY6DNuyUCHTM9LkpEDMvWzwCZTdKhtvTTmpQEFTSED1ZiIPcIwgcWSbaso29B11ULE+6VERxkh27lqMde8SRmtIQJW6UgYDzPwZyRnEKQWVghPwkW5ftoIHv4eSIshV31g/axzZAJAZbJoIgYCVulIGA8z8GckZxCkFlYIT8JFuX7aCB7+HkiLIVd9YP0EsxuziiIGA9WYiD3CMIHFkm2rKNvQddVCxPulREcZIdu5ajHXvEkZEBRiCUgzAACAAAAAgAAAAIAAAA==" - -{ - "psbt": "cHNidP8BAFICAAAAAXWMKSj9PeDI+J59YrFuCuMzBtLWvQfDro7/Cbd+C5TZAQAAAABAZQAAAQzI8AgAAAAAFgAULkXhrjFxJuD29Dq/MH5R49YRQHYAAAAAAAEAiQIAAAABsDXoV21bcbM8ii+Nyo4r8ZWmMEJIiaYqYg6pKaXiOiMAAAAAAP3///8CnBMVIQEAAAAiUSA520UqAgN8jz9APGIBbNHksiweuAEnMZvjgpMKiRUKkoDR8AgAAAAAIgAgd4+G4hOlGOgzbslAh0zPS5KRAzL1s8AmU3Sobb005qWWAAAAAQErgNHwCAAAAAAiACB3j4biE6UY6DNuyUCHTM9LkpEDMvWzwCZTdKhtvTTmpQEImAMARzBEAiBNe5Y/fGWNfCIh2oBoZZHh5Em1kR3GFumpa0bgn9WRCQIgTDKGl/F59wpGRhdJ/jLlOTHqszmHonQTD4qgVNNJIc4BTSED1ZiIPcIwgcWSbaso29B11ULE+6VERxkh27lqMde8SRmtIQJW6UgYDzPwZyRnEKQWVghPwkW5ftoIHv4eSIshV31g/axzZAJAZbJoAAA=", - "complete": true -} - -bitcoin-cli -rpcwallet=recovery finalizepsbt "cHNidP8BAFICAAAAAXWMKSj9PeDI+J59YrFuCuMzBtLWvQfDro7/Cbd+C5TZAQAAAABAZQAAAQzI8AgAAAAAFgAULkXhrjFxJuD29Dq/MH5R49YRQHYAAAAAAAEAiQIAAAABsDXoV21bcbM8ii+Nyo4r8ZWmMEJIiaYqYg6pKaXiOiMAAAAAAP3///8CnBMVIQEAAAAiUSA520UqAgN8jz9APGIBbNHksiweuAEnMZvjgpMKiRUKkoDR8AgAAAAAIgAgd4+G4hOlGOgzbslAh0zPS5KRAzL1s8AmU3Sobb005qWWAAAAAQErgNHwCAAAAAAiACB3j4biE6UY6DNuyUCHTM9LkpEDMvWzwCZTdKhtvTTmpQEImAMARzBEAiBNe5Y/fGWNfCIh2oBoZZHh5Em1kR3GFumpa0bgn9WRCQIgTDKGl/F59wpGRhdJ/jLlOTHqszmHonQTD4qgVNNJIc4BTSED1ZiIPcIwgcWSbaso29B11ULE+6VERxkh27lqMde8SRmtIQJW6UgYDzPwZyRnEKQWVghPwkW5ftoIHv4eSIshV31g/axzZAJAZbJoAAA=" - +```shell +bitcoin-cli -rpcwallet=recovery walletprocesspsbt cHNidP8BAFICAAAAAefLJ+tQJBzJT7gG8Z+xvfuzc0PzwpZJigz+cnU4Kp1eAAAAAAAeCgAAAXAFAwAAAAAAFgAUKBb1JthQ0h3F0fcZnGlWb4l9HPwAAAAAAAEBK0ANAwAAAAAAIlEgRsEcQhkAfS6VDjLeJZ2NJRqKVgaibPLHI6oN28AfRBwiFcAfxVnZyWxZU4ldMVDmTr891pagsI51hlC0j/YlHX5g0Scg6oyRJt9CuIzhFceFlxIolSJ/PTrrUxnVV4zHUivjR7WtAh4KssAhFh/FWdnJbFlTiV0xUOZOvz3WlqCwjnWGULSP9iUdfmDRBQDQSQ5vIRbqjJEm30K4jOEVx4WXEiiVIn89OutTGdVXjMdSK+NHtSkBorv4wrBqM9DCSo9s2yigvE2CbsIJiMCn0WW9crBila72QOqbAAAAAAEXIB/FWdnJbFlTiV0xUOZOvz3WlqCwjnWGULSP9iUdfmDRARggorv4wrBqM9DCSo9s2yigvE2CbsIJiMCn0WW9crBila4AAA== { - "hex": "02000000000101758c2928fd3de0c8f89e7d62b16e0ae33306d2d6bd07c3ae8eff09b77e0b94d9010000000040650000010cc8f008000000001600142e45e1ae317126e0f6f43abf307e51e3d6114076030047304402204d7b963f7c658d7c2221da80686591e1e449b5911dc616e9a96b46e09fd5910902204c328697f179f70a46461749fe32e53931eab33987a274130f8aa054d34921ce014d2103d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc4919ad210256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdac7364024065b26800000000", - "complete": true + "psbt": "cHNidP8BAFICAAAAAefLJ+tQJBzJT7gG8Z+xvfuzc0PzwpZJigz+cnU4Kp1eAAAAAAAeCgAAAXAFAwAAAAAAFgAUKBb1JthQ0h3F0fcZnGlWb4l9HPwAAAAAAAEBK0ANAwAAAAAAIlEgRsEcQhkAfS6VDjLeJZ2NJRqKVgaibPLHI6oN28AfRBwBCIsDQEstkcuMh1AB1Nf1XkhBUuFT6WfeWmx+7VWOaUNW1t56AFz7d+QI1v+Xz7dyQTw8YuzvdoWXajAFzyYwluHc2ysmIOqMkSbfQriM4RXHhZcSKJUifz0661MZ1VeMx1Ir40e1rQIeCrIhwB/FWdnJbFlTiV0xUOZOvz3WlqCwjnWGULSP9iUdfmDRAAA=", + "complete": true, + "hex": "02000000000101e7cb27eb50241cc94fb806f19fb1bdfbb37343f3c296498a0cfe7275382a9d5e00000000001e0a00000170050300000000001600142816f526d850d21dc5d1f7199c69566f897d1cfc03404b2d91cb8c875001d4d7f55e484152e153e967de5a6c7eed558e694356d6de7a005cfb77e408d6ff97cfb772413c3c62ecef7685976a3005cf263096e1dcdb2b2620ea8c9126df42b88ce115c78597122895227f3d3aeb5319d5578cc7522be347b5ad021e0ab221c01fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d100000000" } - -bitcoin-cli -rpcwallet=recovery sendrawtransaction 02000000000101758c2928fd3de0c8f89e7d62b16e0ae33306d2d6bd07c3ae8eff09b77e0b94d9010000000040650000010cc8f008000000001600142e45e1ae317126e0f6f43abf307e51e3d6114076030047304402204d7b963f7c658d7c2221da80686591e1e449b5911dc616e9a96b46e09fd5910902204c328697f179f70a46461749fe32e53931eab33987a274130f8aa054d34921ce014d2103d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc4919ad210256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdac7364024065b26800000000 +``` +```shell +bitcoin-cli sendrawtransaction 02000000000101e7cb27eb50241cc94fb806f19fb1bdfbb37343f3c296498a0cfe7275382a9d5e00000000001e0a00000170050300000000001600142816f526d850d21dc5d1f7199c69566f897d1cfc03404b2d91cb8c875001d4d7f55e484152e153e967de5a6c7eed558e694356d6de7a005cfb77e408d6ff97cfb772413c3c62ecef7685976a3005cf263096e1dcdb2b2620ea8c9126df42b88ce115c78597122895227f3d3aeb5319d5578cc7522be347b5ad021e0ab221c01fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d100000000 +16d5a43fe6260b1a5993d97d711cfb4323fb27b44c9d34c547fb1693bf1c8900 ``` Wait for that transaction to confirm, and your funds will have been successfully recovered! diff --git a/build.gradle.kts b/build.gradle.kts index 3d71d7d51..e97a8ea50 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,8 +27,8 @@ val currentOs = org.gradle.internal.os.OperatingSystem.current() kotlin { - val bitcoinKmpVersion = "0.16.0" // when upgrading bitcoin-kmp, keep secpJniJvmVersion in sync! - val secpJniJvmVersion = "0.13.0" + val bitcoinKmpVersion = "0.17.0" // when upgrading bitcoin-kmp, keep secpJniJvmVersion in sync! + val secpJniJvmVersion = "0.14.0" val serializationVersion = "1.6.2" val coroutineVersion = "1.7.3" @@ -138,6 +138,11 @@ kotlin { languageSettings.optIn("kotlin.ExperimentalStdlibApi") } } + + configurations.all { + // do not cache changing (i.e. SNAPSHOT) dependencies + resolutionStrategy.cacheChangingModulesFor(0, TimeUnit.SECONDS) + } targets.all { compilations.all { diff --git a/publishing/lightning-kmp-snapshot-deploy.sh b/publishing/lightning-kmp-snapshot-deploy.sh index 5baafa725..6a0ba9577 100755 --- a/publishing/lightning-kmp-snapshot-deploy.sh +++ b/publishing/lightning-kmp-snapshot-deploy.sh @@ -21,9 +21,9 @@ mvn deploy:deploy-file -DrepositoryId=ossrh -Durl=https://oss.sonatype.org/conte -Djavadoc=$ARTIFACT_ID_BASE-$VERSION-javadoc.jar popd pushd . -for i in iosarm64 iosx64 jvm linuxx64; do +for i in iosarm64 iossimulatorarm64 iosx64 jvm linuxx64; do cd fr/acinq/lightning/lightning-kmp-$i/$VERSION - if [ $i == iosarm64 ] || [ $i == iosx64 ]; then + if [ $i == iosarm64 ] || [ $i == iossimulatorarm64 ] || [ $i == iosx64 ]; then mvn deploy:deploy-file -DrepositoryId=ossrh -Durl=https://oss.sonatype.org/content/repositories/snapshots/ \ -DpomFile=$ARTIFACT_ID_BASE-$i-$VERSION.pom \ -Dfile=$ARTIFACT_ID_BASE-$i-$VERSION.klib \ diff --git a/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWallet.kt b/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWallet.kt index e2dada15c..bb5efe148 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWallet.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWallet.kt @@ -167,17 +167,9 @@ class ElectrumMiniWallet( } fun computeScriptHash(bitcoinAddress: String): ByteVector32? { - return when (val result = Bitcoin.addressToPublicKeyScript(chainHash, bitcoinAddress)) { - is AddressToPublicKeyScriptResult.Failure -> { - logger.error { "cannot subscribe to $bitcoinAddress ($result)" } - null - } - - is AddressToPublicKeyScriptResult.Success -> { - val pubkeyScript = ByteVector(Script.write(result.script)) - return ElectrumClient.computeScriptHash(pubkeyScript) - } - } + return Bitcoin.addressToPublicKeyScript(chainHash, bitcoinAddress) + .map { ElectrumClient.computeScriptHash(Script.write(it).byteVector()) } + .right } job = launch { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index fe4f5a652..2df046406 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -2,6 +2,11 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* import fr.acinq.bitcoin.Script.tail +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce +import fr.acinq.bitcoin.crypto.musig2.Musig2 +import fr.acinq.bitcoin.crypto.musig2.SecretNonce +import fr.acinq.bitcoin.utils.getOrDefault +import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.electrum.WalletState import fr.acinq.lightning.blockchain.fee.FeeratePerKw @@ -11,7 +16,9 @@ import fr.acinq.lightning.logging.* import fr.acinq.lightning.transactions.CommitmentSpec import fr.acinq.lightning.transactions.DirectedHtlc import fr.acinq.lightning.transactions.Scripts +import fr.acinq.lightning.transactions.SwapInProtocol import fr.acinq.lightning.transactions.Transactions +import fr.acinq.lightning.transactions.* import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.* import kotlinx.coroutines.CompletableDeferred @@ -97,6 +104,7 @@ sealed class InteractiveTxInput { abstract val serialId: Long abstract val outPoint: OutPoint abstract val sequence: UInt + abstract val txOut: TxOut sealed interface Outgoing sealed interface Incoming @@ -104,37 +112,80 @@ sealed class InteractiveTxInput { sealed class Local : InteractiveTxInput(), Outgoing { abstract val previousTx: Transaction abstract val previousTxOutput: Long - abstract val txOut: TxOut + override val outPoint: OutPoint get() = OutPoint(previousTx, previousTxOutput) + override val txOut: TxOut get() = previousTx.txOut[previousTxOutput.toInt()] } /** A local-only input that funds the interactive transaction. */ - data class LocalOnly(override val serialId: Long, override val previousTx: Transaction, override val previousTxOutput: Long, override val sequence: UInt) : Local() { - override val outPoint: OutPoint = OutPoint(previousTx, previousTxOutput) - override val txOut: TxOut = previousTx.txOut[previousTxOutput.toInt()] - } + data class LocalOnly(override val serialId: Long, override val previousTx: Transaction, override val previousTxOutput: Long, override val sequence: UInt) : Local() + + /** A local input that funds the interactive transaction, coming from a 2-of-2 legacy (pay2wsh) swap-in transaction. */ + data class LocalLegacySwapIn( + override val serialId: Long, + override val previousTx: Transaction, + override val previousTxOutput: Long, + override val sequence: UInt, + val userKey: PublicKey, + val serverKey: PublicKey, + val refundDelay: Int + ) : Local() /** A local input that funds the interactive transaction, coming from a 2-of-2 swap-in transaction. */ - data class LocalSwapIn(override val serialId: Long, override val previousTx: Transaction, override val previousTxOutput: Long, override val sequence: UInt, val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : Local() { - override val outPoint: OutPoint = OutPoint(previousTx, previousTxOutput) - override val txOut: TxOut = previousTx.txOut[previousTxOutput.toInt()] - } + data class LocalSwapIn( + override val serialId: Long, + override val previousTx: Transaction, + override val previousTxOutput: Long, + override val sequence: UInt, + val userKey: PublicKey, + val serverKey: PublicKey, + val userRefundKey: PublicKey, + val refundDelay: Int + ) : Local() /** * A remote input that funds the interactive transaction. * We only keep the data we need from our peer's TxAddInput to avoid storing potentially large messages in our DB. */ - sealed class Remote : InteractiveTxInput(), Incoming { - abstract val txOut: TxOut - } + sealed class Remote : InteractiveTxInput(), Incoming /** A remote-only input that funds the interactive transaction. */ data class RemoteOnly(override val serialId: Long, override val outPoint: OutPoint, override val txOut: TxOut, override val sequence: UInt) : Remote() + /** A remote input from a legacy (pay2wsh) swap-in: our peer needs our signature to build a witness for that input. */ + data class RemoteLegacySwapIn( + override val serialId: Long, + override val outPoint: OutPoint, + override val txOut: TxOut, + override val sequence: UInt, + val userKey: PublicKey, + val serverKey: PublicKey, + val refundDelay: Int + ) : Remote() + /** A remote input from a swap-in: our peer needs our signature to build a witness for that input. */ - data class RemoteSwapIn(override val serialId: Long, override val outPoint: OutPoint, override val txOut: TxOut, override val sequence: UInt, val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : Remote() + data class RemoteSwapIn( + override val serialId: Long, + override val outPoint: OutPoint, + override val txOut: TxOut, + override val sequence: UInt, + val userKey: PublicKey, + val serverKey: PublicKey, + val userRefundKey: PublicKey, + val refundDelay: Int + ) : Remote() /** The shared input can be added by us or by our peer, depending on who initiated the protocol. */ - data class Shared(override val serialId: Long, override val outPoint: OutPoint, override val sequence: UInt, val localAmount: MilliSatoshi, val remoteAmount: MilliSatoshi) : InteractiveTxInput(), Incoming, Outgoing + data class Shared( + override val serialId: Long, + override val outPoint: OutPoint, + val publicKeyScript: ByteVector, + override val sequence: UInt, + val localAmount: MilliSatoshi, + val remoteAmount: MilliSatoshi, + val htlcAmount: MilliSatoshi + ) : InteractiveTxInput(), Incoming, Outgoing { + override val txOut: TxOut get() = TxOut((localAmount + remoteAmount + htlcAmount).truncateToSatoshi(), publicKeyScript) + } } sealed class InteractiveTxOutput { @@ -248,8 +299,26 @@ data class FundingContributions(val inputs: List, v } } } - val sharedInput = sharedUtxo?.let { (i, balances) -> listOf(InteractiveTxInput.Shared(0, i.info.outPoint, 0xfffffffdU, balances.toLocal, balances.toRemote)) } ?: listOf() - val localInputs = walletInputs.map { i -> InteractiveTxInput.LocalSwapIn(0, i.previousTx.stripInputWitnesses(), i.outputIndex.toLong(), 0xfffffffdU, swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay) } + val sharedInput = sharedUtxo?.let { (i, balances) -> listOf(InteractiveTxInput.Shared(0, i.info.outPoint, i.info.txOut.publicKeyScript, 0xfffffffdU, balances.toLocal, balances.toRemote, balances.toHtlcs)) } ?: listOf() + val localInputs = walletInputs.map { i -> + when { + Script.isPay2wsh(i.previousTx.txOut[i.outputIndex].publicKeyScript.toByteArray()) -> + InteractiveTxInput.LocalLegacySwapIn( + 0, + i.previousTx.stripInputWitnesses(), + i.outputIndex.toLong(), + 0xfffffffdU, + swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay + ) + else -> InteractiveTxInput.LocalSwapIn( + 0, + i.previousTx.stripInputWitnesses(), + i.outputIndex.toLong(), + 0xfffffffdU, + swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.userRefundPublicKey, swapInKeys.refundDelay + ) + } + } return if (params.isInitiator) { Either.Right(sortFundingContributions(params, sharedInput + localInputs, sharedOutput + nonChangeOutputs + changeOutput)) } else { @@ -262,7 +331,7 @@ data class FundingContributions(val inputs: List, v /** Compute the weight we need to pay on-chain fees for. */ private fun computeWeightPaid(isInitiator: Boolean, sharedInput: SharedFundingInput?, sharedOutputScript: ByteVector, walletInputs: List, localOutputs: List): Int { - val walletInputsWeight = walletInputs.size * Transactions.swapInputWeight + val walletInputsWeight = weight(walletInputs) val localOutputsWeight = localOutputs.sumOf { it.weight() } return if (isInitiator) { // The initiator must add the shared input, the shared output and pay for the fees of the common transaction fields. @@ -284,12 +353,20 @@ data class FundingContributions(val inputs: List, v localOutputs ) + fun weight(walletInputs: List): Int = walletInputs.sumOf { + when { + Script.isPay2wsh(it.previousTx.txOut[it.outputIndex].publicKeyScript.toByteArray()) -> Transactions.swapInputWeightLegacy + else -> Transactions.swapInputWeight + } + } + /** We always randomize the order of inputs and outputs. */ private fun sortFundingContributions(params: InteractiveTxParams, inputs: List, outputs: List): FundingContributions { val sortedInputs = inputs.shuffled().mapIndexed { i, input -> val serialId = 2 * i.toLong() + params.serialIdParity when (input) { is InteractiveTxInput.LocalOnly -> input.copy(serialId = serialId) + is InteractiveTxInput.LocalLegacySwapIn -> input.copy(serialId = serialId) is InteractiveTxInput.LocalSwapIn -> input.copy(serialId = serialId) is InteractiveTxInput.Shared -> input.copy(serialId = serialId) } @@ -324,14 +401,18 @@ data class SharedTransaction( val remoteFees: MilliSatoshi = remoteAmountIn - remoteAmountOut val fees: Satoshi = (localFees + remoteFees).truncateToSatoshi() - fun localOnlyInputs(): List = localInputs.filterIsInstance() + // tx outputs spent by this transaction + val spentOutputs: Map = run { + val sharedOutput = sharedInput?.let { i -> mapOf(i.outPoint to i.txOut) } ?: mapOf() + val localOutputs = localInputs.associate { i -> i.outPoint to i.txOut } + val remoteOutputs = remoteInputs.associate { i -> i.outPoint to i.txOut } + sharedOutput + localOutputs + remoteOutputs + } - fun localSwapInputs(): List = localInputs.filterIsInstance() + fun localOnlyInputs(): List = localInputs.filterIsInstance() fun remoteOnlyInputs(): List = remoteInputs.filterIsInstance() - fun remoteSwapInputs(): List = remoteInputs.filterIsInstance() - fun buildUnsignedTx(): Transaction { val sharedTxIn = sharedInput?.let { i -> listOf(Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong()))) } ?: listOf() val localTxIn = localInputs.map { i -> Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong())) } @@ -344,23 +425,71 @@ data class SharedTransaction( return Transaction(2, inputs, outputs, lockTime) } - fun sign(keyManager: KeyManager, fundingParams: InteractiveTxParams, localParams: LocalParams, remoteNodeId: PublicKey): PartiallySignedSharedTransaction { + fun sign(session: InteractiveTxSession, keyManager: KeyManager, fundingParams: InteractiveTxParams, localParams: LocalParams, remoteNodeId: PublicKey): PartiallySignedSharedTransaction { val unsignedTx = buildUnsignedTx() val sharedSig = fundingParams.sharedInput?.sign(keyManager.channelKeys(localParams.fundingKeyPath), unsignedTx) + // NB: the order in this list must match the order of the transaction's inputs. + val previousOutputs = unsignedTx.txIn.map { spentOutputs[it.outPoint]!! } + + // Public nonces for all the musig2 swap-in inputs (local and remote). + // We have verified that one nonce was provided for each input when receiving `tx_complete`. + val remoteNonces: Map = when (session.txCompleteReceived) { + null -> mapOf() + else -> (localInputs.filterIsInstance() + remoteInputs.filterIsInstance()) + .sortedBy { it.serialId } + .zip(session.txCompleteReceived.publicNonces) + .associate { it.first.serialId to it.second } + } + // If we are swapping funds in, we provide our partial signatures to the corresponding inputs. - val swapUserSigs = unsignedTx.txIn.mapIndexed { i, txIn -> + val legacySwapUserSigs = unsignedTx.txIn.mapIndexed { i, txIn -> localInputs + .filterIsInstance() .find { txIn.outPoint == it.outPoint } - ?.let { input -> Transactions.signSwapInputUser(unsignedTx, i, input.txOut, keyManager.swapInOnChainWallet.userPrivateKey, keyManager.swapInOnChainWallet.remoteServerPublicKey, keyManager.swapInOnChainWallet.refundDelay) } + ?.let { input -> keyManager.swapInOnChainWallet.signSwapInputUserLegacy(unsignedTx, i, input.previousTx.txOut) } }.filterNotNull() + val swapUserPartialSigs = unsignedTx.txIn.mapIndexed { i, txIn -> + localInputs + .filterIsInstance() + .find { txIn.outPoint == it.outPoint } + ?.let { input -> + // We generate our secret nonce when sending the corresponding input, we know it exists in the map. + val userNonce = session.secretNonces[input.serialId]!! + val serverNonce = remoteNonces[input.serialId]!! + keyManager.swapInOnChainWallet.signSwapInputUser(unsignedTx, i, previousOutputs, userNonce.first, userNonce.second, serverNonce) + .map { TxSignaturesTlv.PartialSignature(it, userNonce.second, serverNonce) } + .getOrDefault(null) + } + }.filterNotNull() + // If the remote is swapping funds in, they'll need our partial signatures to finalize their witness. - val swapServerSigs = unsignedTx.txIn.mapIndexed { i, txIn -> + val legacySwapServerSigs = unsignedTx.txIn.mapIndexed { i, txIn -> + remoteInputs + .filterIsInstance() + .find { txIn.outPoint == it.outPoint } + ?.let { input -> + val serverKey = keyManager.swapInOnChainWallet.localServerPrivateKey(remoteNodeId) + val swapInProtocol = SwapInProtocolLegacy(input.userKey, serverKey.publicKey(), input.refundDelay) + swapInProtocol.signSwapInputServer(unsignedTx, i, input.txOut, serverKey) + } + }.filterNotNull() + val swapServerPartialSigs = unsignedTx.txIn.mapIndexed { i, txIn -> remoteInputs .filterIsInstance() .find { txIn.outPoint == it.outPoint } - ?.let { input -> Transactions.signSwapInputServer(unsignedTx, i, input.txOut, input.userKey, keyManager.swapInOnChainWallet.localServerPrivateKey(remoteNodeId), keyManager.swapInOnChainWallet.refundDelay) } + ?.let { input -> + val serverKey = keyManager.swapInOnChainWallet.localServerPrivateKey(remoteNodeId) + val swapInProtocol = SwapInProtocol(input.userKey, serverKey.publicKey(), input.userRefundKey, input.refundDelay) + // We generate our secret nonce when receiving the corresponding input, we know it exists in the map. + val serverNonce = session.secretNonces[input.serialId]!! + val userNonce = remoteNonces[input.serialId]!! + swapInProtocol.signSwapInputServer(unsignedTx, i, previousOutputs, serverKey, serverNonce.first, userNonce, serverNonce.second) + .map { TxSignaturesTlv.PartialSignature(it, userNonce, serverNonce.second) } + .getOrDefault(null) + } }.filterNotNull() - return PartiallySignedSharedTransaction(this, TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig, swapUserSigs, swapServerSigs)) + + return PartiallySignedSharedTransaction(this, TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig, legacySwapUserSigs, legacySwapServerSigs, swapUserPartialSigs, swapServerPartialSigs)) } } @@ -377,10 +506,13 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, override val signedTx = null fun addRemoteSigs(channelKeys: KeyManager.ChannelKeys, fundingParams: InteractiveTxParams, remoteSigs: TxSignatures): FullySignedSharedTransaction? { - if (localSigs.swapInUserSigs.size != tx.localInputs.size) return null + if (localSigs.swapInUserSigs.size != tx.localInputs.filterIsInstance().size) return null + if (localSigs.swapInUserPartialSigs.size != tx.localInputs.filterIsInstance().size) return null + if (remoteSigs.swapInUserSigs.size != tx.remoteInputs.filterIsInstance().size) return null + if (remoteSigs.swapInUserPartialSigs.size != tx.remoteInputs.filterIsInstance().size) return null + if (remoteSigs.swapInServerSigs.size != tx.localInputs.filterIsInstance().size) return null + if (remoteSigs.swapInServerPartialSigs.size != tx.localInputs.filterIsInstance().size) return null if (remoteSigs.witnesses.size != tx.remoteOnlyInputs().size) return null - if (remoteSigs.swapInUserSigs.size != tx.remoteSwapInputs().size) return null - if (remoteSigs.swapInServerSigs.size != tx.localInputs.size) return null if (remoteSigs.txId != localSigs.txId) return null val sharedSigs = fundingParams.sharedInput?.let { when (it) { @@ -393,11 +525,7 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, } } val fullySignedTx = FullySignedSharedTransaction(tx, localSigs, remoteSigs, sharedSigs) - val sharedOutput = fundingParams.sharedInput?.let { i -> mapOf(i.info.outPoint to i.info.txOut) } ?: mapOf() - val localOutputs = tx.localInputs.associate { i -> i.outPoint to i.txOut } - val remoteOutputs = tx.remoteInputs.associate { i -> i.outPoint to i.txOut } - val previousOutputs = sharedOutput + localOutputs + remoteOutputs - return when (runTrying { Transaction.correctlySpends(fullySignedTx.signedTx, previousOutputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }) { + return when (runTrying { Transaction.correctlySpends(fullySignedTx.signedTx, tx.spentOutputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }) { is Try.Success -> fullySignedTx is Try.Failure -> null } @@ -406,20 +534,39 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, data class FullySignedSharedTransaction(override val tx: SharedTransaction, override val localSigs: TxSignatures, val remoteSigs: TxSignatures, val sharedSigs: ScriptWitness?) : SignedSharedTransaction() { override val signedTx = run { + val unsignedTx = tx.buildUnsignedTx() + // NB: the order in this list must match the order of the transaction's inputs. + val previousOutputs = unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! } val sharedTxIn = tx.sharedInput?.let { i -> listOf(Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), sharedSigs ?: ScriptWitness.empty))) } ?: listOf() val localOnlyTxIn = tx.localOnlyInputs().sortedBy { i -> i.serialId }.zip(localSigs.witnesses).map { (i, w) -> Pair(i.serialId, TxIn(OutPoint(i.previousTx, i.previousTxOutput), ByteVector.empty, i.sequence.toLong(), w)) } - val localSwapTxIn = tx.localSwapInputs().sortedBy { i -> i.serialId }.zip(localSigs.swapInUserSigs.zip(remoteSigs.swapInServerSigs)).map { (i, sigs) -> + val localLegacySwapTxIn = tx.localInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(localSigs.swapInUserSigs.zip(remoteSigs.swapInServerSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs - val witness = Scripts.witnessSwapIn2of2(userSig, i.userKey, serverSig, i.serverKey, i.refundDelay) + val swapInProtocol = SwapInProtocolLegacy(i.userKey, i.serverKey, i.refundDelay) + val witness = swapInProtocol.witness(userSig, serverSig) Pair(i.serialId, TxIn(OutPoint(i.previousTx, i.previousTxOutput), ByteVector.empty, i.sequence.toLong(), witness)) } + val localSwapTxIn = tx.localInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(localSigs.swapInUserPartialSigs.zip(remoteSigs.swapInServerPartialSigs)).map { (i, sigs) -> + val (userSig, serverSig) = sigs + val swapInProtocol = SwapInProtocol(i.userKey, i.serverKey, i.userRefundKey, i.refundDelay) + // The remote partial signature may be invalid: when receiving their tx_signatures, we verify that the resulting transaction is valid. + val witness = swapInProtocol.witness(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, previousOutputs, userSig.localNonce, userSig.remoteNonce, userSig.sig, serverSig.sig).getOrDefault(ScriptWitness.empty) + Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), witness)) + } val remoteOnlyTxIn = tx.remoteOnlyInputs().sortedBy { i -> i.serialId }.zip(remoteSigs.witnesses).map { (i, w) -> Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), w)) } - val remoteSwapTxIn = tx.remoteSwapInputs().sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserSigs.zip(localSigs.swapInServerSigs)).map { (i, sigs) -> + val remoteLegacySwapTxIn = tx.remoteInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserSigs.zip(localSigs.swapInServerSigs)).map { (i, sigs) -> + val (userSig, serverSig) = sigs + val swapInProtocol = SwapInProtocolLegacy(i.userKey, i.serverKey, i.refundDelay) + val witness = swapInProtocol.witness(userSig, serverSig) + Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), witness)) + } + val remoteSwapTxIn = tx.remoteInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserPartialSigs.zip(localSigs.swapInServerPartialSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs - val witness = Scripts.witnessSwapIn2of2(userSig, i.userKey, serverSig, i.serverKey, i.refundDelay) + val swapInProtocol = SwapInProtocol(i.userKey, i.serverKey, i.userRefundKey, i.refundDelay) + // The remote partial signature may be invalid: when receiving their tx_signatures, we verify that the resulting transaction is valid. + val witness = swapInProtocol.witness(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, previousOutputs, userSig.localNonce, userSig.remoteNonce, userSig.sig, serverSig.sig).getOrDefault(ScriptWitness.empty) Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), witness)) } - val inputs = (sharedTxIn + localOnlyTxIn + localSwapTxIn + remoteOnlyTxIn + remoteSwapTxIn).sortedBy { (serialId, _) -> serialId }.map { (_, i) -> i } + val inputs = (sharedTxIn + localOnlyTxIn + localLegacySwapTxIn + localSwapTxIn + remoteOnlyTxIn + remoteLegacySwapTxIn + remoteSwapTxIn).sortedBy { (serialId, _) -> serialId }.map { (_, i) -> i } val sharedTxOut = listOf(Pair(tx.sharedOutput.serialId, TxOut(tx.sharedOutput.amount, tx.sharedOutput.pubkeyScript))) val localTxOut = tx.localOutputs.map { o -> Pair(o.serialId, TxOut(o.amount, o.pubkeyScript)) } val remoteTxOut = tx.remoteOutputs.map { o -> Pair(o.serialId, TxOut(o.amount, o.pubkeyScript)) } @@ -455,10 +602,12 @@ sealed class InteractiveTxSessionAction { data class InvalidTxWeight(val channelId: ByteVector32, val txId: TxId) : RemoteFailure() { override fun toString(): String = "transaction weight is too big for standardness rules (txId=$txId)" } data class InvalidTxFeerate(val channelId: ByteVector32, val txId: TxId, val targetFeerate: FeeratePerKw, val actualFeerate: FeeratePerKw) : RemoteFailure() { override fun toString(): String = "transaction feerate too low (txId=$txId, targetFeerate=$targetFeerate, actualFeerate=$actualFeerate" } data class InvalidTxDoesNotDoubleSpendPreviousTx(val channelId: ByteVector32, val txId: TxId, val previousTxId: TxId) : RemoteFailure() { override fun toString(): String = "transaction replacement with txId=$txId doesn't double-spend previous attempt (txId=$previousTxId)" } + data class MissingNonce(val channelId: ByteVector32, val expected: Int, val received: Int): RemoteFailure() { override fun toString(): String = "some musig2 nonces are missing: we expected $expected nonces but received only $received" } // @formatter:on } data class InteractiveTxSession( + val remoteNodeId: PublicKey, val channelKeys: KeyManager.ChannelKeys, val swapInKeys: KeyManager.SwapInOnChainKeys, val fundingParams: InteractiveTxParams, @@ -470,10 +619,11 @@ data class InteractiveTxSession( val remoteInputs: List = listOf(), val localOutputs: List = listOf(), val remoteOutputs: List = listOf(), - val txCompleteSent: Boolean = false, - val txCompleteReceived: Boolean = false, + val txCompleteSent: TxComplete? = null, + val txCompleteReceived: TxComplete? = null, val inputsReceivedCount: Int = 0, val outputsReceivedCount: Int = 0, + val secretNonces: Map> = mapOf() ) { // Example flow: @@ -491,6 +641,7 @@ data class InteractiveTxSession( // +-------+ +-------+ constructor( + remoteNodeId: PublicKey, channelKeys: KeyManager.ChannelKeys, swapInKeys: KeyManager.SwapInOnChainKeys, fundingParams: InteractiveTxParams, @@ -500,6 +651,7 @@ data class InteractiveTxSession( fundingContributions: FundingContributions, previousTxs: List = listOf() ) : this( + remoteNodeId, channelKeys, swapInKeys, fundingParams, @@ -509,13 +661,20 @@ data class InteractiveTxSession( localHtlcs ) - val isComplete: Boolean = txCompleteSent && txCompleteReceived + val isComplete: Boolean = txCompleteSent != null && txCompleteReceived != null fun send(): Pair { return when (val msg = toSend.firstOrNull()) { null -> { - val txComplete = TxComplete(fundingParams.channelId) - val next = copy(txCompleteSent = true) + val localSwapIns = localInputs.filterIsInstance() + val remoteSwapIns = remoteInputs.filterIsInstance() + val publicNonces = (localSwapIns + remoteSwapIns) + .map { it.serialId } + .sorted() + // We generate secret nonces whenever we send and receive tx_add_input, so we know they exist in the map. + .map { serialId -> secretNonces[serialId]!!.second } + val txComplete = TxComplete(fundingParams.channelId, publicNonces) + val next = copy(txCompleteSent = txComplete) if (next.isComplete) { Pair(next, next.validateTx(txComplete)) } else { @@ -523,17 +682,34 @@ data class InteractiveTxSession( } } is Either.Left -> { - val next = copy(toSend = toSend.tail(), localInputs = localInputs + msg.value, txCompleteSent = false) - val swapInParams = TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay) val txAddInput = when (msg.value) { is InteractiveTxInput.LocalOnly -> TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence) - is InteractiveTxInput.LocalSwapIn -> TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence, TlvStream(swapInParams)) + is InteractiveTxInput.LocalLegacySwapIn -> { + val swapInParams = TxAddInputTlv.SwapInParamsLegacy(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay) + TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence, TlvStream(swapInParams)) + } + is InteractiveTxInput.LocalSwapIn -> { + val swapInParams = TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.userRefundPublicKey, swapInKeys.refundDelay) + TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence, TlvStream(swapInParams)) + } is InteractiveTxInput.Shared -> TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.outPoint, msg.value.sequence) } + val nextSecretNonces = when (msg.value) { + // Generate a secret nonce for this input if we don't already have one. + is InteractiveTxInput.LocalSwapIn -> when (secretNonces[msg.value.serialId]) { + null -> { + val secretNonce = Musig2.generateNonce(randomBytes32(), swapInKeys.userPrivateKey, listOf(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey)) + secretNonces + (msg.value.serialId to secretNonce) + } + else -> secretNonces + } + else -> secretNonces + } + val next = copy(toSend = toSend.tail(), localInputs = localInputs + msg.value, txCompleteSent = null, secretNonces = nextSecretNonces) Pair(next, InteractiveTxSessionAction.SendMessage(txAddInput)) } is Either.Right -> { - val next = copy(toSend = toSend.tail(), localOutputs = localOutputs + msg.value, txCompleteSent = false) + val next = copy(toSend = toSend.tail(), localOutputs = localOutputs + msg.value, txCompleteSent = null) val txAddOutput = when (msg.value) { is InteractiveTxOutput.Local -> TxAddOutput(fundingParams.channelId, msg.value.serialId, msg.value.amount, msg.value.pubkeyScript) is InteractiveTxOutput.Shared -> TxAddOutput(fundingParams.channelId, msg.value.serialId, msg.value.amount, msg.value.pubkeyScript) @@ -543,7 +719,7 @@ data class InteractiveTxSession( } } - private fun receiveInput(message: TxAddInput): Either { + private fun receiveInput(message: TxAddInput): Either { if (inputsReceivedCount + 1 >= MAX_INPUTS_OUTPUTS_RECEIVED) { return Either.Left(InteractiveTxSessionAction.TooManyInteractiveTxRounds(message.channelId)) } @@ -556,8 +732,9 @@ data class InteractiveTxSession( val expectedSharedOutpoint = fundingParams.sharedInput?.info?.outPoint ?: return Either.Left(InteractiveTxSessionAction.PreviousTxMissing(message.channelId, message.serialId)) val receivedSharedOutpoint = message.sharedInput ?: return Either.Left(InteractiveTxSessionAction.PreviousTxMissing(message.channelId, message.serialId)) if (expectedSharedOutpoint != receivedSharedOutpoint) return Either.Left(InteractiveTxSessionAction.PreviousTxMissing(message.channelId, message.serialId)) - InteractiveTxInput.Shared(message.serialId, receivedSharedOutpoint, message.sequence, previousFunding.toLocal, previousFunding.toRemote) + InteractiveTxInput.Shared(message.serialId, receivedSharedOutpoint, fundingParams.sharedInput.info.txOut.publicKeyScript, message.sequence, previousFunding.toLocal, previousFunding.toRemote, previousFunding.toHtlcs) } + else -> { if (message.previousTx.txOut.size <= message.previousTxOutput) { return Either.Left(InteractiveTxSessionAction.InputOutOfBounds(message.channelId, message.serialId, message.previousTx.txid, message.previousTxOutput)) @@ -572,9 +749,27 @@ data class InteractiveTxSession( } val outpoint = OutPoint(message.previousTx, message.previousTxOutput) val txOut = message.previousTx.txOut[message.previousTxOutput.toInt()] - when (message.swapInParams) { - null -> InteractiveTxInput.RemoteOnly(message.serialId, outpoint, txOut, message.sequence) - else -> InteractiveTxInput.RemoteSwapIn(message.serialId, outpoint, txOut, message.sequence, message.swapInParams.userKey, message.swapInParams.serverKey, message.swapInParams.refundDelay) + when { + message.swapInParams != null -> InteractiveTxInput.RemoteSwapIn( + message.serialId, + outpoint, + txOut, + message.sequence, + message.swapInParams.userKey, + message.swapInParams.serverKey, + message.swapInParams.userRefundKey, + message.swapInParams.refundDelay + ) + message.swapInParamsLegacy != null -> InteractiveTxInput.RemoteLegacySwapIn( + message.serialId, + outpoint, + txOut, + message.sequence, + message.swapInParamsLegacy.userKey, + message.swapInParamsLegacy.serverKey, + message.swapInParamsLegacy.refundDelay + ) + else -> InteractiveTxInput.RemoteOnly(message.serialId, outpoint, txOut, message.sequence) } } } @@ -584,7 +779,19 @@ data class InteractiveTxSession( if (message.sequence > 0xfffffffdU) { return Either.Left(InteractiveTxSessionAction.NonReplaceableInput(message.channelId, message.serialId, input.outPoint.txid, input.outPoint.index, message.sequence.toLong())) } - return Either.Right(input) + val secretNonces1 = when (input) { + // Generate a secret nonce for this input if we don't already have one. + is InteractiveTxInput.RemoteSwapIn -> when (secretNonces[input.serialId]) { + null -> { + val secretNonce = Musig2.generateNonce(randomBytes32(), swapInKeys.localServerPrivateKey(remoteNodeId), listOf(input.userKey, input.serverKey)) + secretNonces + (input.serialId to secretNonce) + } + else -> secretNonces + } + else -> secretNonces + } + val session1 = this.copy(remoteInputs = remoteInputs + input, inputsReceivedCount = inputsReceivedCount + 1, txCompleteReceived = null, secretNonces = secretNonces1) + return Either.Right(session1) } private fun receiveOutput(message: TxAddOutput): Either { @@ -613,19 +820,19 @@ data class InteractiveTxSession( is TxAddInput -> { receiveInput(message).fold( { f -> Pair(this, f) }, - { input -> copy(remoteInputs = remoteInputs + input, inputsReceivedCount = inputsReceivedCount + 1, txCompleteReceived = false).send() } + { next -> next.send() } ) } is TxAddOutput -> { receiveOutput(message).fold( { f -> Pair(this, f) }, - { output -> copy(remoteOutputs = remoteOutputs + output, outputsReceivedCount = outputsReceivedCount + 1, txCompleteReceived = false).send() } + { output -> copy(remoteOutputs = remoteOutputs + output, outputsReceivedCount = outputsReceivedCount + 1, txCompleteReceived = null).send() } ) } is TxRemoveInput -> { val remoteInputs1 = remoteInputs.filterNot { i -> (i as InteractiveTxInput).serialId == message.serialId } if (remoteInputs.size != remoteInputs1.size) { - val next = copy(remoteInputs = remoteInputs1, txCompleteReceived = false) + val next = copy(remoteInputs = remoteInputs1, txCompleteReceived = null) next.send() } else { Pair(this, InteractiveTxSessionAction.UnknownSerialId(message.channelId, message.serialId)) @@ -634,14 +841,14 @@ data class InteractiveTxSession( is TxRemoveOutput -> { val remoteOutputs1 = remoteOutputs.filterNot { o -> (o as InteractiveTxOutput).serialId == message.serialId } if (remoteOutputs.size != remoteOutputs1.size) { - val next = copy(remoteOutputs = remoteOutputs1, txCompleteReceived = false) + val next = copy(remoteOutputs = remoteOutputs1, txCompleteReceived = null) next.send() } else { Pair(this, InteractiveTxSessionAction.UnknownSerialId(message.channelId, message.serialId)) } } is TxComplete -> { - val next = copy(txCompleteReceived = true) + val next = copy(txCompleteReceived = message) if (next.isComplete) { Pair(next, next.validateTx(null)) } else { @@ -652,6 +859,10 @@ data class InteractiveTxSession( } private fun validateTx(txComplete: TxComplete?): InteractiveTxSessionAction { + // tx_complete MUST have been sent and received for us to reach this state, require is used here to tell the compiler that txCompleteSent and txCompleteReceived are not null + require(txCompleteSent != null) + require(txCompleteReceived != null) + if (localInputs.size + remoteInputs.size > 252 || localOutputs.size + remoteOutputs.size > 252) { return InteractiveTxSessionAction.InvalidTxInputOutputCount(fundingParams.channelId, localInputs.size + remoteInputs.size, localOutputs.size + remoteOutputs.size) } @@ -687,6 +898,12 @@ data class InteractiveTxSession( sharedInputs.first() } + // Our peer must send us one nonce for each swap input (local and remote), ordered by serial_id. + val swapInputsCount = localInputs.count { it is InteractiveTxInput.LocalSwapIn } + remoteInputs.count { it is InteractiveTxInput.RemoteSwapIn } + if (txCompleteReceived.publicNonces.size != swapInputsCount) { + return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, swapInputsCount, txCompleteReceived.publicNonces.size) + } + val sharedTx = SharedTransaction(sharedInput, sharedOutput, localOnlyInputs, remoteOnlyInputs, localOnlyOutputs, remoteOnlyOutputs, fundingParams.lockTime) val tx = sharedTx.buildUnsignedTx() if (sharedTx.localAmountIn < sharedTx.localAmountOut || sharedTx.remoteAmountIn < sharedTx.remoteAmountOut) { @@ -827,6 +1044,7 @@ data class InteractiveTxSigningSession( data class UnsignedLocalCommit(val index: Long, val spec: CommitmentSpec, val commitTx: Transactions.TransactionWithInputInfo.CommitTx, val htlcTxs: List) fun create( + session: InteractiveTxSession, keyManager: KeyManager, channelParams: ChannelParams, fundingParams: InteractiveTxParams, @@ -888,7 +1106,7 @@ data class InteractiveTxSigningSession( // We haven't received the remote commit_sig: we don't have local htlc txs yet. val unsignedLocalCommit = UnsignedLocalCommit(localCommitmentIndex, firstCommitTx.localSpec, firstCommitTx.localCommitTx, listOf()) val remoteCommit = RemoteCommit(remoteCommitmentIndex, firstCommitTx.remoteSpec, firstCommitTx.remoteCommitTx.tx.txid, remotePerCommitmentPoint) - val signedFundingTx = sharedTx.sign(keyManager, fundingParams, channelParams.localParams, channelParams.remoteParams.nodeId) + val signedFundingTx = sharedTx.sign(session, keyManager, fundingParams, channelParams.localParams, channelParams.remoteParams.nodeId) Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, liquidityLease, Either.Left(unsignedLocalCommit), remoteCommit), commitSig) } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt index d6063f507..86d707579 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt @@ -113,7 +113,7 @@ sealed class ChannelState { // this code is only executed for the first transition to Closing, so there can only be one transaction here val closingTx = newState.mutualClosePublished.first() val finalAmount = closingTx.toLocalOutput?.amount ?: 0.sat - val address = closingTx.toLocalOutput?.publicKeyScript?.let { Bitcoin.addressFromPublicKeyScript(staticParams.nodeParams.chainHash, it.toByteArray()).result } ?: "unknown" + val address = closingTx.toLocalOutput?.publicKeyScript?.let { Bitcoin.addressFromPublicKeyScript(staticParams.nodeParams.chainHash, it.toByteArray()).right } ?: "unknown" listOf( ChannelAction.Storage.StoreOutgoingPayment.ViaClose( amount = finalAmount, @@ -142,7 +142,7 @@ sealed class ChannelState { val address = Bitcoin.addressFromPublicKeyScript( chainHash = staticParams.nodeParams.chainHash, pubkeyScript = oldState.commitments.params.localParams.defaultFinalScriptPubKey.toByteArray() // force close always send to the default script - ).result ?: "unknown" + ).right ?: "unknown" listOf( ChannelAction.Storage.StoreOutgoingPayment.ViaClose( amount = channelBalance.truncateToSatoshi(), diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index d26cb1bb0..b99ccd05e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -475,6 +475,7 @@ data class Normal( targetFeerate = cmd.message.feerate ) val session = InteractiveTxSession( + staticParams.remoteNodeId, channelKeys(), keyManager.swapInOnChainWallet, fundingParams, @@ -557,6 +558,7 @@ data class Normal( is Either.Right -> { // The splice initiator always sends the first interactive-tx message. val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession( + staticParams.remoteNodeId, channelKeys(), keyManager.swapInOnChainWallet, fundingParams, @@ -603,6 +605,7 @@ data class Normal( is InteractiveTxSessionAction.SignSharedTx -> { val parentCommitment = commitments.active.first() val signingSession = InteractiveTxSigningSession.create( + interactiveTxSession, keyManager, commitments.params, spliceStatus.spliceSession.fundingParams, @@ -880,7 +883,7 @@ data class Normal( ChannelAction.Storage.StoreOutgoingPayment.ViaSpliceOut( amount = txOut.amount, miningFees = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(), - address = Bitcoin.addressFromPublicKeyScript(staticParams.nodeParams.chainHash, txOut.publicKeyScript.toByteArray()).result ?: "unknown", + address = Bitcoin.addressFromPublicKeyScript(staticParams.nodeParams.chainHash, txOut.publicKeyScript.toByteArray()).right ?: "unknown", txId = action.fundingTx.txId ) }) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt index df4a37c19..b5a5ce3fe 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt @@ -59,7 +59,7 @@ data class WaitForAcceptChannel( } is Either.Right -> { // The channel initiator always sends the first interactive-tx message. - val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession(channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value).send() + val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession(staticParams.remoteNodeId, channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value).send() when (interactiveTxAction) { is InteractiveTxSessionAction.SendMessage -> { val nextState = WaitForFundingCreated( diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt index 32c1e6fda..e297823da 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt @@ -107,7 +107,7 @@ data class WaitForFundingConfirmed( addAll(latestFundingTx.sharedTx.tx.localInputs.map { Either.Left(it) }) addAll(latestFundingTx.sharedTx.tx.localOutputs.map { Either.Right(it) }) } - val session = InteractiveTxSession(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, SharedFundingInputBalances(0.msat, 0.msat, 0.msat), toSend, previousFundingTxs.map { it.sharedTx }, commitments.latest.localCommit.spec.htlcs) + val session = InteractiveTxSession(staticParams.remoteNodeId, channelKeys(), keyManager.swapInOnChainWallet, fundingParams, SharedFundingInputBalances(0.msat, 0.msat, 0.msat), toSend, previousFundingTxs.map { it.sharedTx }, commitments.latest.localCommit.spec.htlcs) val nextState = this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.InProgress(session)) Pair(nextState, listOf(ChannelAction.Message.Send(TxAckRbf(channelId, fundingParams.localContribution)))) } @@ -142,7 +142,7 @@ data class WaitForFundingConfirmed( Pair(this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, ChannelFundingError(channelId).message)))) } is Either.Right -> { - val (session, action) = InteractiveTxSession(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), contributions.value, previousFundingTxs.map { it.sharedTx }).send() + val (session, action) = InteractiveTxSession(staticParams.remoteNodeId, channelKeys(), keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), contributions.value, previousFundingTxs.map { it.sharedTx }).send() when (action) { is InteractiveTxSessionAction.SendMessage -> { val nextState = this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.InProgress(session)) @@ -169,6 +169,7 @@ data class WaitForFundingConfirmed( is InteractiveTxSessionAction.SignSharedTx -> { val replacedCommitment = commitments.latest val signingSession = InteractiveTxSigningSession.create( + rbfSession1, keyManager, commitments.params, rbfSession1.fundingParams, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt index d67627a67..e7fb016e0 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt @@ -55,6 +55,7 @@ data class WaitForFundingCreated( is InteractiveTxSessionAction.SignSharedTx -> { val channelParams = ChannelParams(channelId, channelConfig, channelFeatures, localParams, remoteParams, channelFlags) val signingSession = InteractiveTxSigningSession.create( + interactiveTxSession1, keyManager, channelParams, interactiveTxSession.fundingParams, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt index d9a3d6e42..ffb6e19c6 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt @@ -86,7 +86,7 @@ data class WaitForOpenChannel( Pair(Aborted, listOf(ChannelAction.Message.Send(Error(temporaryChannelId, ChannelFundingError(temporaryChannelId).message)))) } is Either.Right -> { - val interactiveTxSession = InteractiveTxSession(channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value) + val interactiveTxSession = InteractiveTxSession(staticParams.remoteNodeId, channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value) val nextState = WaitForFundingCreated( localParams, remoteParams, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index ab0dab9fe..33716dede 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt @@ -2,11 +2,15 @@ package fr.acinq.lightning.crypto import fr.acinq.bitcoin.* import fr.acinq.bitcoin.DeterministicWallet.hardened +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce +import fr.acinq.bitcoin.crypto.musig2.SecretNonce import fr.acinq.bitcoin.io.ByteArrayInput +import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.DefaultSwapInParams import fr.acinq.lightning.NodeParams import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.transactions.Scripts +import fr.acinq.lightning.transactions.SwapInProtocol +import fr.acinq.lightning.transactions.SwapInProtocolLegacy import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.sum import fr.acinq.lightning.utils.toByteVector @@ -118,28 +122,38 @@ interface KeyManager { val refundDelay: Int = DefaultSwapInParams.RefundDelay ) { private val userExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInUserKeyPath(chain)) + private val userRefundExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInUserRefundKeyPath(chain)) + val userPrivateKey: PrivateKey = userExtendedPrivateKey.privateKey val userPublicKey: PublicKey = userPrivateKey.publicKey() + val userRefundPrivateKey: PrivateKey = DeterministicWallet.derivePrivateKey(userRefundExtendedPrivateKey, 0).privateKey + val userRefundPublicKey: PublicKey = userRefundPrivateKey.publicKey() + private val localServerExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInLocalServerKeyPath(chain)) fun localServerPrivateKey(remoteNodeId: PublicKey): PrivateKey = DeterministicWallet.derivePrivateKey(localServerExtendedPrivateKey, perUserPath(remoteNodeId)).privateKey - val redeemScript: List = Scripts.swapIn2of2(userPublicKey, remoteServerPublicKey, refundDelay) - val pubkeyScript: List = Script.pay2wsh(redeemScript) - val address: String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).result!! + val swapInProtocol = SwapInProtocol(userPublicKey, remoteServerPublicKey, userRefundPublicKey, refundDelay) - /** - * The output script descriptor matching our swap-in addresses. - * That descriptor can be imported in bitcoind to recover funds after the refund delay. - */ - val descriptor = run { - // 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(master).publickeybytes).take(4).toByteArray()) - val encodedChildKey = DeterministicWallet.encode(DeterministicWallet.publicKey(userExtendedPrivateKey), testnet = chain != NodeParams.Chain.Mainnet) - val userKey = "[${masterFingerprint.toHex()}/${encodedSwapInUserKeyPath(chain)}]$encodedChildKey" - "wsh(and_v(v:pk($userKey),or_d(pk(${remoteServerPublicKey.toHex()}),older($refundDelay))))" + // 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 = 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) + } + + fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, privateNonce: SecretNonce, userNonce: IndividualNonce, serverNonce: IndividualNonce): Either { + return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts, userPrivateKey, privateNonce, userNonce, serverNonce) } /** @@ -150,12 +164,11 @@ interface KeyManager { * @return a signed transaction that spends our swap-in transaction. It cannot be published until `swapInTx` has enough confirmations */ fun createRecoveryTransaction(swapInTx: Transaction, address: String, feeRate: FeeratePerKw): Transaction? { - val utxos = swapInTx.txOut.filter { it.publicKeyScript.contentEquals(Script.write(pubkeyScript)) } + val utxos = swapInTx.txOut.filter { it.publicKeyScript.contentEquals(Script.write(legacySwapInProtocol.pubkeyScript)) || it.publicKeyScript.contentEquals(Script.write(swapInProtocol.pubkeyScript)) } return if (utxos.isEmpty()) { null } else { - val pubKeyScript = Bitcoin.addressToPublicKeyScript(chain.chainHash, address).result - pubKeyScript?.let { script -> + Bitcoin.addressToPublicKeyScript(chain.chainHash, address).right?.let { script -> val ourOutput = TxOut(utxos.map { it.amount }.sum(), script) val unsignedTx = Transaction( version = 2, @@ -163,18 +176,23 @@ interface KeyManager { txOut = listOf(ourOutput), lockTime = 0 ) - val fees = run { - val recoveryTx = utxos.foldIndexed(unsignedTx) { index, tx, utxo -> - val sig = Transactions.signSwapInputUser(tx, index, utxo, userPrivateKey, remoteServerPublicKey, refundDelay) - tx.updateWitness(index, Scripts.witnessSwapIn2of2Refund(sig, userPublicKey, remoteServerPublicKey, refundDelay)) + + fun sign(tx: Transaction, index: Int, utxo: TxOut): Transaction { + return if (Script.isPay2wsh(utxo.publicKeyScript.toByteArray())) { + val sig = legacySwapInProtocol.signSwapInputUser(tx, index, utxo, userPrivateKey) + tx.updateWitness(index, legacySwapInProtocol.witnessRefund(sig)) + } else { + val sig = swapInProtocol.signSwapInputRefund(tx, index, utxos, userRefundPrivateKey) + tx.updateWitness(index, swapInProtocol.witnessRefund(sig)) } + } + + val fees = run { + val recoveryTx = utxos.foldIndexed(unsignedTx) { index, tx, utxo -> sign(tx, index, utxo) } Transactions.weight2fee(feeRate, recoveryTx.weight()) } val unsignedTx1 = unsignedTx.copy(txOut = listOf(ourOutput.copy(amount = ourOutput.amount - fees))) - val recoveryTx = utxos.foldIndexed(unsignedTx1) { index, tx, utxo -> - val sig = Transactions.signSwapInputUser(tx, index, utxo, userPrivateKey, remoteServerPublicKey, refundDelay) - tx.updateWitness(index, Scripts.witnessSwapIn2of2Refund(sig, userPublicKey, remoteServerPublicKey, refundDelay)) - } + val recoveryTx = utxos.foldIndexed(unsignedTx1) { index, tx, utxo -> sign(tx, index, utxo) } // this tx is signed but cannot be published until swapInTx has `refundDelay` confirmations recoveryTx } @@ -191,6 +209,8 @@ interface KeyManager { fun swapInLocalServerKeyPath(chain: NodeParams.Chain) = swapInKeyBasePath(chain) / hardened(1) + fun swapInUserRefundKeyPath(chain: NodeParams.Chain) = swapInKeyBasePath(chain) / hardened(2) / 0L + fun encodedSwapInUserKeyPath(chain: NodeParams.Chain) = when (chain) { NodeParams.Chain.Regtest, NodeParams.Chain.Testnet -> "51h/0h/0h" NodeParams.Chain.Mainnet -> "52h/0h/0h" diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index e59afb912..7b08272c4 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -193,7 +193,8 @@ class Peer( val finalAddress: String = nodeParams.keyManager.finalOnChainWallet.address(addressIndex = 0L).also { finalWallet.addAddress(it) } val swapInWallet = ElectrumMiniWallet(nodeParams.chainHash, watcher.client, scope, nodeParams.loggerFactory, name = "swap-in") - val swapInAddress: String = nodeParams.keyManager.swapInOnChainWallet.address.also { swapInWallet.addAddress(it) } + val legacySwapInAddress: String = nodeParams.keyManager.swapInOnChainWallet.legacySwapInProtocol.address(nodeParams.chain).also { swapInWallet.addAddress(it) } + val swapInAddress: String = nodeParams.keyManager.swapInOnChainWallet.swapInProtocol.address(nodeParams.chain).also { swapInWallet.addAddress(it) } private var swapInJob: Job? = null @@ -930,7 +931,7 @@ class Peer( peerConnection?.send(Error(msg.temporaryChannelId, "cancelling open due to local liquidity policy")) return } - val fundingFee = Transactions.weight2fee(msg.fundingFeerate, request.walletInputs.size * Transactions.swapInputWeight) + val fundingFee = Transactions.weight2fee(msg.fundingFeerate, FundingContributions.weight(request.walletInputs)) // We have to pay the fees for our inputs, so we deduce them from our funding amount. val fundingAmount = request.walletInputs.balance - fundingFee // We pay the other fees by pushing the corresponding amount @@ -1136,7 +1137,7 @@ class Peer( cmd.requestId, cmd.walletInputs.balance, cmd.walletInputs.size, - cmd.walletInputs.size * Transactions.swapInputWeight, + FundingContributions.weight(cmd.walletInputs), TlvStream(PleaseOpenChannelTlv.GrandParents(grandParents)) ) logger.info { "sending please_open_channel with ${cmd.walletInputs.size} utxos (amount = ${cmd.walletInputs.balance})" } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt index 3de5553f3..ffb75c69f 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.serialization.v4 import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.readNBytes @@ -211,9 +212,29 @@ object Deserialization { 0x01 -> InteractiveTxInput.Shared( serialId = readNumber(), outPoint = readOutPoint(), + publicKeyScript = ByteVector.empty, sequence = readNumber().toUInt(), localAmount = readNumber().msat, - remoteAmount = readNumber().msat + remoteAmount = readNumber().msat, + htlcAmount = 0.msat + ) + 0x02 -> InteractiveTxInput.Shared( + serialId = readNumber(), + outPoint = readOutPoint(), + publicKeyScript = readDelimitedByteArray().byteVector(), + sequence = readNumber().toUInt(), + localAmount = readNumber().msat, + remoteAmount = readNumber().msat, + htlcAmount = 0.msat + ) + 0x03 -> InteractiveTxInput.Shared( + serialId = readNumber(), + outPoint = readOutPoint(), + publicKeyScript = readDelimitedByteArray().byteVector(), + sequence = readNumber().toUInt(), + localAmount = readNumber().msat, + remoteAmount = readNumber().msat, + htlcAmount = readNumber().msat ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Shared::class}") } @@ -225,13 +246,23 @@ object Deserialization { previousTxOutput = readNumber(), sequence = readNumber().toUInt(), ) - 0x02 -> InteractiveTxInput.LocalSwapIn( + 0x02 -> InteractiveTxInput.LocalLegacySwapIn( + serialId = readNumber(), + previousTx = readTransaction(), + previousTxOutput = readNumber(), + sequence = readNumber().toUInt(), + userKey = readPublicKey(), + serverKey = readPublicKey(), + refundDelay = readNumber().toInt(), + ) + 0x03 -> InteractiveTxInput.LocalSwapIn( serialId = readNumber(), previousTx = readTransaction(), previousTxOutput = readNumber(), sequence = readNumber().toUInt(), userKey = readPublicKey(), serverKey = readPublicKey(), + userRefundKey = readPublicKey(), refundDelay = readNumber().toInt(), ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Local::class}") @@ -244,7 +275,7 @@ object Deserialization { txOut = TxOut.read(readDelimitedByteArray()), sequence = readNumber().toUInt(), ) - 0x02 -> InteractiveTxInput.RemoteSwapIn( + 0x02 -> InteractiveTxInput.RemoteLegacySwapIn( serialId = readNumber(), outPoint = readOutPoint(), txOut = TxOut.read(readDelimitedByteArray()), @@ -253,6 +284,16 @@ object Deserialization { serverKey = readPublicKey(), refundDelay = readNumber().toInt() ) + 0x03 -> InteractiveTxInput.RemoteSwapIn( + serialId = readNumber(), + outPoint = readOutPoint(), + txOut = TxOut.read(readDelimitedByteArray()), + sequence = readNumber().toUInt(), + userKey = readPublicKey(), + serverKey = readPublicKey(), + userRefundKey = readPublicKey(), + refundDelay = readNumber().toInt() + ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Remote::class}") } @@ -570,6 +611,8 @@ object Deserialization { private fun Input.readOutPoint(): OutPoint = OutPoint.read(readDelimitedByteArray()) + private fun Input.readTxOut(): TxOut = TxOut.read(readDelimitedByteArray()) + private fun Input.readTransaction(): Transaction = Transaction.read(readDelimitedByteArray()) private fun Input.readTransactionWithInputInfo(): Transactions.TransactionWithInputInfo = when (val discriminator = read()) { @@ -611,6 +654,8 @@ object Deserialization { private fun Input.readTxId(): TxId = TxId(readByteVector32()) + private fun Input.readPublicNonce() = IndividualNonce(ByteArray(66).also { read(it, 0, it.size) }) + private fun Input.readDelimitedByteArray(): ByteArray { val size = readNumber().toInt() return ByteArray(size).also { read(it, 0, size) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt index d2c124804..70793c4ee 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.serialization.v4 import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Output import fr.acinq.lightning.FeatureSupport @@ -254,12 +255,14 @@ object Serialization { } private fun Output.writeSharedInteractiveTxInput(i: InteractiveTxInput.Shared) = i.run { - write(0x01) + write(0x03) writeNumber(serialId) writeBtcObject(outPoint) + writeDelimited(publicKeyScript.toByteArray()) writeNumber(sequence.toLong()) writeNumber(localAmount.toLong()) writeNumber(remoteAmount.toLong()) + writeNumber(htlcAmount.toLong()) } private fun Output.writeLocalInteractiveTxInput(i: InteractiveTxInput.Local) = when (i) { @@ -270,7 +273,7 @@ object Serialization { writeNumber(previousTxOutput) writeNumber(sequence.toLong()) } - is InteractiveTxInput.LocalSwapIn -> i.run { + is InteractiveTxInput.LocalLegacySwapIn -> i.run { write(0x02) writeNumber(serialId) writeBtcObject(previousTx) @@ -280,6 +283,17 @@ object Serialization { writePublicKey(serverKey) writeNumber(refundDelay) } + is InteractiveTxInput.LocalSwapIn -> i.run { + write(0x03) + writeNumber(serialId) + writeBtcObject(previousTx) + writeNumber(previousTxOutput) + writeNumber(sequence.toLong()) + writePublicKey(userKey) + writePublicKey(serverKey) + writePublicKey(userRefundKey) + writeNumber(refundDelay) + } } private fun Output.writeRemoteInteractiveTxInput(i: InteractiveTxInput.Remote) = when (i) { @@ -290,7 +304,7 @@ object Serialization { writeBtcObject(txOut) writeNumber(sequence.toLong()) } - is InteractiveTxInput.RemoteSwapIn -> i.run { + is InteractiveTxInput.RemoteLegacySwapIn -> i.run { write(0x02) writeNumber(serialId) writeBtcObject(outPoint) @@ -300,6 +314,17 @@ object Serialization { writePublicKey(serverKey) writeNumber(refundDelay) } + is InteractiveTxInput.RemoteSwapIn -> i.run { + write(0x03) + writeNumber(serialId) + writeBtcObject(outPoint) + writeBtcObject(txOut) + writeNumber(sequence.toLong()) + writePublicKey(userKey) + writePublicKey(serverKey) + writePublicKey(userRefundKey) + writeNumber(refundDelay) + } } private fun Output.writeSharedInteractiveTxOutput(o: InteractiveTxOutput.Shared) = o.run { @@ -687,6 +712,8 @@ object Serialization { private fun Output.writeTxId(o: TxId) = write(o.value.toByteArray()) + private fun Output.writePublicNonce(o: IndividualNonce) = write(o.toByteArray()) + private fun Output.writeDelimited(o: ByteArray) { writeNumber(o.size) write(o) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt index d36431ae1..985d09dec 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt @@ -30,32 +30,6 @@ object Scripts { ScriptWitness(listOf(ByteVector.empty, der(sig2, SigHash.SIGHASH_ALL), der(sig1, SigHash.SIGHASH_ALL), ByteVector(Script.write(multiSig2of2(pubkey1, pubkey2))))) } - /** - * @return the script used for a 2-of-2 swap-in as used in Phoenix. - */ - fun swapIn2of2(userKey: PublicKey, serverKey: PublicKey, delayedRefund: Int): List { - // This script was generated with https://bitcoin.sipa.be/miniscript/ using the following miniscript policy: - // and(pk(),or(99@pk(),older())) - // @formatter:off - return listOf( - OP_PUSHDATA(userKey), OP_CHECKSIGVERIFY, OP_PUSHDATA(serverKey), OP_CHECKSIG, OP_IFDUP, - OP_NOTIF, - OP_PUSHDATA(Script.encodeNumber(delayedRefund)), OP_CHECKSEQUENCEVERIFY, - OP_ENDIF - ) - // @formatter:on - } - - fun witnessSwapIn2of2(userSig: ByteVector64, userKey: PublicKey, serverSig: ByteVector64, serverKey: PublicKey, delayedRefund: Int): ScriptWitness { - val redeemScript = swapIn2of2(userKey, serverKey, delayedRefund) - return ScriptWitness(listOf(der(serverSig, SigHash.SIGHASH_ALL), der(userSig, SigHash.SIGHASH_ALL), Script.write(redeemScript).byteVector())) - } - - fun witnessSwapIn2of2Refund(userSig: ByteVector64, userKey: PublicKey, serverKey: PublicKey, delayedRefund: Int): ScriptWitness { - val redeemScript = swapIn2of2(userKey, serverKey, delayedRefund) - return ScriptWitness(listOf(ByteVector.empty, der(userSig, SigHash.SIGHASH_ALL), Script.write(redeemScript).byteVector())) - } - /** * minimal encoding of a number into a script element: * - OP_0 to OP_16 if 0 <= n <= 16 diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt new file mode 100644 index 000000000..d55a2d380 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt @@ -0,0 +1,143 @@ +package fr.acinq.lightning.transactions + +import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce +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 + +/** + * new swap-in protocol based on musig2 and taproot: (user key + server key) OR (user refund key + delay) + * for the common case, we use the musig2 aggregate of the user and server keys, spent through the key-spend path + * for the refund case, we use the refund script, spent through the script-spend path + * we use a different user key for the refund case: this allows us to generate generic descriptor for all swap-in addresses + * (see the descriptor() method below) + */ +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) + private val scriptTree = ScriptTree.Leaf(0, refundScript) + val pubkeyScript: List = Script.pay2tr(internalPublicKey, scriptTree) + + fun address(chain: NodeParams.Chain): String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).right!! + + fun witness(fundingTx: Transaction, index: Int, parentTxOuts: List, userNonce: IndividualNonce, serverNonce: IndividualNonce, userPartialSig: ByteVector32, serverPartialSig: ByteVector32): Either { + val publicKeys = listOf(userPublicKey, serverPublicKey) + val publicNonces = listOf(userNonce, serverNonce) + val sigs = listOf(userPartialSig, serverPartialSig) + return Musig2.aggregateTaprootSignatures(sigs, fundingTx, index, parentTxOuts, publicKeys, publicNonces, scriptTree).map { aggregateSig -> + Script.witnessKeyPathPay2tr(aggregateSig) + } + } + + fun witnessRefund(userSig: ByteVector64): ScriptWitness = Script.witnessScriptPathPay2tr(internalPublicKey, scriptTree, ScriptWitness(listOf(userSig)), scriptTree) + + fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, userPrivateKey: PrivateKey, privateNonce: SecretNonce, userNonce: IndividualNonce, serverNonce: IndividualNonce): Either { + require(userPrivateKey.publicKey() == userPublicKey) { "user private key does not match expected public key: are you using the refund key instead of the user key?" } + val publicKeys = listOf(userPublicKey, serverPublicKey) + val publicNonces = listOf(userNonce, serverNonce) + return Musig2.signTaprootInput(userPrivateKey, fundingTx, index, parentTxOuts, publicKeys, privateNonce, publicNonces, scriptTree) + } + + fun signSwapInputRefund(fundingTx: Transaction, index: Int, parentTxOuts: List, userPrivateKey: PrivateKey): ByteVector64 { + require(userPrivateKey.publicKey() == userRefundKey) { "refund private key does not match expected public key: are you using the user key instead of the refund key?" } + return Transaction.signInputTaprootScriptPath(userPrivateKey, fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, scriptTree.hash()) + } + + fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOuts: List, serverPrivateKey: PrivateKey, privateNonce: SecretNonce, userNonce: IndividualNonce, serverNonce: IndividualNonce): Either { + val publicKeys = listOf(userPublicKey, serverPublicKey) + val publicNonces = listOf(userNonce, serverNonce) + return Musig2.signTaprootInput(serverPrivateKey, fundingTx, index, parentTxOuts, publicKeys, privateNonce, publicNonces, scriptTree) + } + + 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" + } + + 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" + } + } +} + +/** + * legacy swap-in protocol, that uses p2wsh and a single "user + server OR user + delay" script + */ +data class SwapInProtocolLegacy(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val refundDelay: Int) { + // This script was generated with https://bitcoin.sipa.be/miniscript/ using the following miniscript policy: + // and(pk(),or(99@pk(),older())) + // @formatter:off + val redeemScript = listOf( + OP_PUSHDATA(userPublicKey), OP_CHECKSIGVERIFY, OP_PUSHDATA(serverPublicKey), OP_CHECKSIG, OP_IFDUP, + OP_NOTIF, + OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY, + OP_ENDIF + ) + // @formatter:on + + val pubkeyScript: List = Script.pay2wsh(redeemScript) + + fun address(chain: NodeParams.Chain): String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).right!! + + fun witness(userSig: ByteVector64, serverSig: ByteVector64): ScriptWitness { + return ScriptWitness(listOf(Scripts.der(serverSig, SigHash.SIGHASH_ALL), Scripts.der(userSig, SigHash.SIGHASH_ALL), Script.write(redeemScript).byteVector())) + } + + fun witnessRefund(userSig: ByteVector64): ScriptWitness { + return ScriptWitness(listOf(ByteVector.empty, Scripts.der(userSig, SigHash.SIGHASH_ALL), Script.write(redeemScript).byteVector())) + } + + fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOut: TxOut, userKey: PrivateKey): ByteVector64 { + require(userKey.publicKey() == userPublicKey) { "user private key does not match expected public key: are you using the refund key instead of the user key?" } + return Transactions.sign(fundingTx, index, Script.write(redeemScript), parentTxOut.amount, userKey) + } + + fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOut: TxOut, serverKey: PrivateKey): ByteVector64 { + return Transactions.sign(fundingTx, index, Script.write(redeemScript), parentTxOut.amount, serverKey) + } + + 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/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt index 2231a627e..28448b1b8 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt @@ -159,7 +159,10 @@ object Transactions { * - [[ClaimDelayedOutputPenaltyTx]] spends [[HtlcTimeoutTx]] using the revocation secret (published by local) * - [[HtlcPenaltyTx]] spends competes with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] for the same outputs (published by local) */ - const val swapInputWeight = 392 + // legacy swap-in. witness is 2 signatures (73 bytes) + redeem script (77 bytes) + const val swapInputWeightLegacy = 392 + // musig2 swap-in. witness is a single Schnorr signature (64 bytes) + const val swapInputWeight = 233 // The following values are specific to lightning and used to estimate fees. const val claimP2WPKHOutputWeight = 438 @@ -806,18 +809,6 @@ object Transactions { return sign(txInfo.tx, inputIndex, txInfo.input.redeemScript.toByteArray(), txInfo.input.txOut.amount, key, sigHash) } - /** Sign an input from a 2-of-2 swap-in address with the swap user's key. */ - fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOut: TxOut, userKey: PrivateKey, serverKey: PublicKey, refundDelay: Int): ByteVector64 { - val redeemScript = Scripts.swapIn2of2(userKey.publicKey(), serverKey, refundDelay) - return sign(fundingTx, index, Script.write(redeemScript), parentTxOut.amount, userKey) - } - - /** Sign an input from a 2-of-2 swap-in address with the swap server's key. */ - fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOut: TxOut, userKey: PublicKey, serverKey: PrivateKey, refundDelay: Int): ByteVector64 { - val redeemScript = Scripts.swapIn2of2(userKey, serverKey.publicKey(), refundDelay) - return sign(fundingTx, index, Script.write(redeemScript), parentTxOut.amount, serverKey) - } - fun addSigs( commitTx: TransactionWithInputInfo.CommitTx, localFundingPubkey: PublicKey, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt index 9e3ea7dd1..b3c2e6f68 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output import fr.acinq.lightning.utils.sat @@ -23,17 +24,38 @@ sealed class TxAddInputTlv : Tlv { } /** When adding a swap-in input to an interactive-tx, the user needs to provide the corresponding script parameters. */ - data class SwapInParams(val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : TxAddInputTlv() { + data class SwapInParamsLegacy(val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : TxAddInputTlv() { + override val tag: Long get() = SwapInParamsLegacy.tag + override fun write(out: Output) { + LightningCodecs.writeBytes(userKey.value, out) + LightningCodecs.writeBytes(serverKey.value, out) + LightningCodecs.writeU32(refundDelay, out) + } + + companion object : TlvValueReader { + const val tag: Long = 1107 + override fun read(input: Input): SwapInParamsLegacy = SwapInParamsLegacy( + PublicKey(LightningCodecs.bytes(input, 33)), + PublicKey(LightningCodecs.bytes(input, 33)), + LightningCodecs.u32(input) + ) + } + } + + /** When adding a swap-in input to an interactive-tx, the user needs to provide the corresponding script parameters. */ + data class SwapInParams(val userKey: PublicKey, val serverKey: PublicKey, val userRefundKey: PublicKey, val refundDelay: Int) : TxAddInputTlv() { override val tag: Long get() = SwapInParams.tag override fun write(out: Output) { LightningCodecs.writeBytes(userKey.value, out) LightningCodecs.writeBytes(serverKey.value, out) + LightningCodecs.writeBytes(userRefundKey.value, out) LightningCodecs.writeU32(refundDelay, out) } companion object : TlvValueReader { - const val tag: Long = 1107 + const val tag: Long = 1109 override fun read(input: Input): SwapInParams = SwapInParams( + PublicKey(LightningCodecs.bytes(input, 33)), PublicKey(LightningCodecs.bytes(input, 33)), PublicKey(LightningCodecs.bytes(input, 33)), LightningCodecs.u32(input) @@ -48,7 +70,25 @@ sealed class TxRemoveInputTlv : Tlv sealed class TxRemoveOutputTlv : Tlv -sealed class TxCompleteTlv : Tlv +sealed class TxCompleteTlv : Tlv { + /** Public nonces for all Musig2 swap-in inputs (local and remote), ordered by serial id. */ + data class Nonces(val nonces: List) : TxCompleteTlv() { + override val tag: Long get() = Nonces.tag + + override fun write(out: Output) { + nonces.forEach { LightningCodecs.writeBytes(it.toByteArray(), out) } + } + + companion object : TlvValueReader { + const val tag: Long = 101 + override fun read(input: Input): Nonces { + val count = input.availableBytes / 66 + val nonces = (0 until count).map { IndividualNonce(LightningCodecs.bytes(input, 66)) } + return Nonces(nonces) + } + } + } +} sealed class TxSignaturesTlv : Tlv { /** When doing a splice, each peer must provide their signature for the previous 2-of-2 funding output. */ @@ -92,6 +132,57 @@ sealed class TxSignaturesTlv : Tlv { } } + /** A partial musig2 signature, with the corresponding local and remote public nonces. */ + data class PartialSignature(val sig: ByteVector32, val localNonce: IndividualNonce, val remoteNonce: IndividualNonce) + + /** Partial musig2 signatures from the swap user for inputs that belong to them. */ + data class SwapInUserPartialSigs(val psigs: List) : TxSignaturesTlv() { + override val tag: Long get() = SwapInUserPartialSigs.tag + override fun write(out: Output) = psigs.forEach { psig -> + LightningCodecs.writeBytes(psig.sig, out) + LightningCodecs.writeBytes(psig.localNonce.toByteArray(), out) + LightningCodecs.writeBytes(psig.remoteNonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 607 + override fun read(input: Input): SwapInUserPartialSigs { + val count = input.availableBytes / (32 + 66 + 66) + val psigs = (0 until count).map { + val sig = LightningCodecs.bytes(input, 32).byteVector32() + val localNonce = IndividualNonce(LightningCodecs.bytes(input, 66)) + val remoteNonce = IndividualNonce(LightningCodecs.bytes(input, 66)) + PartialSignature(sig, localNonce, remoteNonce) + } + return SwapInUserPartialSigs(psigs) + } + } + } + + /** Partial musig2 signatures from the swap server for inputs that belong to the user. */ + data class SwapInServerPartialSigs(val psigs: List) : TxSignaturesTlv() { + override val tag: Long get() = SwapInServerPartialSigs.tag + override fun write(out: Output) = psigs.forEach { psig -> + LightningCodecs.writeBytes(psig.sig, out) + LightningCodecs.writeBytes(psig.localNonce.toByteArray(), out) + LightningCodecs.writeBytes(psig.remoteNonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 609 + override fun read(input: Input): SwapInServerPartialSigs { + val count = input.availableBytes / (32 + 66 + 66) + val psigs = (0 until count).map { + val sig = LightningCodecs.bytes(input, 32).byteVector32() + val localNonce = IndividualNonce(LightningCodecs.bytes(input, 66)) + val remoteNonce = IndividualNonce(LightningCodecs.bytes(input, 66)) + PartialSignature(sig, localNonce, remoteNonce) + } + return SwapInServerPartialSigs(psigs) + } + } + } + data class ChannelData(val ecb: EncryptedChannelData) : TxSignaturesTlv() { override val tag: Long get() = ChannelData.tag override fun write(out: Output) = LightningCodecs.writeBytes(ecb.data, out) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index dd013e9f1..598be3826 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Input @@ -334,6 +335,7 @@ data class TxAddInput( override val type: Long get() = TxAddInput.type val sharedInput: OutPoint? = tlvs.get()?.let { OutPoint(it.txId, previousTxOutput) } + val swapInParamsLegacy = tlvs.get() val swapInParams = tlvs.get() override fun write(out: Output) { @@ -358,6 +360,7 @@ data class TxAddInput( @Suppress("UNCHECKED_CAST") val readers = mapOf( TxAddInputTlv.SharedInputTxId.tag to TxAddInputTlv.SharedInputTxId.Companion as TlvValueReader, + TxAddInputTlv.SwapInParamsLegacy.tag to TxAddInputTlv.SwapInParamsLegacy.Companion as TlvValueReader, TxAddInputTlv.SwapInParams.tag to TxAddInputTlv.SwapInParams.Companion as TlvValueReader, ) @@ -454,12 +457,22 @@ data class TxComplete( ) : InteractiveTxConstructionMessage(), HasChannelId { override val type: Long get() = TxComplete.type - override fun write(out: Output) = LightningCodecs.writeBytes(channelId.toByteArray(), out) + val publicNonces: List = tlvs.get()?.nonces ?: listOf() + + constructor(channelId: ByteVector32, publicNonces: List) : this(channelId, TlvStream(TxCompleteTlv.Nonces(publicNonces))) + + override fun write(out: Output) { + LightningCodecs.writeBytes(channelId.toByteArray(), out) + TlvStreamSerializer(false, readers).write(tlvs, out) + } companion object : LightningMessageReader { const val type: Long = 70 - override fun read(input: Input): TxComplete = TxComplete(LightningCodecs.bytes(input, 32).byteVector32()) + @Suppress("UNCHECKED_CAST") + val readers = mapOf(TxCompleteTlv.Nonces.tag to TxCompleteTlv.Nonces.Companion as TlvValueReader) + + override fun read(input: Input): TxComplete = TxComplete(LightningCodecs.bytes(input, 32).byteVector32(), TlvStreamSerializer(false, readers).read(input)) } } @@ -469,16 +482,27 @@ data class TxSignatures( val witnesses: List, val tlvs: TlvStream = TlvStream.empty() ) : InteractiveTxMessage(), HasChannelId, HasEncryptedChannelData { - constructor(channelId: ByteVector32, tx: Transaction, witnesses: List, previousFundingSig: ByteVector64?, swapInUserSigs: List, swapInServerSigs: List) : this( + constructor( + channelId: ByteVector32, + tx: Transaction, + witnesses: List, + previousFundingSig: ByteVector64?, + swapInUserSigs: List, + swapInServerSigs: List, + swapInUserPartialSigs: List, + swapInServerPartialSigs: List + ) : this( channelId, tx.txid, witnesses, TlvStream( - listOfNotNull( + setOfNotNull( previousFundingSig?.let { TxSignaturesTlv.PreviousFundingTxSig(it) }, if (swapInUserSigs.isNotEmpty()) TxSignaturesTlv.SwapInUserSigs(swapInUserSigs) else null, if (swapInServerSigs.isNotEmpty()) TxSignaturesTlv.SwapInServerSigs(swapInServerSigs) else null, - ).toSet() + if (swapInUserPartialSigs.isNotEmpty()) TxSignaturesTlv.SwapInUserPartialSigs(swapInUserPartialSigs) else null, + if (swapInServerPartialSigs.isNotEmpty()) TxSignaturesTlv.SwapInServerPartialSigs(swapInServerPartialSigs) else null, + ) ), ) @@ -487,6 +511,8 @@ data class TxSignatures( val previousFundingTxSig: ByteVector64? = tlvs.get()?.sig val swapInUserSigs: List = tlvs.get()?.sigs ?: listOf() val swapInServerSigs: List = tlvs.get()?.sigs ?: listOf() + val swapInUserPartialSigs: List = tlvs.get()?.psigs ?: listOf() + val swapInServerPartialSigs: List = tlvs.get()?.psigs ?: listOf() override val channelData: EncryptedChannelData get() = tlvs.get()?.ecb ?: EncryptedChannelData.empty override fun withNonEmptyChannelData(ecd: EncryptedChannelData): TxSignatures = copy(tlvs = tlvs.addOrUpdate(TxSignaturesTlv.ChannelData(ecd))) @@ -512,6 +538,8 @@ data class TxSignatures( TxSignaturesTlv.PreviousFundingTxSig.tag to TxSignaturesTlv.PreviousFundingTxSig.Companion as TlvValueReader, TxSignaturesTlv.SwapInUserSigs.tag to TxSignaturesTlv.SwapInUserSigs.Companion as TlvValueReader, TxSignaturesTlv.SwapInServerSigs.tag to TxSignaturesTlv.SwapInServerSigs.Companion as TlvValueReader, + TxSignaturesTlv.SwapInUserPartialSigs.tag to TxSignaturesTlv.SwapInUserPartialSigs.Companion as TlvValueReader, + TxSignaturesTlv.SwapInServerPartialSigs.tag to TxSignaturesTlv.SwapInServerPartialSigs.Companion as TlvValueReader, TxSignaturesTlv.ChannelData.tag to TxSignaturesTlv.ChannelData.Companion as TlvValueReader, ) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumClientTest.kt b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumClientTest.kt index 106d5685d..3d9045bed 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumClientTest.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumClientTest.kt @@ -72,7 +72,7 @@ class ElectrumClientTest : LightningTestSuite() { @Test fun `status should be empty for unused addresses`() = runTest { client -> val address = "bc1qzp2r7k62chyyq7g8ppw2dxp2lcrt629ym0swqy" - val scriptHash = ElectrumClient.computeScriptHash(Script.write(Bitcoin.addressToPublicKeyScript(Block.LivenetGenesisBlock.hash, address).result!!).byteVector()) + val scriptHash = ElectrumClient.computeScriptHash(Script.write(Bitcoin.addressToPublicKeyScript(Block.LivenetGenesisBlock.hash, address).right!!).byteVector()) val status = client.startScriptHashSubscription(scriptHash) assertNull(status.status) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWalletTest.kt b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWalletTest.kt index e0e984605..3b15c2781 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWalletTest.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWalletTest.kt @@ -138,7 +138,7 @@ class ElectrumMiniWalletTest : LightningTestSuite() { ), actual = walletState.utxos.map { val txOut = it.previousTx.txOut[it.outputIndex] - val address = Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, txOut.publicKeyScript.toByteArray()).result!! + val address = Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, txOut.publicKeyScript.toByteArray()).right!! Triple(address, it.previousTx.txid to it.outputIndex, txOut.amount) }.toSet() ) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt index b62c61501..1ebc2f2e1 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt @@ -21,7 +21,7 @@ class HelpersTestsCommon : LightningTestSuite() { fun `compute address from pubkey script`() { val pub = PrivateKey(Hex.decode("0101010101010101010101010101010101010101010101010101010101010101")).publicKey() - fun address(script: List, chainHash: BlockHash) = Bitcoin.addressFromPublicKeyScript(chainHash, Script.write(script)).result + fun address(script: List, chainHash: BlockHash) = Bitcoin.addressFromPublicKeyScript(chainHash, Script.write(script)).right listOf(Block.LivenetGenesisBlock.hash, Block.TestnetGenesisBlock.hash, Block.RegtestGenesisBlock.hash).forEach { assertEquals(address(Script.pay2pkh(pub), it), computeP2PkhAddress(pub, it)) @@ -44,7 +44,7 @@ class HelpersTestsCommon : LightningTestSuite() { Triple("a91481b9ac6a59b53927da7277b5ad5460d781b365d987", Block.LivenetGenesisBlock.hash, "3DWwX7NYjnav66qygrm4mBCpiByjammaWy"), ).forEach { assertEquals( - Bitcoin.addressFromPublicKeyScript(it.second, Hex.decode(it.first)).result, + Bitcoin.addressFromPublicKeyScript(it.second, Hex.decode(it.first)).right, it.third ) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt index f9906150e..3d2a4a9dd 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt @@ -25,81 +25,120 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val targetFeerate = FeeratePerKw(5000.sat) val fundingA = 120_000.sat val utxosA = listOf(50_000.sat, 35_000.sat, 60_000.sat) - val fundingB = 40_000.sat - val utxosB = listOf(100_000.sat) - val f = createFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660.sat, 42) + val legacyUtxosA = listOf(10_000.sat, 60_000.sat) + val fundingB = 100_000.sat + val utxosB = listOf(30_000.sat, 100_000.sat) + val legacyUtxosB = listOf(25_000.sat, 50_000.sat) + val f = createFixture(fundingA, utxosA, legacyUtxosA, fundingB, utxosB, legacyUtxosB, targetFeerate, 660.sat, 42) assertEquals(f.fundingParamsA.fundingPubkeyScript(f.channelKeysA), f.fundingParamsB.fundingPubkeyScript(f.channelKeysB)) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) - // Alice --- tx_add_input --> Bob + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + + // 3 swap-in inputs, 2 legacy swap-in inputs, and 2 outputs from Alice + // 2 swap-in inputs, 2 legacy swap-in inputs, and 1 output from Bob + + // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) assertEquals(0xfffffffdU, inputA1.sequence) // Alice <-- tx_add_input --- Bob val (bob1, inputB1) = receiveMessage(bob0, inputA1) // Alice --- tx_add_input --> Bob val (alice2, inputA2) = receiveMessage(alice1, inputB1) - // Alice <-- tx_add_output --- Bob - val (bob2, outputB1) = receiveMessage(bob1, inputA2) + // Alice <-- tx_add_input --- Bob + val (bob2, inputB2) = receiveMessage(bob1, inputA2) // Alice --- tx_add_input --> Bob - val (alice3, inputA3) = receiveMessage(alice2, outputB1) - // Alice <-- tx_complete --- Bob - val (bob3, txCompleteB) = receiveMessage(bob2, inputA3) + val (alice3, inputA3) = receiveMessage(alice2, inputB2) + // Alice <-- tx_add_input --- Bob + val (bob3, inputB3) = receiveMessage(bob2, inputA3) + // Alice --- tx_add_input --> Bob + val (alice4, inputA4) = receiveMessage(alice3, inputB3) + // Alice <-- tx_add_input --- Bob + val (bob4, inputB4) = receiveMessage(bob3, inputA4) + // Alice --- tx_add_input --> Bob + val (alice5, inputA5) = receiveMessage(alice4, inputB4) + // Alice <-- tx_add_output --- Bob + val (bob5, outputB1) = receiveMessage(bob4, inputA5) // Alice --- tx_add_output --> Bob - val (alice4, outputA1) = receiveMessage(alice3, txCompleteB) + val (alice6, outputA1) = receiveMessage(alice5, outputB1) // Alice <-- tx_complete --- Bob - val (bob4, _) = receiveMessage(bob3, outputA1) + val (bob6, txCompleteB1) = receiveMessage(bob5, outputA1) // Alice --- tx_add_output --> Bob - val (alice5, outputA2) = receiveMessage(alice4, txCompleteB) - assertFalse(alice5.isComplete) + val (alice7, outputA2) = receiveMessage(alice6, txCompleteB1) // Alice <-- tx_complete --- Bob - val (bob5, _) = receiveMessage(bob4, outputA2) - assertFalse(bob5.isComplete) - // Alice --- tx_complete --> Bob - val sharedTxA = receiveFinalMessage(alice5, txCompleteB) + val (bob7, txCompleteB2) = receiveMessage(bob6, outputA2) + + val sharedTxA = receiveFinalMessage(alice7, txCompleteB2).second assertNotNull(sharedTxA.txComplete) - val sharedTxB = receiveFinalMessage(bob5, sharedTxA.txComplete!!) + + val (bob8, sharedTxB) = receiveFinalMessage(bob7, sharedTxA.txComplete!!) assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared output. assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == fundingA + fundingB }, 1) - assertEquals(sharedTxA.sharedTx.localAmountIn, 145_000_000.msat) - assertEquals(sharedTxA.sharedTx.remoteAmountIn, 100_000_000.msat) - assertEquals(sharedTxA.sharedTx.totalAmountIn, 245_000.sat) - assertEquals(sharedTxA.sharedTx.fees, 10_140.sat) + assertEquals(sharedTxA.sharedTx.localAmountIn, 215_000_000.msat) + assertEquals(sharedTxA.sharedTx.remoteAmountIn, 205_000_000.msat) + assertEquals(sharedTxA.sharedTx.totalAmountIn, 420_000.sat) + assertEquals(sharedTxA.sharedTx.fees, 15_965.sat) assertTrue(sharedTxB.sharedTx.localFees < sharedTxA.sharedTx.localFees) // Bob sends signatures first as he contributed less than Alice. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) - assertEquals(signedTxB.localSigs.swapInUserSigs.size, 1) - assertEquals(signedTxB.localSigs.swapInServerSigs.size, 3) + val signedTxB = sharedTxB.sharedTx.sign(bob8, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) + assertEquals(signedTxB.localSigs.swapInUserSigs.size, 2) + assertEquals(signedTxB.localSigs.swapInUserPartialSigs.size, 2) + assertEquals(signedTxB.localSigs.swapInServerSigs.size, 2) + assertEquals(signedTxB.localSigs.swapInServerPartialSigs.size, 3) + // Alice detects invalid signatures from Bob. val sigsInvalidTxId = signedTxB.localSigs.copy(txId = TxId(randomBytes32())) - assertNull(sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidTxId)) - val sigsMissingUserSigs = signedTxB.localSigs.copy(tlvs = TlvStream(TxSignaturesTlv.SwapInUserSigs(listOf()), TxSignaturesTlv.SwapInServerSigs(signedTxB.localSigs.swapInServerSigs))) - assertNull(sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingUserSigs)) - val sigsMissingServerSigs = signedTxB.localSigs.copy(tlvs = TlvStream(TxSignaturesTlv.SwapInUserSigs(signedTxB.localSigs.swapInUserSigs), TxSignaturesTlv.SwapInServerSigs(listOf()))) - assertNull(sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingServerSigs)) - val sigsInvalidUserSig = signedTxB.localSigs.copy(tlvs = TlvStream(TxSignaturesTlv.SwapInUserSigs(listOf(randomBytes64())), TxSignaturesTlv.SwapInServerSigs(signedTxB.localSigs.swapInServerSigs))) - assertNull(sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidUserSig)) - val sigsInvalidServerSig = signedTxB.localSigs.copy(tlvs = TlvStream(TxSignaturesTlv.SwapInUserSigs(signedTxB.localSigs.swapInUserSigs), TxSignaturesTlv.SwapInServerSigs(signedTxB.localSigs.swapInServerSigs.reversed()))) - assertNull(sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidServerSig)) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidTxId)) + + val sigsMissingUserSigs = signedTxB.localSigs.copy(tlvs = TlvStream(signedTxB.localSigs.tlvs.records.filterNot { it is TxSignaturesTlv.SwapInUserSigs }.toSet())) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingUserSigs)) + + val sigsMissingUserPartialSigs = signedTxB.localSigs.copy(tlvs = TlvStream(signedTxB.localSigs.tlvs.records.filterNot { it is TxSignaturesTlv.SwapInUserPartialSigs }.toSet())) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingUserPartialSigs)) + + val sigsMissingServerSigs = signedTxB.localSigs.copy(tlvs = TlvStream(signedTxB.localSigs.tlvs.records.filterNot { it is TxSignaturesTlv.SwapInServerSigs }.toSet())) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingServerSigs)) + + val sigsMissingServerPartialSigs = signedTxB.localSigs.copy(tlvs = TlvStream(signedTxB.localSigs.tlvs.records.filterNot { it is TxSignaturesTlv.SwapInServerPartialSigs }.toSet())) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingServerPartialSigs)) + + val invalidUserSigs = signedTxB.localSigs.swapInUserSigs.map { randomBytes64() } + val sigsInvalidUserSig = signedTxB.localSigs.copy(tlvs = TlvStream(signedTxB.localSigs.tlvs.records.filterNot { it is TxSignaturesTlv.SwapInUserSigs }.toSet() + TxSignaturesTlv.SwapInUserSigs(invalidUserSigs))) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidUserSig)) + + val invalidPartialUserSigs = signedTxB.localSigs.swapInUserPartialSigs.map { TxSignaturesTlv.PartialSignature(randomBytes32(), it.localNonce, it.remoteNonce) } + val sigsInvalidUserPartialSig = + signedTxB.localSigs.copy(tlvs = TlvStream(signedTxB.localSigs.tlvs.records.filterNot { it is TxSignaturesTlv.SwapInUserPartialSigs }.toSet() + TxSignaturesTlv.SwapInUserPartialSigs(invalidPartialUserSigs))) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidUserPartialSig)) + + val invalidServerSigs = signedTxB.localSigs.swapInServerSigs.map { randomBytes64() } + val sigsInvalidServerSig = signedTxB.localSigs.copy(tlvs = TlvStream(signedTxB.localSigs.tlvs.records.filterNot { it is TxSignaturesTlv.SwapInServerSigs }.toSet() + TxSignaturesTlv.SwapInServerSigs(invalidServerSigs))) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidServerSig)) + + val invalidPartialServerSigs = signedTxB.localSigs.swapInServerPartialSigs.map { TxSignaturesTlv.PartialSignature(randomBytes32(), it.localNonce, it.remoteNonce) } + val sigsInvalidServerPartialSig = signedTxB.localSigs.copy(tlvs = TlvStream(TxSignaturesTlv.SwapInUserPartialSigs(signedTxB.localSigs.swapInUserPartialSigs), TxSignaturesTlv.SwapInServerPartialSigs(invalidPartialServerSigs))) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidServerPartialSig)) // The resulting transaction is valid and has the right feerate. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) - assertEquals(signedTxA.localSigs.swapInUserSigs.size, 3) - assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) + assertEquals(signedTxA.localSigs.swapInUserSigs.size, 2) + assertEquals(signedTxA.localSigs.swapInUserPartialSigs.size, 3) + assertEquals(signedTxA.localSigs.swapInServerSigs.size, 2) + assertEquals(signedTxA.localSigs.swapInServerPartialSigs.size, 2) val signedTx = signedTxA.signedTx assertEquals(signedTxA.localSigs.txId, signedTx.txid) assertEquals(signedTxB.localSigs.txId, signedTx.txid) assertEquals(signedTx.lockTime, 42) - assertEquals(signedTx.txIn.size, 4) + assertEquals(signedTx.txIn.size, 9) assertEquals(signedTx.txOut.size, 3) Transaction.correctlySpends(signedTx, (sharedTxA.sharedTx.localInputs + sharedTxB.sharedTx.localInputs).map { it.previousTx }, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val feerate = Transactions.fee2rate(signedTxA.tx.fees, signedTx.weight()) @@ -111,56 +150,66 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val targetFeerate = FeeratePerKw(3000.sat) val fundingA = 10_000.sat val utxosA = listOf(50_000.sat) + val legacyUtxosA = listOf(30_000.sat) val fundingB = 50_000.sat val utxosB = listOf(80_000.sat) - val f = createFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660.sat, 0) + val legacyUtxosB = listOf(30_000.sat) + val f = createFixture(fundingA, utxosA, legacyUtxosA, fundingB, utxosB, legacyUtxosB, targetFeerate, 660.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Even though the initiator isn't contributing, they're paying the fees for the common parts of the transaction. // Alice --- tx_add_input --> Bob - val (alice1, inputA) = sendMessage(alice0) + val (alice1, inputA1) = sendMessage(alice0) + // Alice <-- tx_add_input --- Bob + val (bob1, inputB1) = receiveMessage(bob0, inputA1) + // Alice --- tx_add_input --> Bob + val (alice2, inputA2) = receiveMessage(alice1, inputB1) // Alice <-- tx_add_input --- Bob - val (bob1, inputB) = receiveMessage(bob0, inputA) + val (bob2, inputB2) = receiveMessage(bob1, inputA2) // Alice --- tx_add_output --> Bob - val (alice2, outputA1) = receiveMessage(alice1, inputB) + val (alice3, outputA1) = receiveMessage(alice2, inputB2) // Alice <-- tx_add_output --- Bob - val (bob2, outputB) = receiveMessage(bob1, outputA1) + val (bob3, outputB) = receiveMessage(bob2, outputA1) // Alice --- tx_add_output --> Bob - val (alice3, outputA2) = receiveMessage(alice2, outputB) + val (alice4, outputA2) = receiveMessage(alice3, outputB) // Alice <-- tx_complete --- Bob - val (bob3, txCompleteB) = receiveMessage(bob2, outputA2) + val (bob4, txCompleteB) = receiveMessage(bob3, outputA2) // Alice --- tx_complete --> Bob - val sharedTxA = receiveFinalMessage(alice3, txCompleteB) + val (alice5, sharedTxA) = receiveFinalMessage(alice4, txCompleteB) assertNotNull(sharedTxA.txComplete) - val sharedTxB = receiveFinalMessage(bob3, sharedTxA.txComplete!!) + val (bob5, sharedTxB) = receiveFinalMessage(bob4, sharedTxA.txComplete!!) assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared output. assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == fundingA + fundingB }, 1) - assertEquals(sharedTxA.sharedTx.totalAmountIn, 130_000.sat) - assertEquals(sharedTxA.sharedTx.fees, 3732.sat) + assertEquals(sharedTxA.sharedTx.totalAmountIn, 190_000.sat) + assertEquals(sharedTxA.sharedTx.fees, 5130.sat) assertTrue(sharedTxB.sharedTx.localFees < sharedTxA.sharedTx.localFees) // Alice sends signatures first as she contributed less than Bob. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) + assertEquals(signedTxA.localSigs.swapInUserPartialSigs.size, 1) + assertEquals(signedTxA.localSigs.swapInServerPartialSigs.size, 1) // The resulting transaction is valid and has the right feerate. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) + val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) assertNotNull(signedTxB) assertEquals(signedTxB.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxB.localSigs.swapInServerSigs.size, 1) + assertEquals(signedTxB.localSigs.swapInUserPartialSigs.size, 1) + assertEquals(signedTxB.localSigs.swapInServerPartialSigs.size, 1) val signedTx = signedTxB.signedTx assertEquals(signedTxA.localSigs.txId, signedTx.txid) assertEquals(signedTxB.localSigs.txId, signedTx.txid) assertEquals(signedTx.lockTime, 0) - assertEquals(signedTx.txIn.size, 2) + assertEquals(signedTx.txIn.size, 4) assertEquals(signedTx.txOut.size, 3) Transaction.correctlySpends(signedTx, (sharedTxA.sharedTx.localInputs + sharedTxB.sharedTx.localInputs).map { it.previousTx }, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val feerate = Transactions.fee2rate(signedTxB.tx.fees, signedTx.weight()) @@ -172,47 +221,55 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val targetFeerate = FeeratePerKw(5000.sat) val fundingA = 100_000.sat val utxosA = listOf(150_000.sat) + val legacyUtxosA = listOf(30_000.sat) val fundingB = 50_000.sat val utxosB = listOf(200_000.sat) - val f = createFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660.sat, 0) + val legacyUtxosB = listOf(30_000.sat) + val f = createFixture(fundingA, utxosA, legacyUtxosA, fundingB, utxosB, legacyUtxosB, targetFeerate, 660.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob - val (alice1, inputA) = sendMessage(alice0) + val (alice1, inputA1) = sendMessage(alice0) + // Alice <-- tx_add_input --- Bob + val (bob1, inputB1) = receiveMessage(bob0, inputA1) + // Alice --- tx_add_input --> Bob + val (alice2, inputA2) = receiveMessage(alice1, inputB1) // Alice <-- tx_add_input --- Bob - val (bob1, inputB) = receiveMessage(bob0, inputA) + val (bob2, inputB2) = receiveMessage(bob1, inputA2) // Alice --- tx_add_output --> Bob - val (alice2, outputA1) = receiveMessage(alice1, inputB) + val (alice3, outputA1) = receiveMessage(alice2, inputB2) // Alice <-- tx_add_output --- Bob - val (bob2, outputB) = receiveMessage(bob1, outputA1) + val (bob3, outputB) = receiveMessage(bob2, outputA1) // Alice --- tx_add_output --> Bob - val (alice3, outputA2) = receiveMessage(alice2, outputB) + val (alice4, outputA2) = receiveMessage(alice3, outputB) // Alice <-- tx_complete --- Bob - val (bob3, txCompleteB) = receiveMessage(bob2, outputA2) + val (bob4, txCompleteB) = receiveMessage(bob3, outputA2) // Alice --- tx_complete --> Bob - val sharedTxA = receiveFinalMessage(alice3, txCompleteB) + val (alice5, sharedTxA) = receiveFinalMessage(alice4, txCompleteB) assertNotNull(sharedTxA.txComplete) - val sharedTxB = receiveFinalMessage(bob3, sharedTxA.txComplete!!) + val (bob5, sharedTxB) = receiveFinalMessage(bob4, sharedTxA.txComplete!!) assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared output. assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == fundingA + fundingB }, 1) - assertEquals(sharedTxA.sharedTx.totalAmountIn, 350_000.sat) - assertEquals(sharedTxA.sharedTx.fees, 6220.sat) + assertEquals(sharedTxA.sharedTx.totalAmountIn, 410_000.sat) + assertEquals(sharedTxA.sharedTx.fees, 8550.sat) assertTrue(sharedTxA.sharedTx.remoteFees < sharedTxA.sharedTx.localFees) // Alice contributes more than Bob to the funding output, but Bob's inputs are bigger than Alice's, so Alice must sign first. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) + assertEquals(signedTxA.localSigs.swapInUserPartialSigs.size, 1) + assertEquals(signedTxA.localSigs.swapInServerPartialSigs.size, 1) // The resulting transaction is valid and has the right feerate. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) + val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) assertNotNull(signedTxB) Transaction.correctlySpends(signedTxB.signedTx, (sharedTxA.sharedTx.localInputs + sharedTxB.sharedTx.localInputs).map { it.previousTx }, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val feerate = Transactions.fee2rate(signedTxB.tx.fees, signedTxB.signedTx.weight()) @@ -224,11 +281,12 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val targetFeerate = FeeratePerKw(2500.sat) val fundingA = 150_000.sat val utxosA = listOf(80_000.sat, 120_000.sat) - val f = createFixture(fundingA, utxosA, 0.sat, listOf(), targetFeerate, 330.sat, 0) + val legacyUtxosA = listOf(30_000.sat) + val f = createFixture(fundingA, utxosA, legacyUtxosA, 0.sat, listOf(), listOf(), targetFeerate, 330.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, fundingA) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) // Alice <-- tx_complete --- Bob @@ -236,48 +294,54 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice --- tx_add_input --> Bob val (alice2, inputA2) = receiveMessage(alice1, txCompleteB) // Alice <-- tx_complete --- Bob - val (bob2, _) = receiveMessage(bob1, inputA2) + val (bob2, txCompleteB1) = receiveMessage(bob1, inputA2) + // Alice --- tx_add_input --> Bob + val (alice3, inputA3) = receiveMessage(alice2, txCompleteB1) + // Alice <-- tx_complete --- Bob + val (bob3, txCompleteB2) = receiveMessage(bob2, inputA3) // Alice --- tx_add_output --> Bob - val (alice3, outputA1) = receiveMessage(alice2, txCompleteB) + val (alice4, outputA1) = receiveMessage(alice3, txCompleteB2) // Alice <-- tx_complete --- Bob - val (bob3, _) = receiveMessage(bob2, outputA1) + val (bob4, txCompleteB3) = receiveMessage(bob3, outputA1) // Alice --- tx_add_output --> Bob - val (alice4, outputA2) = receiveMessage(alice3, txCompleteB) + val (alice5, outputA2) = receiveMessage(alice4, txCompleteB3) // Alice <-- tx_complete --- Bob - val (bob4, _) = receiveMessage(bob3, outputA2) + val (bob5, txCompleteB4) = receiveMessage(bob4, outputA2) // Alice --- tx_complete --> Bob - val sharedTxA = receiveFinalMessage(alice4, txCompleteB) + val sharedTxA = receiveFinalMessage(alice5, txCompleteB4).second assertNotNull(sharedTxA.txComplete) - val sharedTxB = receiveFinalMessage(bob4, sharedTxA.txComplete!!) + val (bob6, sharedTxB) = receiveFinalMessage(bob5, sharedTxA.txComplete!!) assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared output. assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == fundingA }, 1) - assertEquals(sharedTxA.sharedTx.totalAmountIn, 200_000.sat) - assertEquals(sharedTxA.sharedTx.fees, 2_800.sat) - assertEquals(sharedTxA.sharedTx.localFees, 2_800_000.msat) + assertEquals(sharedTxA.sharedTx.totalAmountIn, 230_000.sat) + assertEquals(sharedTxA.sharedTx.fees, 2985.sat) + assertEquals(sharedTxA.sharedTx.localFees, 2_985_000.msat) assertEquals(sharedTxA.sharedTx.remoteFees, 0.msat) assertEquals(sharedTxB.sharedTx.localFees, 0.msat) - assertEquals(sharedTxB.sharedTx.remoteFees, 2_800_000.msat) + assertEquals(sharedTxB.sharedTx.remoteFees, 2_985_000.msat) // Bob sends signatures first as he did not contribute at all. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob6, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) assertNotNull(signedTxB) assertEquals(signedTxB.localSigs.swapInUserSigs.size, 0) - assertEquals(signedTxB.localSigs.swapInServerSigs.size, 2) + assertEquals(signedTxB.localSigs.swapInUserPartialSigs.size, 0) + assertEquals(signedTxB.localSigs.swapInServerSigs.size, 1) + assertEquals(signedTxB.localSigs.swapInServerPartialSigs.size, 2) // The resulting transaction is valid and has the right feerate. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) - assertEquals(signedTxA.localSigs.swapInUserSigs.size, 2) + assertEquals(signedTxA.localSigs.swapInUserPartialSigs.size, 2) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 0) val signedTx = signedTxA.signedTx assertEquals(signedTxA.localSigs.txId, signedTx.txid) assertEquals(signedTxB.localSigs.txId, signedTx.txid) assertEquals(signedTx.lockTime, 0) - assertEquals(signedTx.txIn.size, 2) + assertEquals(signedTx.txIn.size, 3) assertEquals(signedTx.txOut.size, 2) Transaction.correctlySpends(signedTx, sharedTxA.sharedTx.localInputs.map { it.previousTx }, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val feerate = Transactions.fee2rate(signedTxA.tx.fees, signedTx.weight()) @@ -298,8 +362,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -317,9 +381,9 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice <-- tx_complete --- Bob val (bob4, _) = receiveMessage(bob3, outputA2) // Alice --- tx_complete --> Bob - val sharedTxA = receiveFinalMessage(alice4, txCompleteB) + val (alice5, sharedTxA) = receiveFinalMessage(alice4, txCompleteB) assertNotNull(sharedTxA.txComplete) - val sharedTxB = receiveFinalMessage(bob4, sharedTxA.txComplete!!) + val (bob5, sharedTxB) = receiveFinalMessage(bob4, sharedTxA.txComplete!!) assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared input and the shared output. @@ -331,24 +395,24 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxA.sharedTx.totalAmountIn, 315_000.sat) assertNotNull(sharedTxA.sharedTx.sharedInput) - assertEquals(sharedTxA.sharedTx.localFees, 1_116_000.msat) - assertEquals(sharedTxA.sharedTx.remoteFees, 516_000.msat) + assertEquals(sharedTxA.sharedTx.localFees, 957_000.msat) + assertEquals(sharedTxA.sharedTx.remoteFees, 357_000.msat) assertNotNull(sharedTxB.sharedTx.sharedInput) - assertEquals(sharedTxB.sharedTx.localFees, 516_000.msat) - assertEquals(sharedTxB.sharedTx.remoteFees, 1_116_000.msat) + assertEquals(sharedTxB.sharedTx.localFees, 357_000.msat) + assertEquals(sharedTxB.sharedTx.remoteFees, 957_000.msat) // Bob sends signatures first as he contributed less than Alice. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) assertNotNull(signedTxB) - assertEquals(signedTxB.localSigs.swapInUserSigs.size, 1) - assertEquals(signedTxB.localSigs.swapInServerSigs.size, 1) + assertEquals(signedTxB.localSigs.swapInUserPartialSigs.size, 1) + assertEquals(signedTxB.localSigs.swapInServerPartialSigs.size, 1) assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid and has the right feerate. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) - assertEquals(signedTxA.localSigs.swapInUserSigs.size, 1) - assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) + assertEquals(signedTxA.localSigs.swapInUserPartialSigs.size, 1) + assertEquals(signedTxA.localSigs.swapInServerPartialSigs.size, 1) assertNotNull(signedTxA.localSigs.previousFundingTxSig) val signedTx = signedTxA.signedTx assertEquals(signedTxA.localSigs.txId, signedTx.txid) @@ -375,8 +439,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_add_output --- Bob @@ -390,9 +454,9 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice <-- tx_complete --- Bob val (bob3, _) = receiveMessage(bob2, outputA2) // Alice --- tx_complete --> Bob - val sharedTxA = receiveFinalMessage(alice3, txCompleteB) + val sharedTxA = receiveFinalMessage(alice3, txCompleteB).second assertNotNull(sharedTxA.txComplete) - val sharedTxB = receiveFinalMessage(bob3, sharedTxA.txComplete!!) + val sharedTxB = receiveFinalMessage(bob3, sharedTxA.txComplete!!).second assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared input and the shared output. @@ -412,7 +476,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 1_000_000.msat) // Bob sends signatures first as he did not contribute. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) assertNotNull(signedTxB) assertTrue(signedTxB.localSigs.witnesses.isEmpty()) assertTrue(signedTxB.localSigs.swapInUserSigs.isEmpty()) @@ -420,7 +484,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertTrue(signedTxA.localSigs.witnesses.isEmpty()) assertTrue(signedTxA.localSigs.swapInUserSigs.isEmpty()) @@ -448,8 +512,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_add_output --- Bob @@ -471,9 +535,9 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice <-- tx_complete --- Bob val (bob5, _) = receiveMessage(bob4, outputA4) // Alice --- tx_complete --> Bob - val sharedTxA = receiveFinalMessage(alice5, txCompleteB) + val sharedTxA = receiveFinalMessage(alice5, txCompleteB).second assertNotNull(sharedTxA.txComplete) - val sharedTxB = receiveFinalMessage(bob5, sharedTxA.txComplete!!) + val sharedTxB = receiveFinalMessage(bob5, sharedTxA.txComplete!!).second assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared input and the shared output. @@ -492,13 +556,13 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 1_000_000.msat) // Bob sends signatures first as he did not contribute. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) assertNotNull(signedTxB) assertTrue(signedTxB.localSigs.swapInUserSigs.isEmpty()) assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertTrue(signedTxA.localSigs.swapInUserSigs.isEmpty()) assertNotNull(signedTxA.localSigs.previousFundingTxSig) @@ -526,8 +590,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -549,9 +613,9 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice <-- tx_complete --- Bob val (bob5, _) = receiveMessage(bob4, outputA3) // Alice --- tx_complete --> Bob - val sharedTxA = receiveFinalMessage(alice5, txCompleteB) + val sharedTxA = receiveFinalMessage(alice5, txCompleteB).second assertNotNull(sharedTxA.txComplete) - val sharedTxB = receiveFinalMessage(bob5, sharedTxA.txComplete!!) + val (bob6, sharedTxB) = receiveFinalMessage(bob5, sharedTxA.txComplete!!) assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared input and the shared output. @@ -562,24 +626,24 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxA.sharedTx.totalAmountIn, 375_000.sat) assertNotNull(sharedTxA.sharedTx.sharedInput) - assertEquals(sharedTxA.sharedTx.localFees, 1_240_000.msat) - assertEquals(sharedTxA.sharedTx.remoteFees, 640_000.msat) + assertEquals(sharedTxA.sharedTx.localFees, 1_081_000.msat) + assertEquals(sharedTxA.sharedTx.remoteFees, 481_000.msat) assertNotNull(sharedTxB.sharedTx.sharedInput) - assertEquals(sharedTxB.sharedTx.localFees, 640_000.msat) - assertEquals(sharedTxB.sharedTx.remoteFees, 1_240_000.msat) + assertEquals(sharedTxB.sharedTx.localFees, 481_000.msat) + assertEquals(sharedTxB.sharedTx.remoteFees, 1_081_000.msat) // Bob sends signatures first as he did not contribute. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob6, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) assertNotNull(signedTxB) - assertEquals(signedTxB.localSigs.swapInUserSigs.size, 1) - assertEquals(signedTxB.localSigs.swapInServerSigs.size, 1) + assertEquals(signedTxB.localSigs.swapInUserPartialSigs.size, 1) + assertEquals(signedTxB.localSigs.swapInServerPartialSigs.size, 1) assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) - assertEquals(signedTxA.localSigs.swapInUserSigs.size, 1) - assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) + assertEquals(signedTxA.localSigs.swapInUserPartialSigs.size, 1) + assertEquals(signedTxA.localSigs.swapInServerPartialSigs.size, 1) assertNotNull(signedTxA.localSigs.previousFundingTxSig) val signedTx = signedTxA.signedTx assertEquals(signedTx.txIn.size, 3) @@ -593,11 +657,15 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `remove input - output`() { - val f = createFixture(100_000.sat, listOf(150_000.sat), 0.sat, listOf(), FeeratePerKw(2500.sat), 330.sat, 0) + val f = createFixture(100_000.sat, listOf(150_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(2500.sat), 330.sat, 0) // In this flow we introduce dummy inputs/outputs from Bob to Alice that are then removed. - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) - // Alice --- tx_add_input --> Bob + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), FundingContributions(listOf(), listOf())) + + // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) + val (_, txCompleteB) = receiveMessage(bob0, inputA) + // Alice <-- tx_add_input --- Bob val inputB = TxAddInput(f.channelId, 1, Transaction(2, listOf(), listOf(TxOut(250_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0), 0, 0u) // Alice --- tx_add_output --> Bob @@ -615,7 +683,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice --- tx_complete --> Bob val (alice5, _) = receiveMessage(alice4, remoteOutputB) // Alice <-- tx_complete --- Bob - val sharedTxA = receiveFinalMessage(alice5, TxComplete(f.channelId)) + val sharedTxA = receiveFinalMessage(alice5, txCompleteB).second assertNull(sharedTxA.txComplete) assertEquals(sharedTxA.sharedTx.remoteAmountIn, 0.msat) @@ -671,7 +739,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { TxOut(2500.sat, Script.pay2wpkh(randomKey().publicKey())), ) val previousTx = Transaction(2, listOf(), previousOutputs, 0) - val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) + val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val testCases = mapOf( TxAddInput(f.channelId, 0, previousTx, 0, 0U) to InteractiveTxSessionAction.InvalidSerialId(f.channelId, 0), TxAddInput(f.channelId, 1, previousTx, 0, 0U) to InteractiveTxSessionAction.DuplicateSerialId(f.channelId, 1), @@ -682,7 +750,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { TxAddInput(f.channelId, 9, previousTx, 2, 0xffffffffU) to InteractiveTxSessionAction.NonReplaceableInput(f.channelId, 9, previousTx.txid, 2, 0xffffffff), ) testCases.forEach { (input, expected) -> - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) // Alice --- tx_add_input --> Bob val (alice1, _) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -695,14 +763,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `allow all output types`() { - val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) + val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val testCases = listOf( TxAddOutput(f.channelId, 1, 25_000.sat, Script.write(Script.pay2pkh(randomKey().publicKey())).byteVector()), TxAddOutput(f.channelId, 1, 25_000.sat, Script.write(Script.pay2sh(listOf(OP_1))).byteVector()), TxAddOutput(f.channelId, 1, 25_000.sat, Script.write(listOf(OP_1)).byteVector()), ) testCases.forEach { output -> - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) // Alice --- tx_add_input --> Bob val (alice1, _) = sendMessage(alice0) // Alice <-- tx_add_output --- Bob @@ -714,7 +782,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `invalid output`() { - val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) + val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() val testCases = mapOf( TxAddOutput(f.channelId, 0, 25_000.sat, validScript) to InteractiveTxSessionAction.InvalidSerialId(f.channelId, 0), @@ -722,7 +790,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { TxAddOutput(f.channelId, 3, 329.sat, validScript) to InteractiveTxSessionAction.OutputBelowDust(f.channelId, 3, 329.sat, 330.sat), ) testCases.forEach { (output, expected) -> - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) // Alice --- tx_add_input --> Bob val (alice1, _) = sendMessage(alice0) // Alice <-- tx_add_output --- Bob @@ -735,7 +803,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `remove unknown input - output`() { - val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) + val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val testCases = mapOf( TxRemoveOutput(f.channelId, 52) to InteractiveTxSessionAction.InvalidSerialId(f.channelId, 52), TxRemoveOutput(f.channelId, 53) to InteractiveTxSessionAction.UnknownSerialId(f.channelId, 53), @@ -743,7 +811,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { TxRemoveInput(f.channelId, 57) to InteractiveTxSessionAction.UnknownSerialId(f.channelId, 57), ) testCases.forEach { (msg, expected) -> - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) // Alice --- tx_add_input --> Bob val (alice1, _) = sendMessage(alice0) // Alice <-- tx_remove_(in|out)put --- Bob @@ -754,9 +822,9 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `too many protocol rounds`() { - val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) + val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() - var (alice, _) = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA).send() + var (alice, _) = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA).send() (1..InteractiveTxSession.MAX_INPUTS_OUTPUTS_RECEIVED).forEach { i -> // Alice --- tx_message --> Bob val (alice1, _) = alice.receive(TxAddOutput(f.channelId, 2 * i.toLong() + 1, 2500.sat, validScript)) @@ -768,8 +836,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `too many inputs`() { - val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - var (alice, _) = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA).send() + val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) + var (alice, _) = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA).send() (1..252).forEach { i -> // Alice --- tx_message --> Bob val (alice1, _) = alice.receive(createTxAddInput(f.channelId, 2 * i.toLong() + 1, 5000.sat)) @@ -784,11 +852,10 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `too many outputs`() { - val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - var (alice, _) = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA).send() + val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) + var (alice, _) = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA).send() val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() (1..252).forEach { i -> - // Alice --- tx_message --> Bob // Alice --- tx_message --> Bob val (alice1, _) = alice.receive(TxAddOutput(f.channelId, 2 * i.toLong() + 1, 2500.sat, validScript)) alice = alice1 @@ -802,9 +869,9 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `missing funding output`() { - val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) + val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.nodeIdB, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) // Alice --- tx_add_output --> Bob @@ -816,8 +883,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `multiple funding outputs`() { - val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) // Alice --- tx_add_output --> Bob @@ -835,7 +902,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val spliceOutputA = TxOut(20_000.sat, Script.pay2wpkh(randomKey().publicKey())) val subtractedFundingA = 25_000.sat val f = createSpliceFixture(balanceA, -subtractedFundingA, listOf(), listOf(spliceOutputA), 0.msat, 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, balanceA, emptySet(), f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_output --> Bob val (bob1, _) = receiveMessage(bob0, TxAddOutput(f.channelId, 0, 75_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) // Alice --- tx_add_output --> Bob @@ -847,9 +914,9 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `swap-in input missing user key`() { - val f = createFixture(100_000.sat, listOf(150_000.sat), 0.sat, listOf(), FeeratePerKw(2500.sat), 330.sat, 0) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val f = createFixture(100_000.sat, listOf(), listOf(150_000.sat), 0.sat, listOf(), listOf(), FeeratePerKw(2500.sat), 330.sat, 0) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_complete --- Bob @@ -863,22 +930,44 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice <-- tx_complete --- Bob val (bob3, _) = receiveMessage(bob2, outputA2) // Alice --- tx_complete --> Bob - val sharedTxA = receiveFinalMessage(alice3, txCompleteB) + val sharedTxA = receiveFinalMessage(alice3, txCompleteB).second assertNotNull(sharedTxA.txComplete) - val sharedTxB = receiveFinalMessage(bob3, sharedTxA.txComplete!!) + val sharedTxB = receiveFinalMessage(bob3, sharedTxA.txComplete!!).second assertNull(sharedTxB.txComplete) // Alice didn't send her user key, so Bob thinks there aren't any swap inputs - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) assertTrue(signedTxB.localSigs.swapInServerSigs.isEmpty()) // Alice is unable to sign her input since Bob didn't provide his signature. - assertNull(sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs)) + assertNull(sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs)) + } + + @Test + fun `swap-in input missing user nonce`() { + val f = createFixture(100_000.sat, listOf(150_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(2500.sat), 330.sat, 0) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + // Alice --- tx_add_input --> Bob + val (alice1, inputA) = sendMessage(alice0) + // Alice <-- tx_complete --- Bob + val (bob1, txCompleteB) = receiveMessage(bob0, inputA.copy(tlvs = TlvStream.empty())) + // Alice --- tx_add_output --> Bob + val (alice2, outputA1) = receiveMessage(alice1, txCompleteB) + // Alice <-- tx_complete --- Bob + val (bob2, txCompleteB1) = receiveMessage(bob1, outputA1) + // Alice --- tx_add_output --> Bob + val (alice3, outputA2) = receiveMessage(alice2, txCompleteB1) + // Alice <-- tx_complete --- Bob + val (_, txCompleteB2) = receiveMessage(bob2, outputA2) + // Alice --- tx_complete --> Bob + val (_, missingNonce) = alice3.receive(txCompleteB2) + assertIs(missingNonce) } @Test fun `invalid funding amount`() { - val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) // Alice --- tx_add_output --> Bob @@ -899,8 +988,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_complete --- Bob @@ -938,8 +1027,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `missing previous tx`() { - val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_output --> Bob val failure = receiveInvalidMessage(bob0, TxAddInput(f.channelId, 0, null, 3, 0u)) assertIs(failure) @@ -947,8 +1036,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `total input amount too low`() { - val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) @@ -963,8 +1052,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `minimum fee not met`() { - val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) @@ -980,10 +1069,10 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `previous attempts not double-spent`() { - val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) + val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val sharedOutput = InteractiveTxOutput.Shared(0, f.fundingParamsA.fundingPubkeyScript(f.channelKeysA), 100_000_000.msat, 0.msat, 0.msat) val previousTx1 = Transaction(2, listOf(), listOf(TxOut(150_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0) - val previousTx2 = Transaction(2, listOf(), listOf(TxOut(160_000.sat, Script.pay2wpkh(randomKey().publicKey())), TxOut(175_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0) + val previousTx2 = Transaction(2, listOf(), listOf(TxOut(160_000.sat, Script.pay2wpkh(randomKey().publicKey())), TxOut(200_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() val firstAttempt = FullySignedSharedTransaction( SharedTransaction(null, sharedOutput, listOf(), listOf(InteractiveTxInput.RemoteOnly(2, OutPoint(previousTx1, 0), TxOut(125_000.sat, validScript), 0u)), listOf(), listOf(), 0), @@ -995,7 +1084,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { SharedTransaction(null, sharedOutput, listOf(), firstAttempt.tx.remoteInputs + listOf(InteractiveTxInput.RemoteOnly(4, OutPoint(previousTx2, 1), TxOut(150_000.sat, validScript), 0u)), listOf(), listOf(), 0), TxSignatures(f.channelId, TxId(randomBytes32()), listOf()), ) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB, listOf(firstAttempt, secondAttempt)) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB, listOf(firstAttempt, secondAttempt)) // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, TxAddInput(f.channelId, 4, previousTx2, 1, 0u)) // Alice --- tx_add_output --> Bob @@ -1049,14 +1138,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { ByteVector("82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87") ) ) - val initiatorSigs = TxSignatures(channelId, unsignedTx, listOf(initiatorWitness), null, listOf(), listOf()) + val initiatorSigs = TxSignatures(channelId, unsignedTx, listOf(initiatorWitness), null, listOf(), listOf(), listOf(), listOf()) val nonInitiatorWitness = ScriptWitness( listOf( ByteVector("304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01"), ByteVector("034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484") ) ) - val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, listOf(nonInitiatorWitness), null, listOf(), listOf()) + val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, listOf(nonInitiatorWitness), null, listOf(), listOf(), listOf(), listOf()) val initiatorSignedTx = FullySignedSharedTransaction(initiatorTx, initiatorSigs, nonInitiatorSigs, null) assertEquals(initiatorSignedTx.feerate, FeeratePerKw(262.sat)) val nonInitiatorSignedTx = FullySignedSharedTransaction(nonInitiatorTx, nonInitiatorSigs, initiatorSigs, null) @@ -1081,13 +1170,18 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val localParamsB: LocalParams, val fundingParamsB: InteractiveTxParams, val fundingContributionsB: FundingContributions - ) + ) { + val nodeIdA = localParamsA.nodeId + val nodeIdB = localParamsB.nodeId + } private fun createFixture( fundingAmountA: Satoshi, utxosA: List, + legacyUtxosA: List, fundingAmountB: Satoshi, utxosB: List, + legacyUtxosB: List, targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long @@ -1104,10 +1198,10 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val fundingPubkeyB = channelKeysB.fundingPubKey(fundingTxIndex) val fundingParamsA = InteractiveTxParams(channelId, true, fundingAmountA, fundingAmountB, fundingPubkeyB, lockTime, dustLimit, targetFeerate) val fundingParamsB = InteractiveTxParams(channelId, false, fundingAmountB, fundingAmountA, fundingPubkeyA, lockTime, dustLimit, targetFeerate) - val walletA = createWallet(swapInKeysA, utxosA) + val walletA = createWallet(swapInKeysA, utxosA, legacyUtxosA) val contributionsA = FundingContributions.create(channelKeysA, swapInKeysA, fundingParamsA, null, walletA, listOf(), randomKey().publicKey()) assertNotNull(contributionsA.right) - val walletB = createWallet(swapInKeysB, utxosB) + val walletB = createWallet(swapInKeysB, utxosB, legacyUtxosB) val contributionsB = FundingContributions.create(channelKeysB, swapInKeysB, fundingParamsB, null, walletB, listOf(), randomKey().publicKey()) assertNotNull(contributionsB.right) return Fixture(channelId, TestConstants.Alice.keyManager, channelKeysA, localParamsA, fundingParamsA, contributionsA.right!!, TestConstants.Bob.keyManager, channelKeysB, localParamsB, fundingParamsB, contributionsB.right!!) @@ -1200,11 +1294,11 @@ class InteractiveTxTestsCommon : LightningTestSuite() { return Pair(receiver1, action1.msg as M) } - private fun receiveFinalMessage(receiver: InteractiveTxSession, msg: TxComplete): InteractiveTxSessionAction.SignSharedTx { + private fun receiveFinalMessage(receiver: InteractiveTxSession, msg: TxComplete): Pair { val (receiver1, action1) = receiver.receive(msg) assertIs(action1) assertTrue(receiver1.isComplete) - return action1 + return Pair(receiver1, action1) } private fun receiveInvalidMessage(receiver: InteractiveTxSession, msg: InteractiveTxConstructionMessage): InteractiveTxSessionAction.RemoteFailure { @@ -1213,10 +1307,15 @@ class InteractiveTxTestsCommon : LightningTestSuite() { return action1 } - private fun createWallet(onChainKeys: KeyManager.SwapInOnChainKeys, amounts: List): List { + private fun createWallet(onChainKeys: KeyManager.SwapInOnChainKeys, amounts: List, legacyAmounts: List = listOf()): List { return amounts.map { amount -> val txIn = listOf(TxIn(OutPoint(TxId(randomBytes32()), 2), 0)) - val txOut = listOf(TxOut(amount, onChainKeys.pubkeyScript), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) + val txOut = listOf(TxOut(amount, onChainKeys.swapInProtocol.pubkeyScript), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) + val parentTx = Transaction(2, txIn, txOut, 0) + WalletState.Utxo(parentTx.txid, 0, 0, parentTx) + } + legacyAmounts.map { amount -> + val txIn = listOf(TxIn(OutPoint(TxId(randomBytes32()), 2), 0)) + val txOut = listOf(TxOut(amount, onChainKeys.legacySwapInProtocol.pubkeyScript), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) val parentTx = Transaction(2, txIn, txOut, 0) WalletState.Utxo(parentTx.txid, 0, 0, parentTx) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt index 0918082fa..1cdb20ebd 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -410,7 +410,7 @@ object TestsHelper { } fun createWallet(keyManager: KeyManager, amount: Satoshi): Pair> { - val (privateKey, script) = keyManager.swapInOnChainWallet.run { Pair(userPrivateKey, pubkeyScript) } + val (privateKey, script) = keyManager.swapInOnChainWallet.run { Pair(userPrivateKey, swapInProtocol.pubkeyScript) } val parentTx = Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 3), 0)), listOf(TxOut(amount, script)), 0) return privateKey to listOf(WalletState.Utxo(parentTx.txid, 0, 42, parentTx)) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt index 6d92d0c9b..317ff8650 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt @@ -504,7 +504,7 @@ class QuiescenceTestsCommon : LightningTestSuite() { companion object { private fun createWalletWithFunds(keyManager: KeyManager, utxos: List): List { - val script = keyManager.swapInOnChainWallet.pubkeyScript + val script = keyManager.swapInOnChainWallet.swapInProtocol.pubkeyScript return utxos.map { amount -> val txIn = listOf(TxIn(OutPoint(TxId(Lightning.randomBytes32()), 2), 0)) val txOut = listOf(TxOut(amount, script), TxOut(150.sat, Script.pay2wpkh(Lightning.randomKey().publicKey()))) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt index edf6bc3b3..479bf035b 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -1507,7 +1507,7 @@ class SpliceTestsCommon : LightningTestSuite() { } private fun createWalletWithFunds(keyManager: KeyManager, amounts: List): List { - val script = keyManager.swapInOnChainWallet.pubkeyScript + val script = keyManager.swapInOnChainWallet.legacySwapInProtocol.pubkeyScript return amounts.map { amount -> val txIn = listOf(TxIn(OutPoint(TxId(Lightning.randomBytes32()), 2), 0)) val txOut = listOf(TxOut(amount, script), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt index e2c860b62..02fb630d8 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt @@ -443,7 +443,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { val previousFundingTx = alice.state.latestFundingTx.sharedTx assertIs(previousFundingTx) // Alice adds a new input that increases her contribution and covers the additional fees. - val script = alice.staticParams.nodeParams.keyManager.swapInOnChainWallet.pubkeyScript + val script = alice.staticParams.nodeParams.keyManager.swapInOnChainWallet.legacySwapInProtocol.pubkeyScript val parentTx = Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 1), 0)), listOf(TxOut(30_000.sat, script)), 0) val wallet1 = wallet + listOf(WalletState.Utxo(parentTx.txid, 0, 42, parentTx)) return ChannelCommand.Funding.BumpFundingFee(previousFundingTx.feerate * 1.1, previousFundingParams.localContribution + 20_000.sat, wallet1, previousFundingTx.tx.lockTime + 1) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt index 9bd0811a5..058dff5df 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt @@ -2,11 +2,13 @@ package fr.acinq.lightning.crypto import fr.acinq.bitcoin.* import fr.acinq.bitcoin.crypto.Pack +import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.NodeParams 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 @@ -182,28 +184,53 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { assertEquals(TestConstants.Bob.keyManager.swapInOnChainWallet.remoteServerPublicKey, PublicKey.fromHex("02d8c2f4fe8a017ff3a30eb2a4477f3ebe64ae930f67f907270712a70b18cb8951")) assertEquals( "wsh(and_v(v:pk([14620948/51h/0h/0h]tpubDCvYeHUZisCMV3h1zPevPWQmNPfA3g3vnu7gDqskXVCbJB1VKk2F7LApV6TTdm1sCyGout8ma27CCHvYTuMZxpwrcHnLwL4kaXW8z2KfFcW),or_d(pk(0256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fd),older(25920))))", - TestConstants.Alice.keyManager.swapInOnChainWallet.descriptor + TestConstants.Alice.keyManager.swapInOnChainWallet.legacyDescriptor ) assertEquals( "wsh(and_v(v:pk([85185511/51h/0h/0h]tpubDDt5vQap1awkteTeYioVGLQvj75xrFvjuW6WjNumsedvckEHAMUACubuKtmjmXViDPYMvtnEQt6EGj3eeMVSGRKxRZqCme37j5jAUMhkX5L),or_d(pk(02d8c2f4fe8a017ff3a30eb2a4477f3ebe64ae930f67f907270712a70b18cb8951),older(25920))))", - TestConstants.Bob.keyManager.swapInOnChainWallet.descriptor + TestConstants.Bob.keyManager.swapInOnChainWallet.legacyDescriptor ) } @Test fun `spend swap-in transactions`() { - val swapInTx = Transaction(version = 2, + val swapInTx = Transaction( + version = 2, txIn = listOf(), txOut = listOf( - TxOut(Satoshi(100000), Bitcoin.addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, TestConstants.Alice.keyManager.swapInOnChainWallet.address).result!!), - TxOut(Satoshi(150000), Bitcoin.addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, TestConstants.Alice.keyManager.swapInOnChainWallet.address).result!!) + TxOut(Satoshi(100000), TestConstants.Alice.keyManager.swapInOnChainWallet.legacySwapInProtocol.pubkeyScript), + TxOut(Satoshi(150000), TestConstants.Alice.keyManager.swapInOnChainWallet.legacySwapInProtocol.pubkeyScript), + TxOut(Satoshi(150000), Script.pay2wpkh(randomKey().publicKey())), + TxOut(Satoshi(100000), TestConstants.Alice.keyManager.swapInOnChainWallet.swapInProtocol.pubkeyScript), + 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(swapInTx.txOut.size, recoveryTx.txIn.size) + 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"))) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index c82be7b9c..eccfc4e4d 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -7,9 +7,9 @@ import fr.acinq.bitcoin.Script.pay2wpkh import fr.acinq.bitcoin.Script.pay2wsh import fr.acinq.bitcoin.Script.write import fr.acinq.bitcoin.crypto.Pack +import fr.acinq.bitcoin.crypto.musig2.Musig2 import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta -import fr.acinq.lightning.Lightning.randomBytes import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.blockchain.fee.FeeratePerKw @@ -51,6 +51,7 @@ import fr.acinq.lightning.transactions.Transactions.makeHtlcTxs import fr.acinq.lightning.transactions.Transactions.makeMainPenaltyTx import fr.acinq.lightning.transactions.Transactions.sign import fr.acinq.lightning.transactions.Transactions.swapInputWeight +import fr.acinq.lightning.transactions.Transactions.swapInputWeightLegacy import fr.acinq.lightning.transactions.Transactions.weight2fee import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.UpdateAddHtlc @@ -442,12 +443,12 @@ class TransactionsTestsCommon : LightningTestSuite() { } @Test - fun `spend 2-of-2 swap-in`() { + fun `spend 2-of-2 legacy swap-in`() { val userWallet = TestConstants.Alice.keyManager.swapInOnChainWallet val swapInTx = Transaction( version = 2, txIn = listOf(TxIn(OutPoint(TxId(randomBytes32()), 2), 0)), - txOut = listOf(TxOut(100_000.sat, userWallet.pubkeyScript)), + txOut = listOf(TxOut(100_000.sat, userWallet.legacySwapInProtocol.pubkeyScript)), lockTime = 0 ) // The transaction can be spent if the user and the server produce a signature. @@ -458,10 +459,10 @@ class TransactionsTestsCommon : LightningTestSuite() { txOut = listOf(TxOut(90_000.sat, pay2wpkh(randomKey().publicKey()))), lockTime = 0 ) - val userSig = Transactions.signSwapInputUser(fundingTx, 0, swapInTx.txOut.first(), userWallet.userPrivateKey, userWallet.remoteServerPublicKey, userWallet.refundDelay) - val serverWallet = TestConstants.Bob.keyManager.swapInOnChainWallet - val serverSig = Transactions.signSwapInputServer(fundingTx, 0, swapInTx.txOut.first(), userWallet.userPublicKey, serverWallet.localServerPrivateKey(TestConstants.Alice.nodeParams.nodeId), serverWallet.refundDelay) - val witness = Scripts.witnessSwapIn2of2(userSig, userWallet.userPublicKey, serverSig, userWallet.remoteServerPublicKey, userWallet.refundDelay) + val userSig = userWallet.signSwapInputUserLegacy(fundingTx, 0, swapInTx.txOut) + val serverKey = TestConstants.Bob.keyManager.swapInOnChainWallet.localServerPrivateKey(TestConstants.Alice.nodeParams.nodeId) + val serverSig = userWallet.legacySwapInProtocol.signSwapInputServer(fundingTx, 0, swapInTx.txOut.first(), serverKey) + val witness = userWallet.legacySwapInProtocol.witness(userSig, serverSig) val signedTx = fundingTx.updateWitness(0, witness) Transaction.correctlySpends(signedTx, listOf(swapInTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @@ -473,21 +474,90 @@ class TransactionsTestsCommon : LightningTestSuite() { txOut = listOf(TxOut(90_000.sat, pay2wpkh(randomKey().publicKey()))), lockTime = 0 ) - val userSig = Transactions.signSwapInputUser(fundingTx, 0, swapInTx.txOut.first(), userWallet.userPrivateKey, userWallet.remoteServerPublicKey, userWallet.refundDelay) - val witness = Scripts.witnessSwapIn2of2Refund(userSig, userWallet.userPublicKey, userWallet.remoteServerPublicKey, userWallet.refundDelay) + val userSig = userWallet.signSwapInputUserLegacy(fundingTx, 0, swapInTx.txOut) + val witness = userWallet.legacySwapInProtocol.witnessRefund(userSig) val signedTx = fundingTx.updateWitness(0, witness) Transaction.correctlySpends(signedTx, listOf(swapInTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } } + @Test + fun `spend 2-of-2 swap-in taproot-musig2 version`() { + val userPrivateKey = PrivateKey(ByteArray(32) { 1 }) + val serverPrivateKey = PrivateKey(ByteArray(32) { 2 }) + val refundDelay = 25920 + + val mnemonics = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".split(" ") + val seed = MnemonicCode.toSeed(mnemonics, "") + val masterPrivateKey = DeterministicWallet.derivePrivateKey(DeterministicWallet.generate(seed), "/51'/0'/0'").copy(path = KeyPath.empty) + val userRefundPrivateKey = DeterministicWallet.derivePrivateKey(masterPrivateKey, "0").privateKey + val swapInProtocol = SwapInProtocol(userPrivateKey.publicKey(), serverPrivateKey.publicKey(), userRefundPrivateKey.publicKey(), refundDelay) + + val swapInTx = Transaction( + version = 2, + txIn = listOf(), + txOut = listOf(TxOut(Satoshi(10000), swapInProtocol.pubkeyScript)), + lockTime = 0 + ) + + // The transaction can be spent if the user and the server produce a signature. + run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = TxIn.SEQUENCE_FINAL)), + txOut = listOf(TxOut(Satoshi(10000), pay2wpkh(PrivateKey(randomBytes32()).publicKey()))), + lockTime = 0 + ) + // The first step of a musig2 signing session is to exchange nonces. + // If participants are disconnected before the end of the signing session, they must start again with fresh nonces. + val userNonce = Musig2.generateNonce(randomBytes32(), userPrivateKey, listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey())) + val serverNonce = Musig2.generateNonce(randomBytes32(), serverPrivateKey, listOf(serverPrivateKey.publicKey(), userPrivateKey.publicKey())) + + // Once they have each other's public nonce, they can produce partial signatures. + val userSig = swapInProtocol.signSwapInputUser(tx, 0, swapInTx.txOut, userPrivateKey, userNonce.first, userNonce.second, serverNonce.second).right!! + val serverSig = swapInProtocol.signSwapInputServer(tx, 0, swapInTx.txOut, serverPrivateKey, serverNonce.first, userNonce.second, serverNonce.second).right!! + + // Once they have each other's partial signature, they can aggregate them into a valid signature. + val witness = swapInProtocol.witness(tx, 0, swapInTx.txOut, userNonce.second, serverNonce.second, userSig, serverSig).right + assertNotNull(witness) + val signedTx = tx.updateWitness(0, witness) + Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + + // Or it can be spent with only the user's signature, after a delay. + run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = refundDelay.toLong())), + txOut = listOf(TxOut(Satoshi(10000), pay2wpkh(PrivateKey(randomBytes32()).publicKey()))), + lockTime = 0 + ) + val sig = swapInProtocol.signSwapInputRefund(tx, 0, swapInTx.txOut, userRefundPrivateKey) + val signedTx = tx.updateWitness(0, swapInProtocol.witnessRefund(sig)) + Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + } + @Test fun `swap-in input weight`() { val pubkey = randomKey().publicKey() // DER-encoded ECDSA signatures usually take up to 72 bytes. - val sig = randomBytes(72).toByteVector() + val sig = ByteVector64.fromValidHex("90b658d172a51f1b3f1a2becd30942397f5df97da8cd2c026854607e955ad815ccfd87d366e348acc32aaf15ff45263aebbb7ecc913a0e5999133f447aee828c") + val tx = Transaction(2, listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 2), 0)), listOf(TxOut(50_000.sat, pay2wpkh(pubkey))), 0) + val swapInProtocol = SwapInProtocolLegacy(pubkey, pubkey, 144) + val witness = swapInProtocol.witness(sig, sig) + val swapInput = TxIn(OutPoint(TxId(ByteVector32.Zeroes), 3), ByteVector.empty, 0, witness) + val txWithAdditionalInput = tx.copy(txIn = tx.txIn + listOf(swapInput)) + val inputWeight = txWithAdditionalInput.weight() - tx.weight() + assertEquals(inputWeight, swapInputWeightLegacy) + } + + @Test + fun `swap-in input weight -- musig2 version`() { + val pubkey = randomKey().publicKey() + val sig = ByteVector64.fromValidHex("90b658d172a51f1b3f1a2becd30942397f5df97da8cd2c026854607e955ad815ccfd87d366e348acc32aaf15ff45263aebbb7ecc913a0e5999133f447aee828c") val tx = Transaction(2, listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 2), 0)), listOf(TxOut(50_000.sat, pay2wpkh(pubkey))), 0) - val redeemScript = Scripts.swapIn2of2(pubkey, pubkey, 144) - val witness = ScriptWitness(listOf(sig, sig, write(redeemScript).byteVector())) + val witness = Script.witnessKeyPathPay2tr(sig) val swapInput = TxIn(OutPoint(TxId(ByteVector32.Zeroes), 3), ByteVector.empty, 0, witness) val txWithAdditionalInput = tx.copy(txIn = tx.txIn + listOf(swapInput)) val inputWeight = txWithAdditionalInput.weight() - tx.weight() diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index b36c02e37..acb976052 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.lightning.* @@ -383,11 +384,22 @@ class LightningCodecsTestsCommon : LightningTestSuite() { val channelId2 = ByteVector32("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") val swapInUserKey = PublicKey.fromHex("03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b") val swapInServerKey = PublicKey.fromHex("03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f") + val swapInUserRefundKey = PublicKey.fromHex("033a47288cdae4b25818d0d82802bc114c6f7184f2e071602fa4e3d69881ae2cce") val swapInRefundDelay = 144 - val swapInSignatures = listOf( + val legacySwapInSignatures = listOf( ByteVector64("c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3"), ByteVector64("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), ) + val nonces = listOf( + IndividualNonce("03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7"), + IndividualNonce("031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04"), + IndividualNonce("02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a"), + IndividualNonce("036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + ) + val swapInPartialSignatures = listOf( + TxSignaturesTlv.PartialSignature(ByteVector32("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), nonces[0], nonces[1]), + TxSignaturesTlv.PartialSignature(ByteVector32("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), nonces[2], nonces[3]) + ) val signature = ByteVector64("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") // This is a random mainnet transaction. val tx1 = Transaction.read( @@ -403,20 +415,25 @@ class LightningCodecsTestsCommon : LightningTestSuite() { TxAddInput(channelId2, 0, tx2, 2, 0u) to ByteVector("0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000"), TxAddInput(channelId1, 561, tx1, 0, 0xfffffffdu) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 fffffffd"), TxAddInput(channelId1, 561, OutPoint(tx1, 1), 5u) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 00000005 fd0451201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106"), - TxAddInput(channelId1, 561, tx1, 1, 5u, TlvStream(TxAddInputTlv.SwapInParams(swapInUserKey, swapInServerKey, swapInRefundDelay))) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 fd04534603462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f00000090"), + TxAddInput(channelId1, 561, tx1, 1, 5u, TlvStream(TxAddInputTlv.SwapInParamsLegacy(swapInUserKey, swapInServerKey, swapInRefundDelay))) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 fd04534603462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f00000090"), + TxAddInput(channelId1, 561, tx1, 1, 5u, TlvStream(TxAddInputTlv.SwapInParams(swapInUserKey, swapInServerKey, swapInUserRefundKey, swapInRefundDelay))) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 fd04556703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f033a47288cdae4b25818d0d82802bc114c6f7184f2e071602fa4e3d69881ae2cce00000090"), TxAddOutput(channelId1, 1105, 2047.sat, ByteVector("00149357014afd0ccd265658c9ae81efa995e771f472")) to ByteVector("0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472"), TxRemoveInput(channelId2, 561) to ByteVector("0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231"), TxRemoveOutput(channelId1, 1) to ByteVector("0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001"), TxComplete(channelId1) to ByteVector("0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), - TxSignatures(channelId1, tx2, listOf(ScriptWitness(listOf(ByteVector("68656c6c6f2074686572652c2074686973206973206120626974636f6e212121"), ByteVector("82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87"))), ScriptWitness(listOf(ByteVector("304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01"), ByteVector("034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484")))), null, listOf(), listOf()) to ByteVector("0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"), - TxSignatures(channelId2, tx1, listOf(), null, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000"), - TxSignatures(channelId2, tx1, listOf(), null, swapInSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), null, listOf(), swapInSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), null, swapInSignatures.take(1), swapInSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), - TxSignatures(channelId2, tx1, listOf(), signature, swapInSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), swapInSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, swapInSignatures.take(1), swapInSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId1, tx2, listOf(ScriptWitness(listOf(ByteVector("68656c6c6f2074686572652c2074686973206973206120626974636f6e212121"), ByteVector("82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87"))), ScriptWitness(listOf(ByteVector("304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01"), ByteVector("034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484")))), null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"), + TxSignatures(channelId2, tx1, listOf(), null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000"), + TxSignatures(channelId2, tx1, listOf(), null, legacySwapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), null, listOf(), legacySwapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), null, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), legacySwapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), swapInPartialSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd0261 fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5 fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), TxInitRbf(channelId1, 8388607, FeeratePerKw(4000.sat)) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0"), TxInitRbf(channelId1, 0, FeeratePerKw(4000.sat), TlvStream(TxInitRbfTlv.SharedOutputContributionTlv(1_500_000.sat))) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360"), TxInitRbf(channelId1, 0, FeeratePerKw(4000.sat), TlvStream(TxInitRbfTlv.SharedOutputContributionTlv(0.sat))) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000"),