From dec6dd7e701c71af5a3bec76b46662e69bab0d41 Mon Sep 17 00:00:00 2001 From: Harsha Goli Date: Thu, 14 Apr 2022 12:40:30 -0400 Subject: [PATCH] swap: HTLCV3 added In this commit we add the version 3 htlc, which is implemented with taproot script spending the two payment paths: the claim path case, and the timeout case. --- client.go | 6 +- client_test.go | 2 +- go.mod | 12 +- go.sum | 36 +++-- loopd/view.go | 4 +- loopin.go | 2 +- loopin_test.go | 2 +- swap.go | 2 +- swap/htlc.go | 393 +++++++++++++++++++++++++++++++++++++++++++--- swap/htlc_test.go | 333 +++++++++++++++++++++++++++++++++++++-- sweep/sweeper.go | 8 +- 11 files changed, 733 insertions(+), 67 deletions(-) diff --git a/client.go b/client.go index b6bed80..4037cc2 100644 --- a/client.go +++ b/client.go @@ -195,7 +195,7 @@ func (s *Client) FetchSwaps() ([]*SwapInfo, error) { htlc, err := swap.NewHtlc( GetHtlcScriptVersion(swp.Contract.ProtocolVersion), swp.Contract.CltvExpiry, swp.Contract.SenderKey, - swp.Contract.ReceiverKey, swp.Hash, swap.HtlcP2WSH, + swp.Contract.ReceiverKey, nil, swp.Hash, swap.HtlcP2WSH, s.lndServices.ChainParams, ) if err != nil { @@ -216,7 +216,7 @@ func (s *Client) FetchSwaps() ([]*SwapInfo, error) { htlcNP2WSH, err := swap.NewHtlc( GetHtlcScriptVersion(swp.Contract.ProtocolVersion), swp.Contract.CltvExpiry, swp.Contract.SenderKey, - swp.Contract.ReceiverKey, swp.Hash, swap.HtlcNP2WSH, + swp.Contract.ReceiverKey, nil, swp.Hash, swap.HtlcNP2WSH, s.lndServices.ChainParams, ) if err != nil { @@ -226,7 +226,7 @@ func (s *Client) FetchSwaps() ([]*SwapInfo, error) { htlcP2WSH, err := swap.NewHtlc( GetHtlcScriptVersion(swp.Contract.ProtocolVersion), swp.Contract.CltvExpiry, swp.Contract.SenderKey, - swp.Contract.ReceiverKey, swp.Hash, swap.HtlcP2WSH, + swp.Contract.ReceiverKey, nil, swp.Hash, swap.HtlcP2WSH, s.lndServices.ChainParams, ) if err != nil { diff --git a/client_test.go b/client_test.go index 6a315bc..426ee6c 100644 --- a/client_test.go +++ b/client_test.go @@ -284,7 +284,7 @@ func testResume(t *testing.T, confs uint32, expired, preimageRevealed, scriptVersion := GetHtlcScriptVersion(protocolVersion) htlc, err := swap.NewHtlc( scriptVersion, pendingSwap.Contract.CltvExpiry, senderKey, - receiverKey, hash, swap.HtlcP2WSH, &chaincfg.TestNet3Params, + receiverKey, nil, hash, swap.HtlcP2WSH, &chaincfg.TestNet3Params, ) require.NoError(t, err) require.Equal(t, htlc.PkScript, confIntent.PkScript) diff --git a/go.mod b/go.mod index 4b5d5b9..0bbc876 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,23 @@ module github.com/lightninglabs/loop require ( - github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4 - github.com/btcsuite/btcd/btcec/v2 v2.1.0 - github.com/btcsuite/btcd/btcutil v1.1.0 + github.com/btcsuite/btcd v0.22.0-beta.0.20220316175102-8d5c75c28923 + github.com/btcsuite/btcd/btcec/v2 v2.1.3 + github.com/btcsuite/btcd/btcutil v1.1.1 + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/btcsuite/btcwallet/wtxmgr v1.5.0 github.com/coreos/bbolt v1.3.3 github.com/davecgh/go-spew v1.1.1 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 github.com/fortytw2/leaktest v1.3.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 github.com/jessevdk/go-flags v1.4.0 - github.com/lightninglabs/aperture v0.1.17-beta.0.20220325093943-42b9d4c1be7f + github.com/lightninglabs/aperture v0.1.17-beta.0.20220328072456-4a2632d0be38 github.com/lightninglabs/lndclient v0.15.0-0 github.com/lightninglabs/loop/swapserverrpc v1.0.0 github.com/lightninglabs/protobuf-hex-display v1.4.3-hex-display - github.com/lightningnetwork/lnd v0.14.1-beta.0.20220324135938-0dcaa511a249 + github.com/lightningnetwork/lnd v0.14.1-beta.0.20220328072343-6e2214fd0fc0 github.com/lightningnetwork/lnd/cert v1.1.1 github.com/lightningnetwork/lnd/clock v1.1.0 github.com/lightningnetwork/lnd/queue v1.1.0 diff --git a/go.sum b/go.sum index ad020f0..c2aa1b8 100644 --- a/go.sum +++ b/go.sum @@ -73,39 +73,48 @@ github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcug github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.22.0-beta.0.20220204213055-eaf0459ff879/go.mod h1:osu7EoKiL36UThEgzYPqdRaxeo0NU8VoXqgcnwpey0g= -github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4 h1:CEGr/598C/0LZQUoioaT6sdGGcJgu4+ck0PDeJ/QkKs= github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4/go.mod h1:7alexyj/lHlOtr2PJK7L/+HDJZpcGDn/pAU98r7DY08= -github.com/btcsuite/btcd/btcec/v2 v2.1.0 h1:Whmbo9yShKKG+WrUfYGFfgj77vYBiwhwBSJnM66TMKI= +github.com/btcsuite/btcd v0.22.0-beta.0.20220316175102-8d5c75c28923 h1:6H47xWODLXYDuzHapvx4dauPqFjegX4+rHgUkFQPvfw= +github.com/btcsuite/btcd v0.22.0-beta.0.20220316175102-8d5c75c28923/go.mod h1:taIcYprAW2g6Z9S0gGUxyR+zDwimyDMK5ePOX+iJ2ds= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.1/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= -github.com/btcsuite/btcd/btcutil v1.1.0 h1:MO4klnGY+EWJdoWF12Wkuf4AWDBPMpZNeN/jRLrklUU= github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= -github.com/btcsuite/btcd/btcutil/psbt v1.1.0 h1:1LxDjz2ar4L2mrviBdxrzxesMMcAtj4nuBlX4FdqjOA= +github.com/btcsuite/btcd/btcutil v1.1.1 h1:hDcDaXiP0uEzR8Biqo2weECKqEw0uHDZ9ixIWevVQqY= +github.com/btcsuite/btcd/btcutil v1.1.1/go.mod h1:nbKlBMNm9FGsdvKvu0essceubPiAcI57pYBNnsLAa34= github.com/btcsuite/btcd/btcutil/psbt v1.1.0/go.mod h1:xMuACsIKDzcE3kWMxqK+aLrAWZ8bMdn7YjYEwNs5q8k= +github.com/btcsuite/btcd/btcutil/psbt v1.1.1 h1:t8yNrashLB1ZLqOapxtWy/d8jMfMDgwPKwf70sgnSac= +github.com/btcsuite/btcd/btcutil/psbt v1.1.1/go.mod h1:KsGzRAzAdEimzgERpK9Xm+RhuCMvc4j2ctK0BEQ8JV0= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/btcwallet v0.14.0 h1:+Nwf0GkRUwDjd/F3i9HgrRhDp8RHZFbBZ3kQaZr6zD0= github.com/btcsuite/btcwallet v0.14.0/go.mod h1:KFR1x3ZH7c31i4qA34XIvcsnhrEBLK1SHli52lN8E54= -github.com/btcsuite/btcwallet/wallet/txauthor v1.2.1 h1:oxIaFiF8gmOLJh7wNkXYkyLWT7Pj5igSrn5HthPVDYg= +github.com/btcsuite/btcwallet v0.14.1-0.20220322182735-b0001c262734 h1:gG2UgzXLiMiT4sw74161AEf0LE/mxDM8Ia6TaoV0VBw= +github.com/btcsuite/btcwallet v0.14.1-0.20220322182735-b0001c262734/go.mod h1:QN2tl1ipATUQRo9RtgvMHLSspqx7QWsj30qL+7AXuAo= github.com/btcsuite/btcwallet/wallet/txauthor v1.2.1/go.mod h1:/74bubxX5Js48d76nf/TsNabpYp/gndUuJw4chzCmhU= +github.com/btcsuite/btcwallet/wallet/txauthor v1.2.3 h1:M2yr5UlULvpqtxUqpMxTME/pA92Z9cpqeyvAFk9lAg0= +github.com/btcsuite/btcwallet/wallet/txauthor v1.2.3/go.mod h1:T2xSiKGpUkSLCh68aF+FMXmKK9mFqNdHl9VaqOr+JjU= github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 h1:BtEN5Empw62/RVnZ0VcJaVtVlBijnLlJY+dwjAye2Bg= github.com/btcsuite/btcwallet/wallet/txrules v1.2.0/go.mod h1:AtkqiL7ccKWxuLYtZm8Bu8G6q82w4yIZdgq6riy60z0= github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 h1:wZnOolEAeNOHzHTnznw/wQv+j35ftCIokNrnOTOU5o8= github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs= github.com/btcsuite/btcwallet/walletdb v1.3.5/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= -github.com/btcsuite/btcwallet/walletdb v1.3.6-0.20210803004036-eebed51155ec h1:zcAU3Ij8SmqaE+ITtS76fua2Niq7DRNp46sJRhi8PiI= github.com/btcsuite/btcwallet/walletdb v1.3.6-0.20210803004036-eebed51155ec/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= +github.com/btcsuite/btcwallet/walletdb v1.4.0 h1:/C5JRF+dTuE2CNMCO/or5N8epsrhmSM4710uBQoYPTQ= +github.com/btcsuite/btcwallet/walletdb v1.4.0/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= github.com/btcsuite/btcwallet/wtxmgr v1.5.0 h1:WO0KyN4l6H3JWnlFxfGR7r3gDnlGT7W2cL8vl6av4SU= github.com/btcsuite/btcwallet/wtxmgr v1.5.0/go.mod h1:TQVDhFxseiGtZwEPvLgtfyxuNUDsIdaJdshvWzR0HJ4= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc= github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= -github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4= github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= -github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE= github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= @@ -464,8 +473,8 @@ github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg= github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lightninglabs/aperture v0.1.17-beta.0.20220325093943-42b9d4c1be7f h1:wfyZ3sZXoGayB+V8icHl6uoqOX6wiyncKFK9pTleGRw= -github.com/lightninglabs/aperture v0.1.17-beta.0.20220325093943-42b9d4c1be7f/go.mod h1:lDjRKhndRH0CzZQ2m8dWODdqp/ejEW7esb2u2nlvrw4= +github.com/lightninglabs/aperture v0.1.17-beta.0.20220328072456-4a2632d0be38 h1:ht1wzuSmScXJEnW30Cp2vUBHYzJZvme5DHxNMyg+LT0= +github.com/lightninglabs/aperture v0.1.17-beta.0.20220328072456-4a2632d0be38/go.mod h1:lDjRKhndRH0CzZQ2m8dWODdqp/ejEW7esb2u2nlvrw4= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2/go.mod h1:antQGRDRJiuyQF6l+k6NECCSImgCpwaZapATth2Chv4= @@ -477,8 +486,9 @@ github.com/lightninglabs/protobuf-hex-display v1.4.3-hex-display h1:RZJ8H4ueU/aQ github.com/lightninglabs/protobuf-hex-display v1.4.3-hex-display/go.mod h1:2oKOBU042GKFHrdbgGiKax4xVrFiZu51lhacUZQ9MnE= github.com/lightningnetwork/lightning-onion v1.0.2-0.20220211021909-bb84a1ccb0c5 h1:TkKwqFcQTGYoI+VEqyxA8rxpCin8qDaYX0AfVRinT3k= github.com/lightningnetwork/lightning-onion v1.0.2-0.20220211021909-bb84a1ccb0c5/go.mod h1:7dDx73ApjEZA0kcknI799m2O5kkpfg4/gr7N092ojNo= -github.com/lightningnetwork/lnd v0.14.1-beta.0.20220324135938-0dcaa511a249 h1:ELu8An/QgkPU0yQUvTlUE/ZCMBAf4hWacRd1/ayh4pc= github.com/lightningnetwork/lnd v0.14.1-beta.0.20220324135938-0dcaa511a249/go.mod h1:Tp3ZxsfioUl6kQ30RrbMqWoZyZ4K+fv/o1lMEU8U7rA= +github.com/lightningnetwork/lnd v0.14.1-beta.0.20220328072343-6e2214fd0fc0 h1:pQ7SCPNcaUvPdSikqtCX1nvYn3cdWE8r44zWyZTCgCs= +github.com/lightningnetwork/lnd v0.14.1-beta.0.20220328072343-6e2214fd0fc0/go.mod h1:ocTs4P7UuJrTjl37IyxwSb7/HOzTqF5GX41zFL2guCk= github.com/lightningnetwork/lnd/cert v1.1.1 h1:Nsav0RlIDRbOnzz2Yu69SQlK939IKya3Q2S0mDviIN8= github.com/lightningnetwork/lnd/cert v1.1.1/go.mod h1:1P46svkkd73oSoeI4zjkVKgZNwGq8bkGuPR8z+5vQUs= github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg= @@ -646,6 +656,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= diff --git a/loopd/view.go b/loopd/view.go index c13c8ba..a3d40ee 100644 --- a/loopd/view.go +++ b/loopd/view.go @@ -54,7 +54,7 @@ func viewOut(swapClient *loop.Client, chainParams *chaincfg.Params) error { s.Contract.CltvExpiry, s.Contract.SenderKey, s.Contract.ReceiverKey, - s.Hash, swap.HtlcP2WSH, chainParams, + nil, s.Hash, swap.HtlcP2WSH, chainParams, ) if err != nil { return err @@ -106,7 +106,7 @@ func viewIn(swapClient *loop.Client, chainParams *chaincfg.Params) error { s.Contract.CltvExpiry, s.Contract.SenderKey, s.Contract.ReceiverKey, - s.Hash, swap.HtlcNP2WSH, chainParams, + nil, s.Hash, swap.HtlcNP2WSH, chainParams, ) if err != nil { return err diff --git a/loopin.go b/loopin.go index 3863444..eee0bc5 100644 --- a/loopin.go +++ b/loopin.go @@ -938,7 +938,7 @@ func (s *loopInSwap) publishTimeoutTx(ctx context.Context, } witnessFunc := func(sig []byte) (wire.TxWitness, error) { - return s.htlc.GenTimeoutWitness(sig), nil + return s.htlc.GenTimeoutWitness(sig) } sequence := uint32(0) diff --git a/loopin_test.go b/loopin_test.go index a5ded02..6fe9c7e 100644 --- a/loopin_test.go +++ b/loopin_test.go @@ -387,7 +387,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool, htlc, err := swap.NewHtlc( scriptVersion, contract.CltvExpiry, contract.SenderKey, - contract.ReceiverKey, testPreimage.Hash(), swap.HtlcNP2WSH, + contract.ReceiverKey, nil, testPreimage.Hash(), swap.HtlcNP2WSH, cfg.lnd.ChainParams, ) if err != nil { diff --git a/swap.go b/swap.go index 193bdde..3f778c2 100644 --- a/swap.go +++ b/swap.go @@ -72,7 +72,7 @@ func (s *swapKit) getHtlc(outputType swap.HtlcOutputType) (*swap.Htlc, error) { return swap.NewHtlc( GetHtlcScriptVersion(s.contract.ProtocolVersion), s.contract.CltvExpiry, s.contract.SenderKey, - s.contract.ReceiverKey, s.hash, outputType, + s.contract.ReceiverKey, nil, s.hash, outputType, s.swapConfig.lnd.ChainParams, ) } diff --git a/swap/htlc.go b/swap/htlc.go index 6a2d320..60358b5 100644 --- a/swap/htlc.go +++ b/swap/htlc.go @@ -6,10 +6,13 @@ import ( "errors" "fmt" + btcec "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lntypes" ) @@ -24,6 +27,9 @@ const ( // HtlcNP2WSH is a nested pay-to-witness-script-hash output that can be // paid to be legacy wallets. HtlcNP2WSH + + // HtlcP2TR is a pay-to-taproot output with three separate spend paths. + HtlcP2TR ) // ScriptVersion defines the HTLC script version. @@ -35,17 +41,22 @@ const ( // HtlcV2 refers to the improved version of the HTLC script. HtlcV2 + + // HtlcV3 refers to an upgraded version of HtlcV2 implemented with + // tapscript. + HtlcV3 ) // htlcScript defines an interface for the different HTLC implementations. type HtlcScript interface { // genSuccessWitness returns the success script to spend this htlc with // the preimage. - genSuccessWitness(receiverSig []byte, preimage lntypes.Preimage) wire.TxWitness + genSuccessWitness(receiverSig []byte, + preimage lntypes.Preimage) (wire.TxWitness, error) // GenTimeoutWitness returns the timeout script to spend this htlc after // timeout. - GenTimeoutWitness(senderSig []byte) wire.TxWitness + GenTimeoutWitness(senderSig []byte) (wire.TxWitness, error) // IsSuccessWitness checks whether the given stack is valid for // redeeming the htlc. @@ -90,11 +101,30 @@ var ( // script size. QuoteHtlc, _ = NewHtlc( HtlcV2, - ^int32(0), quoteKey, quoteKey, quoteHash, HtlcP2WSH, + ^int32(0), quoteKey, quoteKey, nil, quoteHash, HtlcP2WSH, &chaincfg.MainNetParams, ) + // ErrInvalidScriptVersion is returned when an unknown htlc version + // is provided to NewHtlc. The supported version are HtlcV1, HtlcV2, + // HtlcV3 as enums. ErrInvalidScriptVersion = fmt.Errorf("invalid script version") + + // ErrInvalidOutputSelected is returned when a taproot output is + // selected for a v1 or v2 script. + ErrInvalidOutputSelected = fmt.Errorf("taproot output selected for " + + "non taproot htlc") + + // ErrSharedKeyNotNeeded is returned when a shared key is provided for + // either the v1 or v2 script. Shared key is only necessary for the v3 + // script. + ErrSharedKeyNotNeeded = fmt.Errorf("shared key not supported for " + + "script version") + + // ErrSharedKeyRequired is returned when a script version requires a + // shared key. + ErrSharedKeyRequired = fmt.Errorf("shared key required for script " + + "version") ) // String returns the string value of HtlcOutputType. @@ -106,14 +136,18 @@ func (h HtlcOutputType) String() string { case HtlcNP2WSH: return "NP2WSH" + case HtlcP2TR: + return "P2TR" + default: return "unknown" } } -// NewHtlc returns a new instance. +// NewHtlc returns a new instance. For v3 scripts, an internal pubkey generated +// by both participants must be provided. func NewHtlc(version ScriptVersion, cltvExpiry int32, - senderKey, receiverKey [33]byte, + senderKey, receiverKey [33]byte, sharedKey *btcec.PublicKey, hash lntypes.Hash, outputType HtlcOutputType, chainParams *chaincfg.Params) (*Htlc, error) { @@ -124,15 +158,30 @@ func NewHtlc(version ScriptVersion, cltvExpiry int32, switch version { case HtlcV1: + if sharedKey != nil { + return nil, ErrSharedKeyNotNeeded + } htlc, err = newHTLCScriptV1( cltvExpiry, senderKey, receiverKey, hash, ) case HtlcV2: + if sharedKey != nil { + return nil, ErrSharedKeyNotNeeded + } htlc, err = newHTLCScriptV2( cltvExpiry, senderKey, receiverKey, hash, ) + case HtlcV3: + if sharedKey == nil { + return nil, ErrSharedKeyRequired + } + htlc, err = newHTLCScriptV3( + cltvExpiry, senderKey, receiverKey, + sharedKey, hash, + ) + default: return nil, ErrInvalidScriptVersion } @@ -141,16 +190,16 @@ func NewHtlc(version ScriptVersion, cltvExpiry int32, return nil, err } - p2wshPkScript, err := input.WitnessScriptHash(htlc.Script()) - if err != nil { - return nil, err - } - var pkScript, sigScript []byte var address btcutil.Address switch outputType { case HtlcNP2WSH: + p2wshPkScript, err := input.WitnessScriptHash(htlc.Script()) + if err != nil { + return nil, err + } + // Generate p2sh script for p2wsh (nested). p2wshPkScriptHash := sha256.Sum256(p2wshPkScript) hash160 := input.Ripemd160H(p2wshPkScriptHash[:]) @@ -185,15 +234,40 @@ func NewHtlc(version ScriptVersion, cltvExpiry int32, } case HtlcP2WSH: - pkScript = p2wshPkScript + pkScript, err = input.WitnessScriptHash(htlc.Script()) + if err != nil { + return nil, err + } address, err = btcutil.NewAddressWitnessScriptHash( - p2wshPkScript[2:], + pkScript[2:], chainParams, ) if err != nil { return nil, err } + + case HtlcP2TR: + // Confirm we have a v3 htlc. + trHtlc, ok := htlc.(*HtlcScriptV3) + if !ok { + return nil, ErrInvalidOutputSelected + } + + // Generate a tapscript address from our HTLC's taptree. + address, err = btcutil.NewAddressTaproot( + schnorr.SerializePubKey(trHtlc.TaprootKey), chainParams, + ) + if err != nil { + return nil, err + } + + // Generate locking script. + pkScript, err = txscript.PayToAddrScript(address) + if err != nil { + return nil, err + } + default: return nil, errors.New("unknown output type") } @@ -219,33 +293,67 @@ func (h *Htlc) GenSuccessWitness(receiverSig []byte, return nil, errors.New("preimage doesn't match hash") } - return h.genSuccessWitness(receiverSig, preimage), nil + return h.genSuccessWitness(receiverSig, preimage) } // AddSuccessToEstimator adds a successful spend to a weight estimator. -func (h *Htlc) AddSuccessToEstimator(estimator *input.TxWeightEstimator) { +func (h *Htlc) AddSuccessToEstimator(estimator *input.TxWeightEstimator) error { maxSuccessWitnessSize := h.MaxSuccessWitnessSize() switch h.OutputType { + case HtlcP2TR: + // Generate tapscript. + trHtlc, ok := h.HtlcScript.(*HtlcScriptV3) + if !ok { + return ErrInvalidOutputSelected + } + successLeaf := txscript.NewBaseTapLeaf(trHtlc.SuccessScript) + timeoutLeaf := txscript.NewBaseTapLeaf(trHtlc.TimeoutScript) + tapscript := input.TapscriptPartialReveal( + trHtlc.InternalPubKey, successLeaf, + timeoutLeaf.TapHash(), + ) + + estimator.AddTapscriptInput(maxSuccessWitnessSize, tapscript) + case HtlcP2WSH: estimator.AddWitnessInput(maxSuccessWitnessSize) case HtlcNP2WSH: estimator.AddNestedP2WSHInput(maxSuccessWitnessSize) } + + return nil } // AddTimeoutToEstimator adds a timeout spend to a weight estimator. -func (h *Htlc) AddTimeoutToEstimator(estimator *input.TxWeightEstimator) { +func (h *Htlc) AddTimeoutToEstimator(estimator *input.TxWeightEstimator) error { maxTimeoutWitnessSize := h.MaxTimeoutWitnessSize() switch h.OutputType { + case HtlcP2TR: + // Generate tapscript. + trHtlc, ok := h.HtlcScript.(*HtlcScriptV3) + if !ok { + return ErrInvalidOutputSelected + } + successLeaf := txscript.NewBaseTapLeaf(trHtlc.SuccessScript) + timeoutLeaf := txscript.NewBaseTapLeaf(trHtlc.TimeoutScript) + tapscript := input.TapscriptPartialReveal( + trHtlc.InternalPubKey, timeoutLeaf, + successLeaf.TapHash(), + ) + + estimator.AddTapscriptInput(maxTimeoutWitnessSize, tapscript) + case HtlcP2WSH: estimator.AddWitnessInput(maxTimeoutWitnessSize) case HtlcNP2WSH: estimator.AddNestedP2WSHInput(maxTimeoutWitnessSize) } + + return nil } // HtlcScriptV1 encapsulates the htlc v1 script. @@ -309,26 +417,27 @@ func newHTLCScriptV1(cltvExpiry int32, senderHtlcKey, // genSuccessWitness returns the success script to spend this htlc with // the preimage. func (h *HtlcScriptV1) genSuccessWitness(receiverSig []byte, - preimage lntypes.Preimage) wire.TxWitness { + preimage lntypes.Preimage) (wire.TxWitness, error) { witnessStack := make(wire.TxWitness, 3) witnessStack[0] = append(receiverSig, byte(txscript.SigHashAll)) witnessStack[1] = preimage[:] witnessStack[2] = h.script - return witnessStack + return witnessStack, nil } // GenTimeoutWitness returns the timeout script to spend this htlc after // timeout. -func (h *HtlcScriptV1) GenTimeoutWitness(senderSig []byte) wire.TxWitness { +func (h *HtlcScriptV1) GenTimeoutWitness( + senderSig []byte) (wire.TxWitness, error) { witnessStack := make(wire.TxWitness, 3) witnessStack[0] = append(senderSig, byte(txscript.SigHashAll)) witnessStack[1] = []byte{0} witnessStack[2] = h.script - return witnessStack + return witnessStack, nil } // IsSuccessWitness checks whether the given stack is valid for redeeming the @@ -443,14 +552,14 @@ func newHTLCScriptV2(cltvExpiry int32, senderHtlcKey, // genSuccessWitness returns the success script to spend this htlc with // the preimage. func (h *HtlcScriptV2) genSuccessWitness(receiverSig []byte, - preimage lntypes.Preimage) wire.TxWitness { + preimage lntypes.Preimage) (wire.TxWitness, error) { witnessStack := make(wire.TxWitness, 3) witnessStack[0] = preimage[:] witnessStack[1] = append(receiverSig, byte(txscript.SigHashAll)) witnessStack[2] = h.script - return witnessStack + return witnessStack, nil } // IsSuccessWitness checks whether the given stack is valid for redeeming the @@ -463,7 +572,8 @@ func (h *HtlcScriptV2) IsSuccessWitness(witness wire.TxWitness) bool { // GenTimeoutWitness returns the timeout script to spend this htlc after // timeout. -func (h *HtlcScriptV2) GenTimeoutWitness(senderSig []byte) wire.TxWitness { +func (h *HtlcScriptV2) GenTimeoutWitness( + senderSig []byte) (wire.TxWitness, error) { witnessStack := make(wire.TxWitness, 4) witnessStack[0] = append(senderSig, byte(txscript.SigHashAll)) @@ -471,7 +581,7 @@ func (h *HtlcScriptV2) GenTimeoutWitness(senderSig []byte) wire.TxWitness { witnessStack[2] = []byte{} witnessStack[3] = h.script - return witnessStack + return witnessStack, nil } // Script returns the htlc script. @@ -512,3 +622,240 @@ func (h *HtlcScriptV2) MaxTimeoutWitnessSize() int { func (h *HtlcScriptV2) SuccessSequence() uint32 { return 1 } + +// HtlcScriptV3 encapsulates the htlc v3 script. +type HtlcScriptV3 struct { + // The final locking script for the timeout path which is available to + // the sender after the set blockheight. + TimeoutScript []byte + + // The final locking script for the success path in which the receiver + // reveals the preimage. + SuccessScript []byte + + // The public key for the keyspend path which bypasses the above two + // locking scripts. + InternalPubKey *btcec.PublicKey + + // The taproot public key which is created with the above 3 inputs. + TaprootKey *btcec.PublicKey +} + +// newHTLCScriptV3 constructs a HtlcScipt with the HTLC V3 taproot script. +func newHTLCScriptV3(cltvExpiry int32, senderHtlcKey, + receiverHtlcKey [33]byte, sharedKey *btcec.PublicKey, + swapHash lntypes.Hash) (*HtlcScriptV3, error) { + + receiverPubKey, err := btcec.ParsePubKey( + receiverHtlcKey[:], + ) + if err != nil { + return nil, err + } + + senderPubKey, err := btcec.ParsePubKey( + senderHtlcKey[:], + ) + if err != nil { + return nil, err + } + + var schnorrSenderKey, schnorrReceiverKey [32]byte + copy(schnorrSenderKey[:], schnorr.SerializePubKey(senderPubKey)) + copy(schnorrReceiverKey[:], schnorr.SerializePubKey(receiverPubKey)) + + // Create our success path script, we'll use this separately + // to generate the success path leaf. + successPathScript, err := GenSuccessPathScript( + schnorrReceiverKey, swapHash, + ) + if err != nil { + return nil, err + } + + // Create our timeout path leaf, we'll use this separately + // to generate the timeout path leaf. + timeoutPathScript, err := GenTimeoutPathScript( + schnorrSenderKey, int64(cltvExpiry), + ) + if err != nil { + return nil, err + } + + // Assemble our taproot script tree from our leaves. + tree := txscript.AssembleTaprootScriptTree( + txscript.NewBaseTapLeaf(successPathScript), + txscript.NewBaseTapLeaf(timeoutPathScript), + ) + + rootHash := tree.RootNode.TapHash() + + // Calculate top level taproot key. + taprootKey := txscript.ComputeTaprootOutputKey( + sharedKey, rootHash[:], + ) + + return &HtlcScriptV3{ + TimeoutScript: timeoutPathScript, + SuccessScript: successPathScript, + InternalPubKey: sharedKey, + TaprootKey: taprootKey, + }, nil +} + +// GenTimeoutPathScript constructs an HtlcScript for the timeout payment path. +// Largest possible bytesize of the script is 32 + 1 + 2 + 1 = 36. +// +// OP_CHECKSIGVERIFY OP_CHECKLOCKTIMEVERIFY +func GenTimeoutPathScript( + senderHtlcKey [32]byte, cltvExpiry int64) ([]byte, error) { + + builder := txscript.NewScriptBuilder() + builder.AddData(senderHtlcKey[:]) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + builder.AddInt64(cltvExpiry) + builder.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY) + return builder.Script() +} + +// GenSuccessPathScript constructs an HtlcScript for the success payment path. +// Largest possible bytesize of the script is 32 + 5*1 + 20 + 3*1 = 60. +// +// OP_CHECKSIGVERIFY +// OP_SIZE 32 OP_EQUALVERIFY +// OP_HASH160 OP_EQUALVERIFY +// 1 OP_CHECKSEQUENCEVERIFY +func GenSuccessPathScript(receiverHtlcKey [32]byte, + swapHash lntypes.Hash) ([]byte, error) { + + builder := txscript.NewScriptBuilder() + + builder.AddData(receiverHtlcKey[:]) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + builder.AddOp(txscript.OP_SIZE) + builder.AddInt64(32) + builder.AddOp(txscript.OP_EQUALVERIFY) + builder.AddOp(txscript.OP_HASH160) + builder.AddData(input.Ripemd160H(swapHash[:])) + builder.AddOp(txscript.OP_EQUALVERIFY) + builder.AddInt64(1) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + + return builder.Script() +} + +// genControlBlock constructs the control block with the depth 1 leaf of the +// unused path to compute the proof. For example if spending path a of (root -> +// a, root -> b), genControlBlock(b.Script) would be used to create the +// controlBlock for a. +func (h *HtlcScriptV3) genControlBlock(leafScript []byte) ([]byte, error) { + var outputKeyYIsOdd bool + + // Check for odd bit. + if h.TaprootKey.SerializeCompressed()[0] == secp.PubKeyFormatCompressedOdd { + outputKeyYIsOdd = true + } + + // Generate proof with unused script path. + leaf := txscript.NewBaseTapLeaf(leafScript) + proof := leaf.TapHash() + + controlBlock := txscript.ControlBlock{ + InternalKey: h.InternalPubKey, + OutputKeyYIsOdd: outputKeyYIsOdd, + LeafVersion: txscript.BaseLeafVersion, + InclusionProof: proof[:], + } + + return controlBlock.ToBytes() +} + +// genSuccessWitness returns the success script to spend this htlc with +// the preimage. +func (h *HtlcScriptV3) genSuccessWitness( + receiverSig []byte, preimage lntypes.Preimage) (wire.TxWitness, error) { + + controlBlockBytes, err := h.genControlBlock(h.TimeoutScript) + if err != nil { + return nil, err + } + + return wire.TxWitness{ + preimage[:], + receiverSig, + h.SuccessScript, + controlBlockBytes, + }, nil +} + +// GenTimeoutWitness returns the timeout script to spend this htlc after +// timeout. +func (h *HtlcScriptV3) GenTimeoutWitness( + senderSig []byte) (wire.TxWitness, error) { + + controlBlockBytes, err := h.genControlBlock(h.SuccessScript) + if err != nil { + return nil, err + } + + return wire.TxWitness{ + senderSig, + h.TimeoutScript, + controlBlockBytes, + }, nil +} + +// IsSuccessWitness checks whether the given stack is valid for +// redeeming the htlc. +func (h *HtlcScriptV3) IsSuccessWitness(witness wire.TxWitness) bool { + return len(witness) == 4 +} + +// Script is not implemented, but necessary to conform to interface. +func (h *HtlcScriptV3) Script() []byte { + return nil +} + +// MaxSuccessWitnessSize returns the maximum witness size for the +// success case witness. +func (h *HtlcScriptV3) MaxSuccessWitnessSize() int { + // Calculate maximum success witness size + // + // - number_of_witness_elements: 1 byte + // - sigLength: 1 byte + // - sig: 64 bytes + // - preimage_length: 1 byte + // - preimage: 32 bytes + // - witness_script_length: 1 byte + // - witness_script: 60 bytes + // - control_block_length: 1 byte + // - control_block: 65 bytes + // - leafVersionAndParity: 1 + // - internalPubkey: 32 + // - proof: 32 + return 1 + 1 + 64 + 1 + 32 + 1 + 60 + 1 + 65 +} + +// MaxTimeoutWitnessSize returns the maximum witness size for the +// timeout case witness. +func (h *HtlcScriptV3) MaxTimeoutWitnessSize() int { + // Calculate maximum timeout witness size + // + // - number_of_witness_elements: 1 byte + // - sigLength: 1 byte + // - sig: 64 bytes + // - witness_script_length: 1 byte + // - witness_script: 36 bytes + // - control_block_length: 1 byte + // - control_block: 65 bytes + // - leafVersionAndParity: 1 + // - internalPubkey: 32 + // - proof: 32 + return 1 + 1 + 64 + 1 + 36 + 1 + 65 +} + +// SuccessSequence returns the sequence to spend this htlc in the +// success case. +func (h *HtlcScriptV3) SuccessSequence() uint32 { + return 1 +} diff --git a/swap/htlc_test.go b/swap/htlc_test.go index 5c37c32..e672217 100644 --- a/swap/htlc_test.go +++ b/swap/htlc_test.go @@ -3,15 +3,18 @@ package swap import ( "bytes" "crypto/sha256" + "encoding/hex" "fmt" "testing" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" @@ -101,6 +104,8 @@ func TestHtlcV2(t *testing.T) { var ( testPreimage = lntypes.Preimage([32]byte{1, 2, 3}) err error + receiverKey [33]byte + senderKey [33]byte ) // We generate a fake output, and the corresponding txin. This output @@ -125,19 +130,14 @@ func TestHtlcV2(t *testing.T) { senderPrivKey, senderPubKey := test.CreateKey(1) receiverPrivKey, receiverPubKey := test.CreateKey(2) - var ( - senderKey [33]byte - receiverKey [33]byte - ) - copy(senderKey[:], senderPubKey.SerializeCompressed()) copy(receiverKey[:], receiverPubKey.SerializeCompressed()) + copy(senderKey[:], senderPubKey.SerializeCompressed()) hash := sha256.Sum256(testPreimage[:]) // Create the htlc. htlc, err := NewHtlc( - HtlcV2, testCltvExpiry, - senderKey, receiverKey, hash, + HtlcV2, testCltvExpiry, senderKey, receiverKey, nil, hash, HtlcP2WSH, &chaincfg.MainNetParams, ) require.NoError(t, err) @@ -155,6 +155,9 @@ func TestHtlcV2(t *testing.T) { receiverSigner := &input.MockSigner{ Privkeys: []*btcec.PrivateKey{receiverPrivKey}, } + prevOutFetcher := txscript.NewCannedPrevOutputFetcher( + htlc.PkScript, 800_000, + ) signTx := func(tx *wire.MsgTx, pubkey *btcec.PublicKey, signer *input.MockSigner) (input.Signature, error) { @@ -167,8 +170,10 @@ func TestHtlcV2(t *testing.T) { WitnessScript: htlc.Script(), Output: htlcOutput, HashType: txscript.SigHashAll, - SigHashes: txscript.NewTxSigHashes(tx), - InputIndex: 0, + SigHashes: txscript.NewTxSigHashes( + tx, prevOutFetcher, + ), + InputIndex: 0, } return signer.SignOutputRaw(tx, signDesc) @@ -227,9 +232,12 @@ func TestHtlcV2(t *testing.T) { ) require.NoError(t, err) - return htlc.GenTimeoutWitness( + witness, err := htlc.GenTimeoutWitness( sweepSig.Serialize(), ) + require.NoError(t, err) + + return witness }, false, }, { @@ -242,13 +250,16 @@ func TestHtlcV2(t *testing.T) { ) require.NoError(t, err) - return htlc.GenTimeoutWitness( + witness, err := htlc.GenTimeoutWitness( sweepSig.Serialize(), ) + require.NoError(t, err) + + return witness }, true, }, { - // Receiver can't spend after timeout. + // Receiver can't spend via timeout path. "timeout case receiver cannot spend", func(t *testing.T) wire.TxWitness { sweepTx.LockTime = testCltvExpiry @@ -257,9 +268,12 @@ func TestHtlcV2(t *testing.T) { ) require.NoError(t, err) - return htlc.GenTimeoutWitness( + witness, err := htlc.GenTimeoutWitness( sweepSig.Serialize(), ) + require.NoError(t, err) + + return witness }, false, }, { @@ -272,7 +286,7 @@ func TestHtlcV2(t *testing.T) { // Create the htlc with the bogus key. htlc, err = NewHtlc( HtlcV2, testCltvExpiry, - bogusKey, receiverKey, hash, + bogusKey, receiverKey, nil, hash, HtlcP2WSH, &chaincfg.MainNetParams, ) require.NoError(t, err) @@ -289,9 +303,12 @@ func TestHtlcV2(t *testing.T) { ) require.NoError(t, err) - return htlc.GenTimeoutWitness( + witness, err := htlc.GenTimeoutWitness( sweepSig.Serialize(), ) + require.NoError(t, err) + + return witness }, false, }, } @@ -306,7 +323,291 @@ func TestHtlcV2(t *testing.T) { return txscript.NewEngine( htlc.PkScript, sweepTx, 0, txscript.StandardVerifyFlags, nil, - nil, int64(htlcValue), + nil, int64(htlcValue), prevOutFetcher, + ) + } + + assertEngineExecution(t, testCase.valid, newEngine) + }) + } +} + +// TestHtlcV3 tests the HTLC V3 script success and timeout spend cases. +func TestHtlcV3(t *testing.T) { + var ( + receiverKey [33]byte + senderKey [33]byte + ) + + preimage := [32]byte{1, 2, 3} + p := lntypes.Preimage(preimage) + hashedPreimage := sha256.Sum256(p[:]) + + value := int64(800_000) + + cltvExpiry := int32(10) + + senderPrivKey, senderPubKey := test.CreateKey(1) + receiverPrivKey, receiverPubKey := test.CreateKey(2) + + copy(receiverKey[:], receiverPubKey.SerializeCompressed()) + copy(senderKey[:], senderPubKey.SerializeCompressed()) + + randomSharedKey, err := hex.DecodeString( + "03fcb7d1b502bd59f4dbc6cf503e5c280189e0e6dd2d10c4c14d97ed8611" + + "a99178", + ) + require.NoError(t, err) + + randomSharedPubKey, err := btcec.ParsePubKey(randomSharedKey) + require.NoError(t, err) + + htlc, err := NewHtlc( + HtlcV3, cltvExpiry, senderKey, receiverKey, randomSharedPubKey, + hashedPreimage, HtlcP2TR, &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + trAddress, ok := htlc.Address.(*btcutil.AddressTaproot) + require.True(t, ok) + + p2trPkScript, err := txscript.PayToAddrScript(trAddress) + require.NoError(t, err) + + tx := wire.NewMsgTx(2) + tx.TxIn = []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash(sha256.Sum256([]byte{1, 2, 3})), + Index: 50, + }, + }} + tx.TxOut = []*wire.TxOut{{ + PkScript: []byte{ + 0, 20, 2, 141, 221, 230, 144, + 171, 89, 230, 219, 198, 90, 157, + 110, 89, 89, 67, 128, 16, 150, 186, + }, + Value: value, + }} + + prevOutFetcher := txscript.NewCannedPrevOutputFetcher( + p2trPkScript, value, + ) + hashCache := txscript.NewTxSigHashes( + tx, prevOutFetcher, + ) + + signTx := func(tx *wire.MsgTx, privateKey *secp.PrivateKey, + leaf txscript.TapLeaf) []byte { + + sig, err := txscript.RawTxInTapscriptSignature( + tx, hashCache, 0, value, p2trPkScript, leaf, + txscript.SigHashDefault, privateKey, + ) + require.NoError(t, err) + + return sig + } + + testCases := []struct { + name string + witness func(*testing.T) wire.TxWitness + valid bool + }{ + { + // Receiver can spend with valid preimage. + "success case spend with valid preimage", + func(t *testing.T) wire.TxWitness { + tx.TxIn[0].Sequence = htlc.SuccessSequence() + tx.LockTime = uint32(cltvExpiry) + + trHtlc, ok := htlc.HtlcScript.(*HtlcScriptV3) + require.True(t, ok) + + sig := signTx( + tx, receiverPrivKey, + txscript.NewBaseTapLeaf( + trHtlc.SuccessScript, + ), + ) + witness, err := htlc.genSuccessWitness( + sig, preimage, + ) + require.NoError(t, err) + + return witness + }, true, + }, + { + // Receiver can't spend with the valid preimage and with + // zero sequence. + "success case no spend with valid preimage and zero sequence", + func(t *testing.T) wire.TxWitness { + tx.TxIn[0].Sequence = 0 + + trHtlc, ok := htlc.HtlcScript.(*HtlcScriptV3) + require.True(t, ok) + + sig := signTx( + tx, receiverPrivKey, + txscript.NewBaseTapLeaf( + trHtlc.SuccessScript, + ), + ) + witness, err := htlc.genSuccessWitness( + sig, preimage, + ) + require.NoError(t, err) + + return witness + }, false, + }, + { + // Sender can't spend when haven't yet timed out. + "timeout case no spend before timeout", + func(t *testing.T) wire.TxWitness { + tx.TxIn[0].Sequence = htlc.SuccessSequence() + tx.LockTime = uint32(cltvExpiry) - 1 + + trHtlc, ok := htlc.HtlcScript.(*HtlcScriptV3) + require.True(t, ok) + + sig := signTx( + tx, senderPrivKey, + txscript.NewBaseTapLeaf( + trHtlc.TimeoutScript, + ), + ) + + witness, err := htlc.GenTimeoutWitness(sig) + require.NoError(t, err) + + return witness + }, false, + }, + { + // Sender can spend after timeout. + "timeout case spend after timeout", + func(t *testing.T) wire.TxWitness { + tx.TxIn[0].Sequence = htlc.SuccessSequence() + tx.LockTime = uint32(cltvExpiry) + + trHtlc, ok := htlc.HtlcScript.(*HtlcScriptV3) + require.True(t, ok) + + sig := signTx( + tx, senderPrivKey, + txscript.NewBaseTapLeaf( + trHtlc.TimeoutScript, + ), + ) + + witness, err := htlc.GenTimeoutWitness(sig) + require.NoError(t, err) + + return witness + }, true, + }, + { + // Receiver can't spend via timeout path. + "timeout case receiver cannot spend", + func(t *testing.T) wire.TxWitness { + tx.TxIn[0].Sequence = htlc.SuccessSequence() + tx.LockTime = uint32(cltvExpiry) + + trHtlc, ok := htlc.HtlcScript.(*HtlcScriptV3) + require.True(t, ok) + + sig := signTx( + tx, receiverPrivKey, + txscript.NewBaseTapLeaf( + trHtlc.TimeoutScript, + ), + ) + + witness, err := htlc.GenTimeoutWitness(sig) + require.NoError(t, err) + + return witness + }, false, + }, + { + // Sender can't spend after timeout with wrong sender + // key. + "timeout case cannot spend with wrong key", + func(t *testing.T) wire.TxWitness { + var bogusKeyBytes [33]byte + + _, bogusKey := test.CreateKey(5) + + copy( + bogusKeyBytes[:], + bogusKey.SerializeCompressed(), + ) + + var shnorrSenderKey [32]byte + copy( + shnorrSenderKey[:], + schnorr.SerializePubKey( + senderPubKey, + ), + ) + + htlc, err := NewHtlc( + HtlcV3, cltvExpiry, bogusKeyBytes, + receiverKey, randomSharedPubKey, + hashedPreimage, HtlcP2TR, + &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + trAddress, ok := htlc.Address.(*btcutil.AddressTaproot) + require.True(t, ok) + + p2trPkScript, err := txscript.PayToAddrScript( + trAddress, + ) + require.NoError(t, err) + + prevOutFetcher := txscript.NewCannedPrevOutputFetcher( + p2trPkScript, 800_000, + ) + hashCache = txscript.NewTxSigHashes( + tx, prevOutFetcher, + ) + + timeoutScript, err := GenTimeoutPathScript( + shnorrSenderKey, int64(cltvExpiry), + ) + require.NoError(t, err) + + sig := signTx( + tx, senderPrivKey, + txscript.NewBaseTapLeaf( + timeoutScript, + ), + ) + witness, err := htlc.genSuccessWitness( + sig, preimage, + ) + require.NoError(t, err) + + return witness + }, false, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + tx.TxIn[0].Witness = testCase.witness(t) + + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine( + p2trPkScript, tx, 0, + txscript.StandardVerifyFlags, nil, + hashCache, value, prevOutFetcher, ) } diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 6fcba91..80fa3cd 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -92,7 +92,7 @@ func (s *Sweeper) CreateSweepTx( // function that is expected to add the weight of the input to the weight // estimator. func (s *Sweeper) GetSweepFee(ctx context.Context, - addInputEstimate func(*input.TxWeightEstimator), + addInputEstimate func(*input.TxWeightEstimator) error, destAddr btcutil.Address, sweepConfTarget int32) ( btcutil.Amount, error) { @@ -117,7 +117,11 @@ func (s *Sweeper) GetSweepFee(ctx context.Context, return 0, fmt.Errorf("unknown address type %T", destAddr) } - addInputEstimate(&weightEstimate) + err = addInputEstimate(&weightEstimate) + if err != nil { + return 0, err + } + weight := weightEstimate.Weight() return feeRate.FeeForWeight(int64(weight)), nil