diff --git a/go.mod b/go.mod index d60430061d..2ee6522c84 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,16 @@ module github.com/btcsuite/btcwallet 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/btcutil/psbt 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/btcutil/psbt 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/wallet/txauthor v1.2.1 github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 - github.com/btcsuite/btcwallet/walletdb v1.3.5 + github.com/btcsuite/btcwallet/walletdb v1.4.0 github.com/btcsuite/btcwallet/wtxmgr v1.5.0 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 github.com/davecgh/go-spew v1.1.1 @@ -21,19 +22,15 @@ require ( github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf github.com/lightninglabs/neutrino v0.13.2 github.com/lightningnetwork/lnd/ticker v1.0.0 - github.com/stretchr/testify v1.5.1 + github.com/lightningnetwork/lnd/tlv v1.0.2 + github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922 // indirect google.golang.org/grpc v1.18.0 ) -replace github.com/btcsuite/btcwallet/walletdb => ./walletdb - -// The old version of ginko that's used in btcd imports an ancient version of -// gopkg.in/fsnotify.v1 that isn't go mod compatible. We fix that import error -// by replacing ginko (which is only a test library anyway) with a more recent -// version. -replace github.com/onsi/ginkgo => github.com/onsi/ginkgo v1.14.2 +// TODO(guggero): Remove this replace once we have a tagged version of txauthor. +replace github.com/btcsuite/btcwallet/wallet/txauthor => ./wallet/txauthor go 1.16 diff --git a/go.sum b/go.sum index 6fde455e94..4b72b84606 100644 --- a/go.sum +++ b/go.sum @@ -5,33 +5,39 @@ 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/psbt v1.1.0/go.mod h1:xMuACsIKDzcE3kWMxqK+aLrAWZ8bMdn7YjYEwNs5q8k= +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.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/wallet/txauthor v1.2.1 h1:oxIaFiF8gmOLJh7wNkXYkyLWT7Pj5igSrn5HthPVDYg= -github.com/btcsuite/btcwallet/wallet/txauthor v1.2.1/go.mod h1:/74bubxX5Js48d76nf/TsNabpYp/gndUuJw4chzCmhU= 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.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/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= @@ -63,6 +69,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= @@ -92,16 +100,25 @@ github.com/lightningnetwork/lnd/queue v1.0.1 h1:jzJKcTy3Nj5lQrooJ3aaw9Lau3I0IwvQ github.com/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms= github.com/lightningnetwork/lnd/ticker v1.0.0 h1:S1b60TEGoTtCe2A0yeB+ecoj/kkS4qpwh6l+AkQEZwU= github.com/lightningnetwork/lnd/ticker v1.0.0/go.mod h1:iaLXJiVgI1sPANIF2qYYUJXjoksPNvGNYowB8aRbpX0= +github.com/lightningnetwork/lnd/tlv v1.0.2 h1:LG7H3Uw/mHYGnEeHRPg+STavAH+UsFvuBflD0PzcYFQ= +github.com/lightningnetwork/lnd/tlv v1.0.2/go.mod h1:fICAfsqk1IOsC1J7G9IdsWX1EqWRMqEDCNxZJSKr9C4= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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= go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50 h1:ASw9n1EHMftwnP3Az4XW6e308+gNsrHzmdhd0Olz9Hs= go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -133,6 +150,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -169,6 +187,8 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/waddrmgr/address.go b/waddrmgr/address.go index 567459e936..d8b9e39b71 100644 --- a/waddrmgr/address.go +++ b/waddrmgr/address.go @@ -10,6 +10,7 @@ import ( "sync" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/txscript" @@ -51,6 +52,25 @@ const ( // WitnessScript represents a p2wsh (pay-to-witness-script-hash) address // type. WitnessScript + + // TaprootPubKey represents a p2tr (pay-to-taproot) address type that + // uses BIP-0086 (for the derivation path and for calculating the tap + // root hash/tweak). + TaprootPubKey + + // TaprootScript represents a p2tr (pay-to-taproot) address type that + // commits to a script and not just a single key. + TaprootScript +) + +const ( + // witnessVersionV0 is the SegWit v0 witness version used for p2wpkh and + // p2wsh outputs and addresses. + witnessVersionV0 byte = 0x00 + + // witnessVersionV1 is the SegWit v1 witness version used for p2tr + // outputs and addresses. + witnessVersionV1 byte = 0x01 ) // ManagedAddress is an interface that provides acces to information regarding @@ -126,6 +146,17 @@ type ManagedScriptAddress interface { Script() ([]byte, error) } +// ManagedTaprootScriptAddress extends ManagedScriptAddress and represents a +// pay-to-taproot script address. It additionally provides information about the +// script. +type ManagedTaprootScriptAddress interface { + ManagedScriptAddress + + // TaprootScript returns all the information needed to derive the script + // tree root hash needed to arrive at the tweaked taproot key. + TaprootScript() (*Tapscript, error) +} + // managedAddress represents a public key address. It also may or may not have // the private key associated with the public key. type managedAddress struct { @@ -269,6 +300,9 @@ func (a *managedAddress) PubKey() *btcec.PublicKey { // pubKeyBytes returns the serialized public key bytes for the managed address // based on whether or not the managed address is marked as compressed. func (a *managedAddress) pubKeyBytes() []byte { + if a.addrType == TaprootPubKey { + return schnorr.SerializePubKey(a.pubKey) + } if a.compressed { return a.pubKey.SerializeCompressed() } @@ -415,6 +449,16 @@ func newManagedAddressWithoutPrivKey(m *ScopedKeyManager, if err != nil { return nil, err } + + case TaprootPubKey: + tapKey := txscript.ComputeTaprootKeyNoScript(pubKey) + address, err = btcutil.NewAddressTaproot( + schnorr.SerializePubKey(tapKey), + m.rootManager.chainParams, + ) + if err != nil { + return nil, err + } } return &managedAddress{ @@ -507,6 +551,14 @@ func newManagedAddressFromExtKey(s *ScopedKeyManager, return managedAddr, nil } +// clearTextScriptSetter is a non-exported interface to identify script types +// that allow their clear text script to be set. +type clearTextScriptSetter interface { + // setClearText sets the unencrypted script on the struct after + // unlocking/decrypting it. + setClearTextScript([]byte) +} + // baseScriptAddress represents the common fields of a pay-to-script-hash and // a pay-to-witness-script-hash address. type baseScriptAddress struct { @@ -518,6 +570,8 @@ type baseScriptAddress struct { scriptMutex sync.Mutex } +var _ clearTextScriptSetter = (*baseScriptAddress)(nil) + // unlock decrypts and stores the associated script. It will fail if the key is // invalid or the encrypted script is not available. The returned clear text // script will always be a copy that may be safely used by the caller without @@ -576,6 +630,13 @@ func (a *baseScriptAddress) Internal() bool { return false } +// setClearText sets the unencrypted script on the struct after unlocking/ +// decrypting it. +func (a *baseScriptAddress) setClearTextScript(script []byte) { + a.scriptClearText = make([]byte, len(script)) + copy(a.scriptClearText, script) +} + // scriptAddress represents a pay-to-script-hash address. type scriptAddress struct { baseScriptAddress @@ -750,38 +811,110 @@ var _ ManagedScriptAddress = (*witnessScriptAddress)(nil) // newWitnessScriptAddress initializes and returns a new // pay-to-witness-script-hash address. -func newWitnessScriptAddress(m *ScopedKeyManager, account uint32, scriptHash, +func newWitnessScriptAddress(m *ScopedKeyManager, account uint32, scriptIdent, scriptEncrypted []byte, witnessVersion byte, - isSecretScript bool) (*witnessScriptAddress, error) { - - var ( - address btcutil.Address - err error - ) + isSecretScript bool) (ManagedScriptAddress, error) { switch witnessVersion { - case 0x00: - address, err = btcutil.NewAddressWitnessScriptHash( - scriptHash, m.rootManager.chainParams, + case witnessVersionV0: + address, err := btcutil.NewAddressWitnessScriptHash( + scriptIdent, m.rootManager.chainParams, ) + if err != nil { + return nil, err + } - case 0x01: - address, err = btcutil.NewAddressTaproot( - scriptHash, m.rootManager.chainParams, + return &witnessScriptAddress{ + baseScriptAddress: baseScriptAddress{ + manager: m, + account: account, + scriptEncrypted: scriptEncrypted, + }, + address: address, + witnessVersion: witnessVersion, + isSecretScript: isSecretScript, + }, nil + + case witnessVersionV1: + address, err := btcutil.NewAddressTaproot( + scriptIdent, m.rootManager.chainParams, ) + if err != nil { + return nil, err + } + + // Lift the x-only coordinate of the tweaked public key. + tweakedPubKey, err := schnorr.ParsePubKey(scriptIdent) + if err != nil { + return nil, fmt.Errorf("error lifting public key from "+ + "script ident: %v", err) + } + + return &taprootScriptAddress{ + witnessScriptAddress: witnessScriptAddress{ + baseScriptAddress: baseScriptAddress{ + manager: m, + account: account, + scriptEncrypted: scriptEncrypted, + }, + address: address, + witnessVersion: witnessVersion, + isSecretScript: isSecretScript, + }, + TweakedPubKey: tweakedPubKey, + }, nil + + default: + return nil, fmt.Errorf("unsupported witness version %d", + witnessVersion) } +} + +// taprootScriptAddress represents a pay-to-taproot address that commits to a +// script. +type taprootScriptAddress struct { + witnessScriptAddress + + TweakedPubKey *btcec.PublicKey +} + +// Enforce taprootScriptAddress satisfies the ManagedTaprootScriptAddress +// interface. +var _ ManagedTaprootScriptAddress = (*taprootScriptAddress)(nil) + +// AddrType returns the address type of the managed address. This can be used +// to quickly discern the address type without further processing +// +// This is part of the ManagedAddress interface implementation. +func (a *taprootScriptAddress) AddrType() AddressType { + return TaprootScript +} + +// Address returns the btcutil.Address which represents the managed address. +// This will be a pay-to-taproot address. +// +// This is part of the ManagedAddress interface implementation. +func (a *taprootScriptAddress) Address() btcutil.Address { + return a.address +} + +// AddrHash returns the script hash for the address. +// +// This is part of the ManagedAddress interface implementation. +func (a *taprootScriptAddress) AddrHash() []byte { + return schnorr.SerializePubKey(a.TweakedPubKey) +} + +// TaprootScript returns all the information needed to derive the script tree +// root hash needed to arrive at the tweaked taproot key. +func (a *taprootScriptAddress) TaprootScript() (*Tapscript, error) { + // Need to decrypt our internal script first. We need to be unlocked for + // this. + script, err := a.Script() if err != nil { return nil, err } - return &witnessScriptAddress{ - baseScriptAddress: baseScriptAddress{ - manager: m, - account: account, - scriptEncrypted: scriptEncrypted, - }, - address: address, - witnessVersion: witnessVersion, - isSecretScript: isSecretScript, - }, nil + // Decode the additional TLV encoded data. + return tlvDecodeTaprootTaprootScript(script) } diff --git a/waddrmgr/db.go b/waddrmgr/db.go index 12612de2e6..4a8fb9ee28 100644 --- a/waddrmgr/db.go +++ b/waddrmgr/db.go @@ -74,6 +74,7 @@ const ( adtImport addressType = 1 // not iota as they need to be stable for db adtScript addressType = 2 adtWitnessScript addressType = 3 + adtTaprootScript addressType = 4 ) // accountType represents a type of address stored in the database. @@ -1622,6 +1623,11 @@ func fetchAddressByHash(ns walletdb.ReadBucket, scope *KeyScope, return deserializeScriptAddress(row) case adtWitnessScript: return deserializeWitnessScriptAddress(row) + case adtTaprootScript: + // A taproot script address is just a normal script address that + // TLV encodes more stuff in the raw script part. But in the + // database we store the same fields. + return deserializeWitnessScriptAddress(row) } str := fmt.Sprintf("unsupported address type '%d'", row.addrType) @@ -1849,8 +1855,17 @@ func putWitnessScriptAddress(ns walletdb.ReadWriteBucket, scope *KeyScope, rawData := serializeWitnessScriptAddress( witnessVersion, isSecretScript, encryptedHash, encryptedScript, ) + + addrType := adtWitnessScript + if witnessVersion == witnessVersionV1 { + // A taproot script stores a TLV encoded blob of data in the + // raw data field. So we only really need to use a different + // storage type since all other fields stay the same. + addrType = adtTaprootScript + } + addrRow := dbAddressRow{ - addrType: adtWitnessScript, + addrType: addrType, account: account, addTime: uint64(time.Now().Unix()), syncStatus: status, diff --git a/waddrmgr/manager_test.go b/waddrmgr/manager_test.go index 9c12a311a9..ef04742c4c 100644 --- a/waddrmgr/manager_test.go +++ b/waddrmgr/manager_test.go @@ -16,6 +16,7 @@ import ( "time" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" @@ -892,6 +893,7 @@ func testImportScript(tc *testContext) bool { name string in []byte isWitness bool + isTaproot bool witnessVersion byte isSecretScript bool blockstamp BlockStamp @@ -973,18 +975,23 @@ func testImportScript(tc *testContext) bool { }, }, { - name: "p2tr multisig", - isWitness: true, + name: "p2tr tapscript with all tap leaves", + isTaproot: true, witnessVersion: 1, isSecretScript: true, - in: hexToBytes("52210305a662958b547fe25a71cd28fc7ef1c2" + - "ad4a79b12f34fc40137824b88e61199d21038552c09d9" + - "a709c8cbba6e472307d3f8383f46181895a76e01e258f" + - "09033b4a78210205ad9a838cff17d79fee2841bec72e9" + - "9b6fd4e62fd9214fcf845b1cf8438062053ae"), + // The encoded *Tapscript struct for a script with all + // tap script leaves known. + in: hexToBytes( + "0101000221c00ef94ee79c07cbd1988fffd6e6aea1e2" + + "5c3b033a2fd64fe14a9b955e5355f0c60346" + + "1d0101c0021876a914f6c97547d73156abb3" + + "00ae059905c4acaadd09dd88270101c00222" + + "200ef94ee79c07cbd1988fffd6e6aea1e25c" + + "3b033a2fd64fe14a9b955e5355f0c6ac", + ), expected: expectedAddr{ - address: "bc1pc57jdm7kcnufnc339fvy2caflj6lkfeqasdfghftl7dd77dfpresqu7vep", - addressHash: hexToBytes("c53d26efd6c4f899e2312a584563a9fcb5fb2720ec1a945d2bff9adf79a908f3"), + address: "bc1pu92qt24cl4spyp4rsj9sa3y4ma6a3fszgewcmway9f6f80vgnduq5lnd0u", + addressHash: hexToBytes("e15405aab8fd601206a3848b0ec495df75d8a602465d8dbba42a7493bd889b78"), internal: false, imported: true, compressed: true, @@ -1023,13 +1030,27 @@ func testImportScript(tc *testContext) bool { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) var err error - if test.isWitness { + switch { + case test.isWitness: addr, err = tc.manager.ImportWitnessScript( ns, test.in, &test.blockstamp, test.witnessVersion, test.isSecretScript, ) - } else { + + case test.isTaproot: + var script *Tapscript + script, err = tlvDecodeTaprootTaprootScript( + test.in, + ) + require.NoError(tc.t, err) + addr, err = tc.manager.ImportTaprootScript( + ns, script, &test.blockstamp, + test.witnessVersion, + test.isSecretScript, + ) + + default: addr, err = tc.manager.ImportScript( ns, test.in, &test.blockstamp, ) @@ -1068,10 +1089,21 @@ func testImportScript(tc *testContext) bool { scriptHash[:], chainParams, ) - case test.isWitness && test.witnessVersion == 1: - scriptHash := sha256.Sum256(test.in) + case test.isTaproot: + var ( + script *Tapscript + taprootKey *btcec.PublicKey + ) + script, err = tlvDecodeTaprootTaprootScript( + test.in, + ) + require.NoError(tc.t, err) + taprootKey, err = script.TaprootKey() + require.NoError(tc.t, err) + utilAddr, err = btcutil.NewAddressTaproot( - scriptHash[:], chainParams, + schnorr.SerializePubKey(taprootKey), + chainParams, ) default: @@ -2957,3 +2989,128 @@ func TestDeriveFromKeyPathCache(t *testing.T) { require.Equal(t, cachedKey.Serialize(), cachedKey2.Serialize()) require.Equal(t, derivedKey.Serialize(), cachedKey2.Serialize()) } + +// TestTaprootPubKeyDerivation tests that p2tr addresses can be derived from the +// scoped manager when using the BIP0086 key scope. +func TestTaprootPubKeyDerivation(t *testing.T) { + t.Parallel() + + teardown, db := emptyDB(t) + defer teardown() + + // From: https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki + rootKey, _ := hdkeychain.NewKeyFromString( + "xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLi" + + "sriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu", + ) + + // We'll start the test by creating a new root manager that will be + // used for the duration of the test. + var mgr *Manager + err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey) + if err != nil { + return err + } + err = Create( + ns, rootKey, pubPassphrase, privPassphrase, + &chaincfg.MainNetParams, fastScrypt, time.Time{}, + ) + if err != nil { + return err + } + mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + if err != nil { + return err + } + + return mgr.Unlock(ns, privPassphrase) + }) + require.NoError(t, err, "create/open: unexpected error: %v", err) + + defer mgr.Close() + + // Now that we have the manager created, we'll fetch one of the default + // scopes for usage within this test. + scopedMgr, err := mgr.FetchScopedKeyManager(KeyScopeBIP0086) + require.NoError( + t, err, "unable to fetch scope %v: %v", KeyScopeBIP0086, err, + ) + + externalPath := DerivationPath{ + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 0, + Index: 0, + } + internalPath := DerivationPath{ + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 1, + Index: 0, + } + + assertAddressDerivation( + t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) { + return scopedMgr.DeriveFromKeyPath(ns, externalPath) + }, + "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr", + ) + assertAddressDerivation( + t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) { + addrs, err := scopedMgr.NextExternalAddresses(ns, 0, 1) + if err != nil { + return nil, err + } + return addrs[0], nil + }, + "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr", + ) + assertAddressDerivation( + t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) { + return scopedMgr.LastExternalAddress(ns, 0) + }, + "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr", + ) + assertAddressDerivation( + t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) { + return scopedMgr.DeriveFromKeyPath(ns, internalPath) + }, + "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7", + ) + assertAddressDerivation( + t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) { + addrs, err := scopedMgr.NextInternalAddresses(ns, 0, 1) + if err != nil { + return nil, err + } + return addrs[0], nil + }, + "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7", + ) + assertAddressDerivation( + t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) { + return scopedMgr.LastInternalAddress(ns, 0) + }, + "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7", + ) +} + +// assertAddressDerivation makes sure the address derived in the given callback +// is the one that is expected. +func assertAddressDerivation(t *testing.T, db walletdb.DB, + fn func(walletdb.ReadWriteBucket) (ManagedAddress, error), + expectedAddr string) { + + var address ManagedAddress + err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + + var err error + address, err = fn(ns) + return err + }) + require.NoError(t, err, "unable to derive addr: %v", err) + + require.Equal(t, expectedAddr, address.Address().String()) +} diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index b9d33e6ae0..37a77632d7 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -7,6 +7,7 @@ import ( "sync" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" @@ -129,6 +130,31 @@ func (k KeyScope) String() string { return fmt.Sprintf("m/%v'/%v'", k.Purpose, k.Coin) } +// Identity is a closure that returns the identifier of an address. +type Identity func() []byte + +// ScriptHashIdentity returns the identity closure for a p2sh script. +func ScriptHashIdentity(script []byte) Identity { + return func() []byte { + return btcutil.Hash160(script) + } +} + +// WitnessScriptHashIdentity returns the identity closure for a p2wsh script. +func WitnessScriptHashIdentity(script []byte) Identity { + return func() []byte { + digest := sha256.Sum256(script) + return digest[:] + } +} + +// TaprootIdentity returns the identity closure for a p2tr script. +func TaprootIdentity(taprootKey *btcec.PublicKey) Identity { + return func() []byte { + return schnorr.SerializePubKey(taprootKey) + } +} + // ScopeAddrSchema is the address schema of a particular KeyScope. This will be // persisted within the database, and will be consulted when deriving any keys // for a particular scope to know how to encode the public keys as addresses. @@ -157,6 +183,13 @@ var ( Coin: 0, } + // KeyScopeBIP0086 is the key scope for BIP0086 derivation. BIP0086 + // will be used to derive all p2tr addresses. + KeyScopeBIP0086 = KeyScope{ + Purpose: 86, + Coin: 0, + } + // KeyScopeBIP0044 is the key scope for BIP0044 derivation. Legacy // wallets will only be able to use this key scope, and no keys beyond // it. @@ -170,6 +203,7 @@ var ( DefaultKeyScopes = []KeyScope{ KeyScopeBIP0049Plus, KeyScopeBIP0084, + KeyScopeBIP0086, KeyScopeBIP0044, } @@ -185,6 +219,10 @@ var ( ExternalAddrType: WitnessPubKey, InternalAddrType: WitnessPubKey, }, + KeyScopeBIP0086: { + ExternalAddrType: TaprootPubKey, + InternalAddrType: TaprootPubKey, + }, KeyScopeBIP0044: { InternalAddrType: PubKeyHash, ExternalAddrType: PubKeyHash, @@ -2053,7 +2091,9 @@ func (s *ScopedKeyManager) toImportedPublicManagedAddress( func (s *ScopedKeyManager) ImportScript(ns walletdb.ReadWriteBucket, script []byte, bs *BlockStamp) (ManagedScriptAddress, error) { - return s.importScriptAddress(ns, script, bs, Script, 0, true) + return s.importScriptAddress( + ns, ScriptHashIdentity(script), script, bs, Script, 0, true, + ) } // ImportWitnessScript imports a user-provided script into the address manager. @@ -2073,14 +2113,45 @@ func (s *ScopedKeyManager) ImportWitnessScript(ns walletdb.ReadWriteBucket, isSecretScript bool) (ManagedScriptAddress, error) { return s.importScriptAddress( - ns, script, bs, WitnessScript, witnessVersion, isSecretScript, + ns, WitnessScriptHashIdentity(script), script, bs, + WitnessScript, witnessVersion, isSecretScript, ) } +// ImportTaprootScript imports a user-provided taproot script into the address +// manager. The imported script will act as a pay-to-taproot address. +func (s *ScopedKeyManager) ImportTaprootScript(ns walletdb.ReadWriteBucket, + tapscript *Tapscript, bs *BlockStamp, witnessVersion byte, + isSecretScript bool) (ManagedTaprootScriptAddress, error) { + + // Make sure we have everything we need to calculate the script root and + // tweak the taproot key. + taprootKey, err := tapscript.TaprootKey() + if err != nil { + return nil, fmt.Errorf("error calculating script root: %v", err) + } + + script, err := tlvEncodeTaprootScript(tapscript) + if err != nil { + return nil, fmt.Errorf("error encoding taproot script: %v", err) + } + + managedAddr, err := s.importScriptAddress( + ns, TaprootIdentity(taprootKey), script, bs, + TaprootScript, witnessVersion, isSecretScript, + ) + if err != nil { + return nil, err + } + + // We know this is a taproot address at this point. + return managedAddr.(ManagedTaprootScriptAddress), nil +} + // importScriptAddress imports a new pay-to-script or pay-to-witness-script // address. func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, - script []byte, bs *BlockStamp, addrType AddressType, + identity Identity, script []byte, bs *BlockStamp, addrType AddressType, witnessVersion byte, isSecretScript bool) (ManagedScriptAddress, error) { @@ -2099,30 +2170,21 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, return nil, managerError(ErrWatchingOnly, errWatchingOnly, nil) } - // Witness script addresses use a SHA256. - var scriptHash []byte - switch addrType { - case WitnessScript: - digest := sha256.Sum256(script) - scriptHash = digest[:] - default: - scriptHash = btcutil.Hash160(script) - } - // Prevent duplicates. - alreadyExists := s.existsAddress(ns, scriptHash) + scriptIdent := identity() + alreadyExists := s.existsAddress(ns, scriptIdent) if alreadyExists { - str := fmt.Sprintf("address for script hash %x already exists", - scriptHash) + str := fmt.Sprintf("address for script hash/key %x already "+ + "exists", scriptIdent) return nil, managerError(ErrDuplicateAddress, str, nil) } - // Encrypt the script hash using the crypto public key so it is + // Encrypt the script hash/key using the crypto public key, so it is // accessible when the address manager is locked or watching-only. - encryptedHash, err := s.rootManager.cryptoKeyPub.Encrypt(scriptHash) + encryptedHash, err := s.rootManager.cryptoKeyPub.Encrypt(scriptIdent) if err != nil { - str := fmt.Sprintf("failed to encrypt script hash %x", - scriptHash) + str := fmt.Sprintf("failed to encrypt script hash/key %x", + scriptIdent) return nil, managerError(ErrCrypto, str, err) } @@ -2139,7 +2201,7 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, encryptedScript, err := cryptoKey.Encrypt(script) if err != nil { str := fmt.Sprintf("failed to encrypt script for %x", - scriptHash) + scriptIdent) return nil, managerError(ErrCrypto, str, err) } @@ -2155,16 +2217,16 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, // Save the new imported address to the db and update start block (if // needed) in a single transaction. switch addrType { - case WitnessScript: + case WitnessScript, TaprootScript: err = putWitnessScriptAddress( - ns, &s.scope, scriptHash, ImportedAddrAccount, ssNone, + ns, &s.scope, scriptIdent, ImportedAddrAccount, ssNone, witnessVersion, isSecretScript, encryptedHash, encryptedScript, ) default: err = putScriptAddress( - ns, &s.scope, scriptHash, ImportedAddrAccount, ssNone, + ns, &s.scope, scriptIdent, ImportedAddrAccount, ssNone, encryptedHash, encryptedScript, ) } @@ -2191,42 +2253,33 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, // when not a watching-only address manager, make a copy of the script // since it will be cleared on lock and the script the caller passed // should not be cleared out from under the caller. - var ( - managedAddr ManagedScriptAddress - baseScriptAddr *baseScriptAddress - ) + var managedAddr ManagedScriptAddress switch addrType { - case WitnessScript: - witnessAddr, err := newWitnessScriptAddress( - s, ImportedAddrAccount, scriptHash, encryptedScript, + case WitnessScript, TaprootScript: + managedAddr, err = newWitnessScriptAddress( + s, ImportedAddrAccount, scriptIdent, encryptedScript, witnessVersion, isSecretScript, ) - if err != nil { - return nil, err - } - managedAddr = witnessAddr - baseScriptAddr = &witnessAddr.baseScriptAddress default: - scriptAddr, err := newScriptAddress( - s, ImportedAddrAccount, scriptHash, encryptedScript, + managedAddr, err = newScriptAddress( + s, ImportedAddrAccount, scriptIdent, encryptedScript, ) - if err != nil { - return nil, err - } - managedAddr = scriptAddr - baseScriptAddr = &scriptAddr.baseScriptAddress + } + if err != nil { + return nil, err } // Even if the script is secret, we are currently unlocked, so we keep a // clear text copy of the script around to avoid decrypting it on each // access. - baseScriptAddr.scriptClearText = make([]byte, len(script)) - copy(baseScriptAddr.scriptClearText, script) + if cts, ok := managedAddr.(clearTextScriptSetter); ok { + cts.setClearTextScript(script) + } // Add the new managed address to the cache of recent addresses and // return it. - s.addrs[addrKey(scriptHash)] = managedAddr + s.addrs[addrKey(scriptIdent)] = managedAddr return managedAddr, nil } @@ -2413,7 +2466,7 @@ func (s *ScopedKeyManager) cloneKeyWithVersion(key *hdkeychain.ExtendedKey) ( switch net { case wire.MainNet: switch s.scope { - case KeyScopeBIP0044: + case KeyScopeBIP0044, KeyScopeBIP0086: version = HDVersionMainNetBIP0044 case KeyScopeBIP0049Plus: version = HDVersionMainNetBIP0049 @@ -2427,7 +2480,7 @@ func (s *ScopedKeyManager) cloneKeyWithVersion(key *hdkeychain.ExtendedKey) ( netparams.SigNetWire(s.rootManager.ChainParams()): switch s.scope { - case KeyScopeBIP0044: + case KeyScopeBIP0044, KeyScopeBIP0086: version = HDVersionTestNetBIP0044 case KeyScopeBIP0049Plus: version = HDVersionTestNetBIP0049 @@ -2439,7 +2492,7 @@ func (s *ScopedKeyManager) cloneKeyWithVersion(key *hdkeychain.ExtendedKey) ( case wire.SimNet: switch s.scope { - case KeyScopeBIP0044: + case KeyScopeBIP0044, KeyScopeBIP0086: version = HDVersionSimNetBIP0044 // We use the mainnet versions for simnet keys when the keys // belong to a key scope which simnet doesn't have a defined diff --git a/waddrmgr/tapscript.go b/waddrmgr/tapscript.go new file mode 100644 index 0000000000..67aab41900 --- /dev/null +++ b/waddrmgr/tapscript.go @@ -0,0 +1,80 @@ +package waddrmgr + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/txscript" +) + +// TapscriptType is a special type denoting the different variants of +// tapscripts. +type TapscriptType uint8 + +const ( + // TapscriptTypeFullTree is the type of tapscript that knows its full + // tree with all individual leaves present. + TapscriptTypeFullTree TapscriptType = 0 + + // TapscriptTypePartialReveal is the type of tapscript that only knows + // a single revealed leaf and the merkle/inclusion proof for the rest of + // the tree. + TapscriptTypePartialReveal TapscriptType = 1 +) + +// Tapscript is a struct that holds either a full taproot tapscript with all +// individual leaves or a single leaf and the corresponding proof to arrive at +// the root hash. +type Tapscript struct { + // Type is the type of the tapscript. + Type TapscriptType + + // ControlBlock houses the main information about the internal key and + // the resulting key's parity. And, in case of the + // TapscriptTypePartialReveal type, the control block also contains the + // inclusion proof and the leaf version for the revealed script. + ControlBlock *txscript.ControlBlock + + // Leaves is the full set of tap leaves in their proper order. This is + // only set if the Type is TapscriptTypeFullTree. + Leaves []txscript.TapLeaf + + // RevealedScript is the script of the single revealed script. Is only + // set if the Type is TapscriptTypePartialReveal. + RevealedScript []byte +} + +// TaprootKey calculates the tweaked taproot key from the given internal key and +// the tree information in this tapscript struct. If any information required to +// calculate the root hash is missing, this method returns an error. +func (t *Tapscript) TaprootKey() (*btcec.PublicKey, error) { + if t.ControlBlock == nil || t.ControlBlock.InternalKey == nil { + return nil, fmt.Errorf("internal key is missing") + } + + switch t.Type { + case TapscriptTypeFullTree: + if len(t.Leaves) == 0 { + return nil, fmt.Errorf("missing leaves") + } + + tree := txscript.AssembleTaprootScriptTree(t.Leaves...) + rootHash := tree.RootNode.TapHash() + return txscript.ComputeTaprootOutputKey( + t.ControlBlock.InternalKey, rootHash[:], + ), nil + + case TapscriptTypePartialReveal: + if len(t.RevealedScript) == 0 { + return nil, fmt.Errorf("revealed script missing") + } + + rootHash := t.ControlBlock.RootHash(t.RevealedScript) + return txscript.ComputeTaprootOutputKey( + t.ControlBlock.InternalKey, rootHash, + ), nil + + default: + return nil, fmt.Errorf("unknown tapscript type %d", t.Type) + } +} diff --git a/waddrmgr/tapscript_test.go b/waddrmgr/tapscript_test.go new file mode 100644 index 0000000000..27c9c2ea13 --- /dev/null +++ b/waddrmgr/tapscript_test.go @@ -0,0 +1,88 @@ +package waddrmgr + +import ( + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/txscript" + "github.com/stretchr/testify/require" +) + +var ( + testInternalKey, _ = btcec.ParsePubKey(hexToBytes( + "020ef94ee79c07cbd1988fffd6e6aea1e25c3b033a2fd64fe14a9b955e53" + + "55f0c6", + )) + + testScript1 = hexToBytes( + "76a914f6c97547d73156abb300ae059905c4acaadd09dd88", + ) + testScript2 = hexToBytes( + "200ef94ee79c07cbd1988fffd6e6aea1e25c3b033a2fd64fe14a9b955e53" + + "55f0c6ac", + ) + testScript1Proof = hexToBytes( + "6c2e4bb01e316abaaee288d69c06cc608cedefd6e1a06813786c4ec51b6e" + + "1d38", + ) + + testTaprootKey = hexToBytes( + "e15405aab8fd601206a3848b0ec495df75d8a602465d8dbba42a7493bd88" + + "9b78", + ) +) + +// TestTaprootKey tests that the taproot tweaked key can be calculated correctly +// for both a tree with all leaves known as well as a partially revealed tree +// with an inclusion/merkle proof. +func TestTaprootKey(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + given *Tapscript + expected []byte + }{{ + name: "full tree", + given: &Tapscript{ + Type: TapscriptTypeFullTree, + Leaves: []txscript.TapLeaf{ + txscript.NewBaseTapLeaf(testScript1), + txscript.NewBaseTapLeaf(testScript2), + }, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testInternalKey, + LeafVersion: txscript.BaseLeafVersion, + }, + }, + expected: testTaprootKey, + }, { + name: "partial tree with proof", + given: &Tapscript{ + Type: TapscriptTypePartialReveal, + RevealedScript: testScript2, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testInternalKey, + LeafVersion: txscript.BaseLeafVersion, + InclusionProof: testScript1Proof, + }, + }, + expected: testTaprootKey, + }} + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(tt *testing.T) { + taprootKey, err := tc.given.TaprootKey() + require.NoError(tt, err) + + require.Equal( + tt, tc.expected, schnorr.SerializePubKey( + taprootKey, + ), + ) + }) + } +} diff --git a/waddrmgr/tlv.go b/waddrmgr/tlv.go new file mode 100644 index 0000000000..bf5cfd228d --- /dev/null +++ b/waddrmgr/tlv.go @@ -0,0 +1,271 @@ +package waddrmgr + +import ( + "bytes" + "fmt" + "io" + + "github.com/btcsuite/btcd/txscript" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + typeTapscriptType tlv.Type = 1 + typeTapscriptControlBlock tlv.Type = 2 + typeTapscriptLeaves tlv.Type = 3 + typeTapscriptRevealedScript tlv.Type = 4 + + typeTapLeafVersion tlv.Type = 1 + typeTapLeafScript tlv.Type = 2 +) + +// tlvEncodeTaprootScript encodes the given internal key and full set of taproot +// script leaves into a byte slice encoded as a TLV stream. +func tlvEncodeTaprootScript(s *Tapscript) ([]byte, error) { + if s == nil { + return nil, fmt.Errorf("cannot encode nil script") + } + + typ := uint8(s.Type) + tlvRecords := []tlv.Record{ + tlv.MakePrimitiveRecord(typeTapscriptType, &typ), + } + + if s.ControlBlock != nil { + if s.ControlBlock.InternalKey == nil { + return nil, fmt.Errorf("control block is missing " + + "internal key") + } + + blockBytes, err := s.ControlBlock.ToBytes() + if err != nil { + return nil, fmt.Errorf("error encoding control block: "+ + "%v", err) + } + tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord( + typeTapscriptControlBlock, &blockBytes, + )) + } + + if len(s.Leaves) > 0 { + tlvRecords = append(tlvRecords, tlv.MakeDynamicRecord( + typeTapscriptLeaves, &s.Leaves, func() uint64 { + return recordSize(leavesEncoder, &s.Leaves) + }, leavesEncoder, leavesDecoder, + )) + } + + if len(s.RevealedScript) > 0 { + tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord( + typeTapscriptRevealedScript, &s.RevealedScript, + )) + } + + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + err = tlvStream.Encode(&buf) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// tlvDecodeTaprootTaprootScript decodes the given byte slice as a TLV stream +// and attempts to parse the taproot internal key and full set of leaves from +// it. +func tlvDecodeTaprootTaprootScript(tlvData []byte) (*Tapscript, error) { + + var ( + typ uint8 + controlBlockBytes []byte + s = &Tapscript{} + ) + + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord(typeTapscriptType, &typ), + tlv.MakePrimitiveRecord( + typeTapscriptControlBlock, &controlBlockBytes, + ), + tlv.MakeDynamicRecord( + typeTapscriptLeaves, &s.Leaves, func() uint64 { + return recordSize(leavesEncoder, &s.Leaves) + }, leavesEncoder, leavesDecoder, + ), + tlv.MakePrimitiveRecord( + typeTapscriptRevealedScript, &s.RevealedScript, + ), + ) + if err != nil { + return nil, err + } + + parsedTypes, err := tlvStream.DecodeWithParsedTypes(bytes.NewReader( + tlvData, + )) + if err != nil { + return nil, err + } + + s.Type = TapscriptType(typ) + if t, ok := parsedTypes[typeTapscriptControlBlock]; ok && t == nil { + s.ControlBlock, err = txscript.ParseControlBlock( + controlBlockBytes, + ) + if err != nil { + return nil, fmt.Errorf("error decoding control block: "+ + "%v", err) + } + } + + return s, nil +} + +// leavesEncoder is a custom TLV decoder for a slice of tap leaf records. +func leavesEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + if v, ok := val.(*[]txscript.TapLeaf); ok { + for _, c := range *v { + leafVersion := uint8(c.LeafVersion) + tlvRecords := []tlv.Record{ + tlv.MakePrimitiveRecord( + typeTapLeafVersion, &leafVersion, + ), + } + + if len(c.Script) > 0 { + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord( + typeTapLeafScript, &c.Script, + ), + ) + + } + + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + var leafTLVBytes bytes.Buffer + err = tlvStream.Encode(&leafTLVBytes) + if err != nil { + return err + } + + // We encode the record with a varint length followed by + // the _raw_ TLV bytes. + tlvLen := uint64(len(leafTLVBytes.Bytes())) + if err := tlv.WriteVarInt(w, tlvLen, buf); err != nil { + return err + } + + _, err = w.Write(leafTLVBytes.Bytes()) + if err != nil { + return err + } + } + + return nil + } + + return tlv.NewTypeForEncodingErr(val, "[]txscript.TapLeaf") +} + +// leavesDecoder is a custom TLV decoder for a slice of tap leaf records. +func leavesDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + if v, ok := val.(*[]txscript.TapLeaf); ok { + var leaves []txscript.TapLeaf + + // Using the length information given, we'll create a new + // limited reader that'll return an EOF once the end has been + // reached so the stream stops consuming bytes. + innerTlvReader := io.LimitedReader{ + R: r, + N: int64(l), + } + + for { + // Read out the varint that encodes the size of this + // inner TLV record. + blobSize, err := tlv.ReadVarInt(r, buf) + if err == io.EOF { + break + } else if err != nil { + return err + } + + innerInnerTlvReader := io.LimitedReader{ + R: &innerTlvReader, + N: int64(blobSize), + } + + var ( + leafVersion uint8 + script []byte + ) + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord( + typeTapLeafVersion, &leafVersion, + ), + tlv.MakePrimitiveRecord( + typeTapLeafScript, &script, + ), + ) + if err != nil { + return err + } + + parsedTypes, err := tlvStream.DecodeWithParsedTypes( + &innerInnerTlvReader, + ) + if err != nil { + return err + } + + leaf := txscript.TapLeaf{ + LeafVersion: txscript.TapscriptLeafVersion( + leafVersion, + ), + } + + // Only set script when actually parsed to make + // difference between nil and empty slice work + // correctly. The parsedTypes entry must be nil if it + // was parsed fully. + if t, ok := parsedTypes[typeTapLeafScript]; ok && t == nil { + leaf.Script = script + } + + leaves = append(leaves, leaf) + } + + *v = leaves + return nil + } + + return tlv.NewTypeForDecodingErr(val, "[]txscript.TapLeaf", l, l) +} + +// recordSize returns the amount of bytes this TLV record will occupy when +// encoded. +func recordSize(encoder tlv.Encoder, v interface{}) uint64 { + var ( + b bytes.Buffer + buf [8]byte + ) + + // We know that encoding works since the tests pass in the build this + // file is checked into, so we'll simplify things and simply encode it + // ourselves then report the total amount of bytes used. + if err := encoder(&b, v, &buf); err != nil { + // This should never error out, but we log it just in case it + // does. + log.Errorf("encoding the record failed: %v", err) + } + + return uint64(len(b.Bytes())) +} diff --git a/waddrmgr/tlv_test.go b/waddrmgr/tlv_test.go new file mode 100644 index 0000000000..3235c815a6 --- /dev/null +++ b/waddrmgr/tlv_test.go @@ -0,0 +1,162 @@ +package waddrmgr + +import ( + "testing" + + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/txscript" + "github.com/stretchr/testify/require" +) + +var ( + testPubKey, _ = schnorr.ParsePubKey(hexToBytes( + "29faddf1254d490d6add49e2b08cf52b561038c72baec0edb3cfacff71" + + "ff1021", + )) + testScript = []byte{99, 88, 77, 66, 55, 44} + testProof = [32]byte{99, 88, 77, 66} +) + +// TestTlvEncodeDecode tests encoding and decoding of taproot script TLV data. +func TestTlvEncodeDecode(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + given *Tapscript + expected *Tapscript + expectedErrEncode string + expectedErrDecode string + }{{ + name: "nil", + expectedErrEncode: "cannot encode nil script", + }, { + name: "empty", + given: &Tapscript{}, + expected: &Tapscript{}, + }, { + name: "no leaves", + given: &Tapscript{ + Type: TapscriptTypeFullTree, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + }, + }, + expected: &Tapscript{ + Type: TapscriptTypeFullTree, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + InclusionProof: []byte{}, + }, + }, + }, { + name: "no pubkey", + given: &Tapscript{ + Type: TapscriptTypeFullTree, + ControlBlock: &txscript.ControlBlock{}, + }, + expectedErrEncode: "control block is missing internal key", + }, { + name: "empty leaf", + given: &Tapscript{ + Type: TapscriptTypeFullTree, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + }, + Leaves: []txscript.TapLeaf{{}}, + }, + expected: &Tapscript{ + Type: TapscriptTypeFullTree, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + InclusionProof: []byte{}, + }, + Leaves: []txscript.TapLeaf{{}}, + }, + }, { + name: "full key and leaves", + given: &Tapscript{ + Type: TapscriptTypeFullTree, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + }, + Leaves: []txscript.TapLeaf{ + txscript.NewBaseTapLeaf(testScript), + }, + }, + expected: &Tapscript{ + Type: TapscriptTypeFullTree, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + InclusionProof: []byte{}, + }, + Leaves: []txscript.TapLeaf{ + txscript.NewBaseTapLeaf(testScript), + }, + }, + }, { + name: "invalid proof", + given: &Tapscript{ + Type: TapscriptTypePartialReveal, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + InclusionProof: testScript, + }, + RevealedScript: testScript, + }, + expectedErrDecode: "error decoding control block: control " + + "block proof is not a multiple of 32: 6", + }, { + name: "inclusion proof no leaves", + given: &Tapscript{ + Type: TapscriptTypePartialReveal, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + InclusionProof: testProof[:], + }, + RevealedScript: testScript, + }, + expected: &Tapscript{ + Type: TapscriptTypePartialReveal, + ControlBlock: &txscript.ControlBlock{ + InternalKey: testPubKey, + InclusionProof: testProof[:], + }, + RevealedScript: testScript, + }, + }} + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(tt *testing.T) { + data, err := tlvEncodeTaprootScript(tc.given) + + if tc.expectedErrEncode != "" { + require.Error(tt, err) + require.Contains( + tt, err.Error(), tc.expectedErrEncode, + ) + + return + } + + require.NoError(tt, err) + require.NotEmpty(tt, data) + + decoded, err := tlvDecodeTaprootTaprootScript(data) + if tc.expectedErrDecode != "" { + require.Error(tt, err) + require.Contains( + tt, err.Error(), tc.expectedErrDecode, + ) + + return + } + + require.NoError(tt, err) + + require.Equal(tt, tc.expected, decoded) + }) + } +} diff --git a/wallet/createtx.go b/wallet/createtx.go index d2ce400a99..7d5fc4ad10 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -416,11 +416,22 @@ func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx, // validateMsgTx verifies transaction input scripts for tx. All previous output // scripts from outputs redeemed by the transaction, in the same order they are // spent, must be passed in the prevScripts slice. -func validateMsgTx(tx *wire.MsgTx, prevScripts [][]byte, inputValues []btcutil.Amount) error { - hashCache := txscript.NewTxSigHashes(tx) +func validateMsgTx(tx *wire.MsgTx, prevScripts [][]byte, + inputValues []btcutil.Amount) error { + + inputFetcher, err := txauthor.TXPrevOutFetcher( + tx, prevScripts, inputValues, + ) + if err != nil { + return err + } + + hashCache := txscript.NewTxSigHashes(tx, inputFetcher) for i, prevScript := range prevScripts { - vm, err := txscript.NewEngine(prevScript, tx, i, - txscript.StandardVerifyFlags, nil, hashCache, int64(inputValues[i])) + vm, err := txscript.NewEngine( + prevScript, tx, i, txscript.StandardVerifyFlags, nil, + hashCache, int64(inputValues[i]), inputFetcher, + ) if err != nil { return fmt.Errorf("cannot create script engine: %s", err) } diff --git a/wallet/import.go b/wallet/import.go index feb9f1f2ba..45d4ef2793 100644 --- a/wallet/import.go +++ b/wallet/import.go @@ -404,6 +404,59 @@ func (w *Wallet) ImportPublicKey(pubKey *btcec.PublicKey, return nil } +// ImportTaprootScript imports a user-provided taproot script into the address +// manager. The imported script will act as a pay-to-taproot address. +func (w *Wallet) ImportTaprootScript(scope waddrmgr.KeyScope, + tapscript *waddrmgr.Tapscript, bs *waddrmgr.BlockStamp, + witnessVersion byte, isSecretScript bool) (waddrmgr.ManagedAddress, + error) { + + manager, err := w.Manager.FetchScopedKeyManager(scope) + if err != nil { + return nil, err + } + + // The starting block for the key is the genesis block unless otherwise + // specified. + if bs == nil { + bs = &waddrmgr.BlockStamp{ + Hash: *w.chainParams.GenesisHash, + Height: 0, + Timestamp: w.chainParams.GenesisBlock.Header.Timestamp, + } + } else if bs.Timestamp.IsZero() { + // Only update the new birthday time from default value if we + // actually have timestamp info in the header. + header, err := w.chainClient.GetBlockHeader(&bs.Hash) + if err == nil { + bs.Timestamp = header.Timestamp + } + } + + // TODO: Perform rescan if requested. + var addr waddrmgr.ManagedAddress + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + addr, err = manager.ImportTaprootScript( + ns, tapscript, bs, witnessVersion, isSecretScript, + ) + return err + }) + if err != nil { + return nil, err + } + + log.Infof("Imported address %v", addr.Address()) + + err = w.chainClient.NotifyReceived([]btcutil.Address{addr.Address()}) + if err != nil { + return nil, fmt.Errorf("unable to subscribe for address "+ + "notifications: %v", err) + } + + return addr, nil +} + // ImportPrivateKey imports a private key to the wallet and writes the new // wallet to disk. // diff --git a/wallet/psbt.go b/wallet/psbt.go index 91a0457e33..7146b65bed 100644 --- a/wallet/psbt.go +++ b/wallet/psbt.go @@ -272,7 +272,7 @@ func (w *Wallet) FinalizePsbt(keyScope *waddrmgr.KeyScope, account uint32, // ones to sign. If there is any input without witness data that we // cannot sign because it's not our UTXO, this will be a hard failure. tx := packet.UnsignedTx - sigHashes := txscript.NewTxSigHashes(tx) + sigHashes := txscript.NewTxSigHashes(tx, PsbtPrevOutputFetcher(packet)) for idx, txIn := range tx.TxIn { in := packet.Inputs[idx] @@ -390,6 +390,39 @@ func (w *Wallet) FinalizePsbt(keyScope *waddrmgr.KeyScope, account uint32, return nil } +// PsbtPrevOutputFetcher returns a txscript.PrevOutFetcher built from the UTXO +// information in a PSBT packet. +func PsbtPrevOutputFetcher(packet *psbt.Packet) *txscript.MultiPrevOutFetcher { + fetcher := txscript.NewMultiPrevOutFetcher(nil) + for idx, txIn := range packet.UnsignedTx.TxIn { + in := packet.Inputs[idx] + + // Skip any input that has no UTXO. + if in.WitnessUtxo == nil && in.NonWitnessUtxo == nil { + continue + } + + if in.NonWitnessUtxo != nil { + prevIndex := txIn.PreviousOutPoint.Index + fetcher.AddPrevOut( + txIn.PreviousOutPoint, + in.NonWitnessUtxo.TxOut[prevIndex], + ) + + continue + } + + // Fall back to witness UTXO only for older wallets. + if in.WitnessUtxo != nil { + fetcher.AddPrevOut( + txIn.PreviousOutPoint, in.WitnessUtxo, + ) + } + } + + return fetcher +} + // constantInputSource creates an input source function that always returns the // static set of user-selected UTXOs. func constantInputSource(eligible []wtxmgr.Credit) txauthor.InputSource { diff --git a/wallet/signer.go b/wallet/signer.go index ffa1f5525a..38b8d787fb 100644 --- a/wallet/signer.go +++ b/wallet/signer.go @@ -67,7 +67,7 @@ func (w *Wallet) ScriptForOutput(output *wire.TxOut) ( return nil, nil, nil, err } - // Otherwise, this is a regular p2wkh output, so we include the + // Otherwise, this is a regular p2wkh or p2tr output, so we include the // witness program itself as the subscript to generate the proper // sighash digest. As part of the new sighash digest algorithm, the // p2wkh witness program will be expanded into a regular p2kh @@ -110,6 +110,21 @@ func (w *Wallet) ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut, } } + // We need to produce a Schnorr signature for p2tr key spend addresses. + if txscript.IsPayToTaproot(output.PkScript) { + // We can now generate a valid witness which will allow us to + // spend this output. + witnessScript, err := txscript.TaprootWitnessSignature( + tx, sigHashes, inputIndex, output.Value, + output.PkScript, hashType, privKey, + ) + if err != nil { + return nil, nil, err + } + + return witnessScript, nil, nil + } + // Generate a valid witness stack for the input. witnessScript, err := txscript.WitnessSignature( tx, sigHashes, inputIndex, output.Value, witnessProgram, diff --git a/wallet/signer_test.go b/wallet/signer_test.go index c96986990e..5684a95ad3 100644 --- a/wallet/signer_test.go +++ b/wallet/signer_test.go @@ -74,7 +74,10 @@ func runTestCase(t *testing.T, w *Wallet, scope waddrmgr.KeyScope, }}, TxOut: []*wire.TxOut{utxOut}, } - sigHashes := txscript.NewTxSigHashes(outgoingTx) + fetcher := txscript.NewCannedPrevOutputFetcher( + utxOut.PkScript, utxOut.Value, + ) + sigHashes := txscript.NewTxSigHashes(outgoingTx, fetcher) // Compute the input script to spend the UTXO now. witness, script, err := w.ComputeInputScript( diff --git a/wallet/txauthor/author.go b/wallet/txauthor/author.go index 28d9212dbf..486fbbc211 100644 --- a/wallet/txauthor/author.go +++ b/wallet/txauthor/author.go @@ -203,11 +203,16 @@ type SecretsSource interface { // are passed in prevPkScripts and the slice length must match the number of // inputs. Private keys and redeem scripts are looked up using a SecretsSource // based on the previous output script. -func AddAllInputScripts(tx *wire.MsgTx, prevPkScripts [][]byte, inputValues []btcutil.Amount, - secrets SecretsSource) error { +func AddAllInputScripts(tx *wire.MsgTx, prevPkScripts [][]byte, + inputValues []btcutil.Amount, secrets SecretsSource) error { + + inputFetcher, err := TXPrevOutFetcher(tx, prevPkScripts, inputValues) + if err != nil { + return err + } inputs := tx.TxIn - hashCache := txscript.NewTxSigHashes(tx) + hashCache := txscript.NewTxSigHashes(tx, inputFetcher) chainParams := secrets.ChainParams() if len(inputs) != len(prevPkScripts) { @@ -224,19 +229,32 @@ func AddAllInputScripts(tx *wire.MsgTx, prevPkScripts [][]byte, inputValues []bt // function which generates both the sigScript, and the witness // script. case txscript.IsPayToScriptHash(pkScript): - err := spendNestedWitnessPubKeyHash(inputs[i], pkScript, - int64(inputValues[i]), chainParams, secrets, - tx, hashCache, i) + err := spendNestedWitnessPubKeyHash( + inputs[i], pkScript, int64(inputValues[i]), + chainParams, secrets, tx, hashCache, i, + ) if err != nil { return err } + case txscript.IsPayToWitnessPubKeyHash(pkScript): - err := spendWitnessKeyHash(inputs[i], pkScript, - int64(inputValues[i]), chainParams, secrets, - tx, hashCache, i) + err := spendWitnessKeyHash( + inputs[i], pkScript, int64(inputValues[i]), + chainParams, secrets, tx, hashCache, i, + ) if err != nil { return err } + + case txscript.IsPayToTaproot(pkScript): + err := spendTaprootKey( + inputs[i], pkScript, int64(inputValues[i]), + chainParams, secrets, tx, hashCache, i, + ) + if err != nil { + return err + } + default: sigScript := inputs[i].SignatureScript script, err := txscript.SignTxOutput(chainParams, tx, i, @@ -304,6 +322,43 @@ func spendWitnessKeyHash(txIn *wire.TxIn, pkScript []byte, return nil } +// spendTaprootKey generates, and sets a valid witness for spending the passed +// pkScript with the specified input amount. The input amount *must* +// correspond to the output value of the previous pkScript, or else verification +// will fail since the new sighash digest algorithm defined in BIP0341 includes +// the input value in the sighash. +func spendTaprootKey(txIn *wire.TxIn, pkScript []byte, + inputValue int64, chainParams *chaincfg.Params, secrets SecretsSource, + tx *wire.MsgTx, hashCache *txscript.TxSigHashes, idx int) error { + + // First obtain the key pair associated with this p2tr address. If the + // pkScript is incorrect or derived from a different internal key or + // with a script root, we simply won't find a corresponding private key + // here. + _, addrs, _, err := txscript.ExtractPkScriptAddrs(pkScript, chainParams) + if err != nil { + return err + } + privKey, _, err := secrets.GetKey(addrs[0]) + if err != nil { + return err + } + + // We can now generate a valid witness which will allow us to spend this + // output. + witnessScript, err := txscript.TaprootWitnessSignature( + tx, hashCache, idx, inputValue, pkScript, + txscript.SigHashDefault, privKey, + ) + if err != nil { + return err + } + + txIn.Witness = witnessScript + + return nil +} + // spendNestedWitnessPubKey generates both a sigScript, and valid witness for // spending the passed pkScript with the specified input amount. The generated // sigScript is the version 0 p2wkh witness program corresponding to the queried @@ -371,5 +426,32 @@ func spendNestedWitnessPubKeyHash(txIn *wire.TxIn, pkScript []byte, // for each input of an authored transaction. Private keys and redeem scripts // are looked up using a SecretsSource based on the previous output script. func (tx *AuthoredTx) AddAllInputScripts(secrets SecretsSource) error { - return AddAllInputScripts(tx.Tx, tx.PrevScripts, tx.PrevInputValues, secrets) + return AddAllInputScripts( + tx.Tx, tx.PrevScripts, tx.PrevInputValues, secrets, + ) +} + +// TXPrevOutFetcher creates a txscript.PrevOutFetcher from a given slice of +// previous pk scripts and input values. +func TXPrevOutFetcher(tx *wire.MsgTx, prevPkScripts [][]byte, + inputValues []btcutil.Amount) (*txscript.MultiPrevOutFetcher, error) { + + if len(tx.TxIn) != len(prevPkScripts) { + return nil, errors.New("tx.TxIn and prevPkScripts slices " + + "must have equal length") + } + if len(tx.TxIn) != len(inputValues) { + return nil, errors.New("tx.TxIn and inputValues slices " + + "must have equal length") + } + + fetcher := txscript.NewMultiPrevOutFetcher(nil) + for idx, txin := range tx.TxIn { + fetcher.AddPrevOut(txin.PreviousOutPoint, &wire.TxOut{ + Value: int64(inputValues[idx]), + PkScript: prevPkScripts[idx], + }) + } + + return fetcher, nil } diff --git a/wallet/txauthor/go.mod b/wallet/txauthor/go.mod index 0a7c6cff4a..4ffb5b2a49 100644 --- a/wallet/txauthor/go.mod +++ b/wallet/txauthor/go.mod @@ -3,8 +3,8 @@ module github.com/btcsuite/btcwallet/wallet/txauthor go 1.12 require ( - github.com/btcsuite/btcd v0.22.0-beta.0.20220204213055-eaf0459ff879 - github.com/btcsuite/btcd/btcutil v1.1.0 + github.com/btcsuite/btcd v0.22.0-beta.0.20220316175102-8d5c75c28923 + github.com/btcsuite/btcd/btcutil v1.1.1 github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 ) diff --git a/wallet/txauthor/go.sum b/wallet/txauthor/go.sum index 3725274eea..af7a93a1f2 100644 --- a/wallet/txauthor/go.sum +++ b/wallet/txauthor/go.sum @@ -3,13 +3,20 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBA github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= 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 h1:nOLGKUr1yXZhQHKBk2Y3JH+LkBEMB7husoAuGO24m2E= github.com/btcsuite/btcd v0.22.0-beta.0.20220204213055-eaf0459ff879/go.mod h1:osu7EoKiL36UThEgzYPqdRaxeo0NU8VoXqgcnwpey0g= -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 v1.1.1 h1:hDcDaXiP0uEzR8Biqo2weECKqEw0uHDZ9ixIWevVQqY= +github.com/btcsuite/btcd/btcutil v1.1.1/go.mod h1:nbKlBMNm9FGsdvKvu0essceubPiAcI57pYBNnsLAa34= +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= @@ -19,14 +26,13 @@ github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 h1:wZnOolEAeNOHzHTnznw/wQv+j github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 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/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= @@ -45,6 +51,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= @@ -72,6 +80,13 @@ github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -123,3 +138,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/wallet/wallet.go b/wallet/wallet.go index 27a114e64f..583f769fcd 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -3259,6 +3259,7 @@ func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + inputFetcher := txscript.NewMultiPrevOutFetcher(nil) for i, txIn := range tx.TxIn { prevOutScript, ok := additionalPrevScripts[txIn.PreviousOutPoint] if !ok { @@ -3275,6 +3276,9 @@ func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, } prevOutScript = txDetails.MsgTx.TxOut[prevIndex].PkScript } + inputFetcher.AddPrevOut(txIn.PreviousOutPoint, &wire.TxOut{ + PkScript: prevOutScript, + }) // Set up our callbacks that we pass to txscript so it can // look up the appropriate keys and scripts by address. @@ -3353,8 +3357,11 @@ func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, // Either it was already signed or we just signed it. // Find out if it is completely satisfied or still needs more. - vm, err := txscript.NewEngine(prevOutScript, tx, i, - txscript.StandardVerifyFlags, nil, nil, 0) + vm, err := txscript.NewEngine( + prevOutScript, tx, i, + txscript.StandardVerifyFlags, nil, nil, 0, + inputFetcher, + ) if err == nil { err = vm.Execute() }