diff --git a/.gitignore b/.gitignore index 48b8bf9..4271e67 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ vendor/ + +# go proj +go.work* +.vscode/ + diff --git a/cmd/btcatomicswap/btcatomicswap b/cmd/btcatomicswap/btcatomicswap new file mode 100755 index 0000000..b4b1bf7 Binary files /dev/null and b/cmd/btcatomicswap/btcatomicswap differ diff --git a/cmd/btcatomicswap/go.mod b/cmd/btcatomicswap/go.mod index d2c7040..8a1bb66 100644 --- a/cmd/btcatomicswap/go.mod +++ b/cmd/btcatomicswap/go.mod @@ -1,12 +1,23 @@ module github.com/decred/atomicswap/cmd/btcatomicswap +go 1.20 + +require ( + github.com/btcsuite/btcd v0.24.0 + github.com/btcsuite/btcd/btcutil v1.1.5 + github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 + github.com/btcsuite/btcwallet/wallet/txrules v1.2.1 + golang.org/x/crypto v0.19.0 +) + require ( - github.com/btcsuite/btcd v0.0.0-20181013004428-67e573d211ac + github.com/aead/siphash v1.0.1 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect - github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a - github.com/btcsuite/btcwallet v0.0.0-20181017015332-c4dd27e481f9 github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 + github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/kkdai/bstream v1.0.0 // indirect + golang.org/x/sys v0.17.0 // indirect ) diff --git a/cmd/btcatomicswap/go.sum b/cmd/btcatomicswap/go.sum index d7f6a30..50a591e 100644 --- a/cmd/btcatomicswap/go.sum +++ b/cmd/btcatomicswap/go.sum @@ -1,16 +1,123 @@ -github.com/btcsuite/btcd v0.0.0-20181013004428-67e573d211ac h1:/zx+Hglw2JN/pwVam1Z8cTCTl4pWyrbvOn2oooqCQSs= -github.com/btcsuite/btcd v0.0.0-20181013004428-67e573d211ac/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ= +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +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.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= +github.com/btcsuite/btcd v0.24.0 h1:gL3uHE/IaFj6fcZSu03SvqPMSx7s/dPzfpG/atRwWdo= +github.com/btcsuite/btcd v0.24.0/go.mod h1:K4IDc1593s8jKXIF7yS7yCTSxrknB9z0STzc2j6XgE4= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= +github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/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-20180706230648-ab6388e0c60a h1:RQMUrEILyYJEoAT34XS/kLu40vC0+po/UfxrBBA4qZE= -github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/btcwallet v0.0.0-20181017015332-c4dd27e481f9 h1:YmgBq9XwdnZGBRF2DXq1a4qh/pfduvdQlzaDCjPV/jc= -github.com/btcsuite/btcwallet v0.0.0-20181017015332-c4dd27e481f9/go.mod h1:+q7/nPeXqu8jJ0ah0fcMOlWGZ2GeL2QoL6c7nCFUEVA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcwallet/wallet/txrules v1.2.1 h1:UZo7YRzdHbwhK7Rhv3PO9bXgTxiOH45edK5qdsdiatk= +github.com/btcsuite/btcwallet/wallet/txrules v1.2.1/go.mod h1:MVSqRkju/IGxImXYPfBkG65FgEZYA4fXchheILMVl8g= 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/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/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= +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= -golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= -golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8= +github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +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.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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/cmd/btcatomicswap/main.go b/cmd/btcatomicswap/main.go index 23ad543..a14f1e8 100644 --- a/cmd/btcatomicswap/main.go +++ b/cmd/btcatomicswap/main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 The Decred developers +// Copyright (c) 2024 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -9,6 +9,7 @@ import ( "bytes" "crypto/rand" "crypto/sha256" + "encoding/binary" "encoding/hex" "encoding/json" "errors" @@ -20,20 +21,19 @@ import ( "strings" "time" + "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" rpc "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/wallet/txrules" "golang.org/x/crypto/ripemd160" ) +// run script verifier const verify = true -const secretSize = 32 - const txVersion = 2 var ( @@ -46,6 +46,7 @@ var ( rpcuserFlag = flagset.String("rpcuser", "", "username for wallet RPC authentication") rpcpassFlag = flagset.String("rpcpass", "", "password for wallet RPC authentication") testnetFlag = flagset.Bool("testnet", false, "use testnet network") + regtestFlag = flagset.Bool("regtest", false, "use regtest network") ) // There are two directions that the atomic swap can be performed, as the @@ -98,12 +99,12 @@ type offlineCommand interface { } type initiateCmd struct { - cp2Addr *btcutil.AddressPubKeyHash + cp2Addr *btcutil.AddressWitnessPubKeyHash amount btcutil.Amount } type participateCmd struct { - cp1Addr *btcutil.AddressPubKeyHash + cp1Addr *btcutil.AddressWitnessPubKeyHash amount btcutil.Amount secretHash []byte } @@ -188,6 +189,8 @@ func run() (err error, showUsage bool) { if *testnetFlag { chainParams = &chaincfg.TestNet3Params + } else if *regtestFlag { + chainParams = &chaincfg.RegressionNetParams } var cmd command @@ -201,9 +204,9 @@ func run() (err error, showUsage bool) { return fmt.Errorf("participant address is not "+ "intended for use on %v", chainParams.Name), true } - cp2AddrP2PKH, ok := cp2Addr.(*btcutil.AddressPubKeyHash) + cp2AddrP2WPKH, ok := cp2Addr.(*btcutil.AddressWitnessPubKeyHash) if !ok { - return errors.New("participant address is not P2PKH"), true + return errors.New("participant address is not P2WPKH"), true } amountF64, err := strconv.ParseFloat(args[2], 64) @@ -215,7 +218,10 @@ func run() (err error, showUsage bool) { return err, true } - cmd = &initiateCmd{cp2Addr: cp2AddrP2PKH, amount: amount} + cmd = &initiateCmd{ + cp2Addr: cp2AddrP2WPKH, + amount: amount, + } case "participate": cp1Addr, err := btcutil.DecodeAddress(args[1], chainParams) @@ -226,9 +232,9 @@ func run() (err error, showUsage bool) { return fmt.Errorf("initiator address is not "+ "intended for use on %v", chainParams.Name), true } - cp1AddrP2PKH, ok := cp1Addr.(*btcutil.AddressPubKeyHash) + cp1AddrP2WPKH, ok := cp1Addr.(*btcutil.AddressWitnessPubKeyHash) if !ok { - return errors.New("initiator address is not P2PKH"), true + return errors.New("initiator address is not P2WPKH"), true } amountF64, err := strconv.ParseFloat(args[2], 64) @@ -248,7 +254,7 @@ func run() (err error, showUsage bool) { return errors.New("secret hash has wrong size"), true } - cmd = &participateCmd{cp1Addr: cp1AddrP2PKH, amount: amount, secretHash: secretHash} + cmd = &participateCmd{cp1Addr: cp1AddrP2WPKH, amount: amount, secretHash: secretHash} case "redeem": contract, err := hex.DecodeString(args[1]) @@ -380,27 +386,43 @@ func walletPort(params *chaincfg.Params) string { return "8332" case &chaincfg.TestNet3Params: return "18332" + case &chaincfg.RegressionNetParams: + return "18443" default: return "" } } -// createSig creates and returns the serialized raw signature and compressed -// pubkey for a transaction input signature. Due to limitations of the Bitcoin -// Core RPC API, this requires dumping a private key and signing in the client, -// rather than letting the wallet sign. -func createSig(tx *wire.MsgTx, idx int, pkScript []byte, addr btcutil.Address, - c *rpc.Client) (sig, pubkey []byte, err error) { +// createWitnessSig creates and returns the serialized raw signature and compressed +// pubkey for a transaction input signature. +// +// returns [sig][pubkey] +// +// Due to limitations of the Bitcoin Core RPC API, this requires dumping a private +// key and signing in the client, rather than letting the wallet sign. +func createWitnessSig( + tx *wire.MsgTx, + idx int, + contractValue int64, + contractPkScript []byte, + sigHashes *txscript.TxSigHashes, + addr btcutil.Address, + c *rpc.Client) ([]byte, []byte, error) { wif, err := c.DumpPrivKey(addr) if err != nil { return nil, nil, err } - sig, err = txscript.RawTxInSignature(tx, idx, pkScript, txscript.SigHashAll, wif.PrivKey) + privKey := wif.PrivKey + defer privKey.Zero() + + sig, err := txscript.RawTxInWitnessSignature(tx, sigHashes, idx, contractValue, + contractPkScript, txscript.SigHashAll, privKey) if err != nil { return nil, nil, err } - return sig, wif.PrivKey.PubKey().SerializeCompressed(), nil + + return sig, privKey.PubKey().SerializeCompressed(), nil } // fundRawTransaction calls the fundrawtransaction JSON-RPC method. It is @@ -586,8 +608,8 @@ func getFeePerKb(c *rpc.Client) (useFee, relayFee btcutil.Amount, err error) { // implemented manually as the rpcclient implementation always passes the // account parameter which was removed in Bitcoin Core 0.15. func getRawChangeAddress(c *rpc.Client) (btcutil.Address, error) { - params := []json.RawMessage{[]byte(`"legacy"`)} - rawResp, err := c.RawRequest("getrawchangeaddress", params) + // The default is bech32 for recent bitcoin rpc + rawResp, err := c.RawRequest("getrawchangeaddress", nil) if err != nil { return nil, err } @@ -604,8 +626,8 @@ func getRawChangeAddress(c *rpc.Client) (btcutil.Address, error) { return nil, fmt.Errorf("address %v is not intended for use on %v", addrStr, chainParams.Name) } - if _, ok := addr.(*btcutil.AddressPubKeyHash); !ok { - return nil, fmt.Errorf("getrawchangeaddress: address %v is not P2PKH", + if _, ok := addr.(*btcutil.AddressWitnessPubKeyHash); !ok { + return nil, fmt.Errorf("getrawchangeaddress: address %v is not P2WPKH", addr) } return addr, nil @@ -642,7 +664,7 @@ func promptPublishTx(c *rpc.Client, tx *wire.MsgTx, name string) error { // contractArgs specifies the common parameters used to create the initiator's // and participant's contract. type contractArgs struct { - them *btcutil.AddressPubKeyHash + them *btcutil.AddressWitnessPubKeyHash amount btcutil.Amount locktime int64 secretHash []byte @@ -652,7 +674,7 @@ type contractArgs struct { // payment transaction, as well as the transaction to perform a refund. type builtContract struct { contract []byte - contractP2SH btcutil.Address + contractP2WSH btcutil.Address contractTxHash *chainhash.Hash contractTx *wire.MsgTx contractFee btcutil.Amount @@ -660,31 +682,35 @@ type builtContract struct { refundFee btcutil.Amount } -// buildContract creates a contract for the parameters specified in args, using +// buildSegwitContract creates a contract for the parameters specified in args, using // wallet RPC to generate an internal address to redeem the refund and to sign // the payment to the contract transaction. -func buildContract(c *rpc.Client, args *contractArgs) (*builtContract, error) { +func buildSegwitContract(c *rpc.Client, args *contractArgs) (*builtContract, error) { refundAddr, err := getRawChangeAddress(c) if err != nil { return nil, fmt.Errorf("getrawchangeaddress: %v", err) } - refundAddrH, ok := refundAddr.(interface { - Hash160() *[ripemd160.Size]byte - }) - if !ok { + if _, ok := refundAddr.(interface{ Hash160() *[ripemd160.Size]byte }); !ok { return nil, errors.New("unable to create hash160 from change address") } - contract, err := atomicSwapContract(refundAddrH.Hash160(), args.them.Hash160(), - args.locktime, args.secretHash) + contract, err := MakeContract( + args.them, + refundAddr, + args.secretHash, + args.locktime, + true, //segwit + chainParams) if err != nil { return nil, err } - contractP2SH, err := btcutil.NewAddressScriptHash(contract, chainParams) + + contractHash := sha256Hash(contract) + contractP2WSH, err := btcutil.NewAddressWitnessScriptHash(contractHash, chainParams) if err != nil { return nil, err } - contractP2SHPkScript, err := txscript.PayToAddrScript(contractP2SH) + contractP2WSHPkScript, err := txscript.PayToAddrScript(contractP2WSH) if err != nil { return nil, err } @@ -694,13 +720,13 @@ func buildContract(c *rpc.Client, args *contractArgs) (*builtContract, error) { return nil, err } - unsignedContract := wire.NewMsgTx(txVersion) - unsignedContract.AddTxOut(wire.NewTxOut(int64(args.amount), contractP2SHPkScript)) - unsignedContract, contractFee, err := fundRawTransaction(c, unsignedContract, feePerKb) + unsignedContractTx := wire.NewMsgTx(txVersion) + unsignedContractTx.AddTxOut(wire.NewTxOut(int64(args.amount), contractP2WSHPkScript)) + unsignedContractTx, contractFee, err := fundRawTransaction(c, unsignedContractTx, feePerKb) if err != nil { return nil, fmt.Errorf("fundrawtransaction: %v", err) } - contractTx, complete, err := signRawTransaction(c, unsignedContract) + contractTx, complete, err := signRawTransaction(c, unsignedContractTx) if err != nil { return nil, fmt.Errorf("signrawtransaction: %v", err) } @@ -717,7 +743,7 @@ func buildContract(c *rpc.Client, args *contractArgs) (*builtContract, error) { return &builtContract{ contract, - contractP2SH, + contractP2WSH, &contractTxHash, contractTx, contractFee, @@ -729,25 +755,28 @@ func buildContract(c *rpc.Client, args *contractArgs) (*builtContract, error) { func buildRefund(c *rpc.Client, contract []byte, contractTx *wire.MsgTx, feePerKb, minFeePerKb btcutil.Amount) ( refundTx *wire.MsgTx, refundFee btcutil.Amount, err error) { - contractP2SH, err := btcutil.NewAddressScriptHash(contract, chainParams) + contractHash := sha256Hash(contract) + contractP2WSH, err := btcutil.NewAddressWitnessScriptHash(contractHash, chainParams) if err != nil { return nil, 0, err } - contractP2SHPkScript, err := txscript.PayToAddrScript(contractP2SH) + contractP2WSHPkScript, err := txscript.PayToAddrScript(contractP2WSH) if err != nil { return nil, 0, err } contractTxHash := contractTx.TxHash() + var contractValue = int64(0) contractOutPoint := wire.OutPoint{Hash: contractTxHash, Index: ^uint32(0)} for i, o := range contractTx.TxOut { - if bytes.Equal(o.PkScript, contractP2SHPkScript) { + if bytes.Equal(o.PkScript, contractP2WSHPkScript) { contractOutPoint.Index = uint32(i) + contractValue = o.Value break } } if contractOutPoint.Index == ^uint32(0) { - return nil, 0, errors.New("contract tx does not contain a P2SH contract payment") + return nil, 0, errors.New("contract tx does not contain a P2WSH contract payment") } refundAddress, err := getRawChangeAddress(c) @@ -759,45 +788,63 @@ func buildRefund(c *rpc.Client, contract []byte, contractTx *wire.MsgTx, feePerK return nil, 0, err } - pushes, err := txscript.ExtractAtomicSwapDataPushes(0, contract) + sender, _, locktime, _, err := ExtractSwapDetails( + contract, + true, // segwit + chainParams) if err != nil { - // expected to only be called with good input - panic(err) + return nil, 0, err } - refundAddr, err := btcutil.NewAddressPubKeyHash(pushes.RefundHash160[:], chainParams) + refundAddr, err := btcutil.NewAddressWitnessPubKeyHash(sender.ScriptAddress(), chainParams) if err != nil { return nil, 0, err } refundTx = wire.NewMsgTx(txVersion) - refundTx.LockTime = uint32(pushes.LockTime) - refundTx.AddTxOut(wire.NewTxOut(0, refundOutScript)) // amount set below - refundSize := estimateRefundSerializeSize(contract, refundTx.TxOut) + refundTx.LockTime = uint32(locktime) + // prepare single tx input + refundTx.AddTxIn(wire.NewTxIn(&contractOutPoint, nil, nil)) + refundTx.TxIn[0].Sequence = 0 + refundTx.TxIn[0].SignatureScript = nil + // with input but no output yet get vsize & fee + refundSize := refundTxSerialSizeEst(refundTx) refundFee = txrules.FeeForSerializeSize(feePerKb, refundSize) + // now add the single output + refundTx.AddTxOut(wire.NewTxOut(0, refundOutScript)) refundTx.TxOut[0].Value = contractTx.TxOut[contractOutPoint.Index].Value - int64(refundFee) + // check dust if txrules.IsDustOutput(refundTx.TxOut[0], minFeePerKb) { return nil, 0, fmt.Errorf("refund output value of %v is dust", btcutil.Amount(refundTx.TxOut[0].Value)) } - txIn := wire.NewTxIn(&contractOutPoint, nil, nil) - txIn.Sequence = 0 - refundTx.AddTxIn(txIn) + // NewTxSigHashes uses the PrevOutFetcher only for detecting a taproot + // output, so we can provide a dummy that always returns a wire.TxOut + // with a nil pkScript that so IsPayToTaproot returns false. + prevOutFetcher := new(txscript.CannedPrevOutputFetcher) + sigHashes := txscript.NewTxSigHashes(refundTx, prevOutFetcher) - refundSig, refundPubKey, err := createSig(refundTx, 0, contract, refundAddr, c) - if err != nil { - return nil, 0, err - } - refundSigScript, err := refundP2SHContract(contract, refundSig, refundPubKey) + sig, pubKey, err := createWitnessSig(refundTx, 0, contractValue, contract, sigHashes, refundAddr, c) if err != nil { return nil, 0, err } - refundTx.TxIn[0].SignatureScript = refundSigScript + refundTxWitness := RefundP2WSHContract(contract, sig, pubKey) + + refundTx.TxIn[0].Witness = refundTxWitness if verify { - e, err := txscript.NewEngine(contractTx.TxOut[contractOutPoint.Index].PkScript, - refundTx, 0, txscript.StandardVerifyFlags, txscript.NewSigCache(10), - txscript.NewTxSigHashes(refundTx), contractTx.TxOut[contractOutPoint.Index].Value) + e, err := txscript.NewEngine( + // pubkey script + contractTx.TxOut[contractOutPoint.Index].PkScript, + // refund transaction + refundTx, + // transaction input index + 0, + txscript.StandardVerifyFlags, + txscript.NewSigCache(10), + txscript.NewTxSigHashes(refundTx, prevOutFetcher), + contractValue, + prevOutFetcher) if err != nil { panic(err) } @@ -820,18 +867,24 @@ func calcFeePerKb(absoluteFee btcutil.Amount, serializeSize int) float64 { } func (cmd *initiateCmd) runCommand(c *rpc.Client) error { - var secret [secretSize]byte + var secret [SecretKeySize]byte _, err := rand.Read(secret[:]) if err != nil { return err } secretHash := sha256Hash(secret[:]) + fmt.Printf("Secret: %x\n", secret) + fmt.Printf("Secret hash: %x\n\n", secretHash) + // locktime after 500,000,000 (Tue Nov 5 00:53:20 1985 UTC) is interpreted // as a unix time rather than a block height. locktime := time.Now().Add(48 * time.Hour).Unix() + if *regtestFlag || *testnetFlag { + locktime = time.Now().Add(20 * time.Second).Unix() + } - b, err := buildContract(c, &contractArgs{ + b, err := buildSegwitContract(c, &contractArgs{ them: cmd.cp2Addr, amount: cmd.amount, locktime: locktime, @@ -847,9 +900,11 @@ func (cmd *initiateCmd) runCommand(c *rpc.Client) error { fmt.Printf("Secret: %x\n", secret) fmt.Printf("Secret hash: %x\n\n", secretHash) + fmt.Printf("Locktime: %d seconds - expires at unix time %d (%x))\n\n", + locktime-time.Now().Unix(), locktime, locktime) fmt.Printf("Contract fee: %v (%0.8f BTC/kB)\n", b.contractFee, contractFeePerKb) fmt.Printf("Refund fee: %v (%0.8f BTC/kB)\n\n", b.refundFee, refundFeePerKb) - fmt.Printf("Contract (%v):\n", b.contractP2SH) + fmt.Printf("Contract (%v):\n", b.contractP2WSH) fmt.Printf("%x\n\n", b.contract) var contractBuf bytes.Buffer contractBuf.Grow(b.contractTx.SerializeSize()) @@ -869,8 +924,11 @@ func (cmd *participateCmd) runCommand(c *rpc.Client) error { // locktime after 500,000,000 (Tue Nov 5 00:53:20 1985 UTC) is interpreted // as a unix time rather than a block height. locktime := time.Now().Add(24 * time.Hour).Unix() + if *regtestFlag || *testnetFlag { + locktime = time.Now().Add(20 * time.Second).Unix() + } - b, err := buildContract(c, &contractArgs{ + b, err := buildSegwitContract(c, &contractArgs{ them: cmd.cp1Addr, amount: cmd.amount, locktime: locktime, @@ -886,7 +944,9 @@ func (cmd *participateCmd) runCommand(c *rpc.Client) error { fmt.Printf("Contract fee: %v (%0.8f BTC/kB)\n", b.contractFee, contractFeePerKb) fmt.Printf("Refund fee: %v (%0.8f BTC/kB)\n\n", b.refundFee, refundFeePerKb) - fmt.Printf("Contract (%v):\n", b.contractP2SH) + fmt.Printf("Locktime: %d seconds - expires at unix time %d (%x))\n\n", + locktime-time.Now().Unix(), locktime, locktime) + fmt.Printf("Contract (%v):\n", b.contractP2WSH) fmt.Printf("%x\n\n", b.contract) var contractBuf bytes.Buffer contractBuf.Grow(b.contractTx.SerializeSize()) @@ -903,30 +963,35 @@ func (cmd *participateCmd) runCommand(c *rpc.Client) error { } func (cmd *redeemCmd) runCommand(c *rpc.Client) error { - pushes, err := txscript.ExtractAtomicSwapDataPushes(0, cmd.contract) + _, receiver, locktime, _, err := ExtractSwapDetails( + cmd.contract, + true, // segwit + chainParams) if err != nil { return err } - if pushes == nil { - return errors.New("contract is not an atomic swap script recognized by this tool") - } - recipientAddr, err := btcutil.NewAddressPubKeyHash(pushes.RecipientHash160[:], - chainParams) + + recipientAddr, err := btcutil.NewAddressWitnessPubKeyHash(receiver.ScriptAddress(), chainParams) if err != nil { return err } - contractHash := btcutil.Hash160(cmd.contract) - contractOut := -1 + + contractHash256 := sha256.Sum256(cmd.contract) + contractOutIdx := -1 + var contractValue = int64(0) for i, out := range cmd.contractTx.TxOut { - sc, addrs, _, _ := txscript.ExtractPkScriptAddrs(out.PkScript, chainParams) - if sc == txscript.ScriptHashTy && - bytes.Equal(addrs[0].(*btcutil.AddressScriptHash).Hash160()[:], contractHash) { - contractOut = i + sc, addrs, _, err := txscript.ExtractPkScriptAddrs(out.PkScript, chainParams) + if err != nil || sc != txscript.WitnessV0ScriptHashTy { // Pay to witness script hash + continue + } + if bytes.Equal(addrs[0].(*btcutil.AddressWitnessScriptHash).WitnessProgram()[:], contractHash256[:]) { + contractOutIdx = i + contractValue = out.Value break } } - if contractOut == -1 { - return errors.New("transaction does not contain a contract output") + if contractOutIdx == -1 { + return errors.New("contract transaction does not contain the contract output") } addr, err := getRawChangeAddress(c) @@ -941,7 +1006,7 @@ func (cmd *redeemCmd) runCommand(c *rpc.Client) error { contractTxHash := cmd.contractTx.TxHash() contractOutPoint := wire.OutPoint{ Hash: contractTxHash, - Index: uint32(contractOut), + Index: uint32(contractOutIdx), } feePerKb, minFeePerKb, err := getFeePerKb(c) @@ -950,25 +1015,34 @@ func (cmd *redeemCmd) runCommand(c *rpc.Client) error { } redeemTx := wire.NewMsgTx(txVersion) - redeemTx.LockTime = uint32(pushes.LockTime) + // prepare single tx input + redeemTx.LockTime = uint32(locktime) //ignored? redeemTx.AddTxIn(wire.NewTxIn(&contractOutPoint, nil, nil)) - redeemTx.AddTxOut(wire.NewTxOut(0, outScript)) // amount set below - redeemSize := estimateRedeemSerializeSize(cmd.contract, redeemTx.TxOut) + redeemTx.TxIn[0].SignatureScript = nil + // with input but no output yet get vsize & fee + redeemSize := redeemTxSerialSizeEst(redeemTx) fee := txrules.FeeForSerializeSize(feePerKb, redeemSize) - redeemTx.TxOut[0].Value = cmd.contractTx.TxOut[contractOut].Value - int64(fee) + // now add the single output + redeemTx.AddTxOut(wire.NewTxOut(0, outScript)) + redeemTx.TxOut[0].Value = cmd.contractTx.TxOut[contractOutIdx].Value - int64(fee) + // check dust if txrules.IsDustOutput(redeemTx.TxOut[0], minFeePerKb) { return fmt.Errorf("redeem output value of %v is dust", btcutil.Amount(redeemTx.TxOut[0].Value)) } - redeemSig, redeemPubKey, err := createSig(redeemTx, 0, cmd.contract, recipientAddr, c) - if err != nil { - return err - } - redeemSigScript, err := redeemP2SHContract(cmd.contract, redeemSig, redeemPubKey, cmd.secret) + // NewTxSigHashes uses the PrevOutFetcher only for detecting a taproot + // output, so we can provide a dummy that always returns a wire.TxOut + // with a nil pkScript so that IsPayToTaproot returns false. + prevOutFetcher := new(txscript.CannedPrevOutputFetcher) + sigHashes := txscript.NewTxSigHashes(redeemTx, prevOutFetcher) + + sig, pubKey, err := createWitnessSig(redeemTx, 0, contractValue, cmd.contract, sigHashes, recipientAddr, c) if err != nil { return err } - redeemTx.TxIn[0].SignatureScript = redeemSigScript + redeemTxWitness := RedeemP2WSHContract(cmd.contract, sig, pubKey, cmd.secret) + + redeemTx.TxIn[0].Witness = redeemTxWitness redeemTxHash := redeemTx.TxHash() redeemFeePerKb := calcFeePerKb(fee, redeemTx.SerializeSize()) @@ -976,17 +1050,28 @@ func (cmd *redeemCmd) runCommand(c *rpc.Client) error { var buf bytes.Buffer buf.Grow(redeemTx.SerializeSize()) redeemTx.Serialize(&buf) + fmt.Printf("Redeem value: %v \n\n", contractValue) fmt.Printf("Redeem fee: %v (%0.8f BTC/kB)\n\n", fee, redeemFeePerKb) fmt.Printf("Redeem transaction (%v):\n", &redeemTxHash) fmt.Printf("%x\n\n", buf.Bytes()) if verify { - e, err := txscript.NewEngine(cmd.contractTx.TxOut[contractOutPoint.Index].PkScript, - redeemTx, 0, txscript.StandardVerifyFlags, txscript.NewSigCache(10), - txscript.NewTxSigHashes(redeemTx), cmd.contractTx.TxOut[contractOut].Value) + e, err := txscript.NewEngine( + // pubkey script + cmd.contractTx.TxOut[contractOutPoint.Index].PkScript, + // refund transaction + redeemTx, + // transaction input index + 0, + txscript.StandardVerifyFlags, + txscript.NewSigCache(10), + txscript.NewTxSigHashes(redeemTx, prevOutFetcher), + contractValue, + prevOutFetcher) if err != nil { panic(err) } + err = e.Execute() if err != nil { panic(err) @@ -997,14 +1082,6 @@ func (cmd *redeemCmd) runCommand(c *rpc.Client) error { } func (cmd *refundCmd) runCommand(c *rpc.Client) error { - pushes, err := txscript.ExtractAtomicSwapDataPushes(0, cmd.contract) - if err != nil { - return err - } - if pushes == nil { - return errors.New("contract is not an atomic swap script recognized by this tool") - } - feePerKb, minFeePerKb, err := getFeePerKb(c) if err != nil { return err @@ -1033,20 +1110,41 @@ func (cmd *extractSecretCmd) runCommand(c *rpc.Client) error { } func (cmd *extractSecretCmd) runOfflineCommand() error { - // Loop over all pushed data from all inputs, searching for one that hashes - // to the expected hash. By searching through all data pushes, we avoid any - // issues that could be caused by the initiator redeeming the participant's - // contract with some "nonstandard" or unrecognized transaction or script + // Loop over witness items from all inputs, searching for one that hashes to + // the expected hash. + // We also try to avoid any issues that could be caused by the initiator redeeming + // the participant's contract with some "nonstandard" or unrecognized tx or script // type. for _, in := range cmd.redemptionTx.TxIn { - pushes, err := txscript.PushedData(in.SignatureScript) - if err != nil { - return err + // Check the witness stack + for _, w := range in.Witness { + // fast path + // check items of the correct length on the witness stack + if len(w) == SecretKeySize { + if bytes.Equal(sha256Hash(w), cmd.secretHash) { + fmt.Printf("Secret: %x\n", w) + return nil + } + } } - for _, push := range pushes { - if bytes.Equal(sha256Hash(push), cmd.secretHash) { - fmt.Printf("Secret: %x\n", push) - return nil + containsSecretKey := func(b []byte) bool { + last := len(b) - SecretKeySize + for i := 0; i <= last; i++ { + s := b[i : i+SecretKeySize] + if bytes.Equal(sha256Hash(s), cmd.secretHash) { + fmt.Printf("Secret: %x\n", s) + return true + } + } + return false + } + // check inside longer items on witness stack + for _, w := range in.Witness { + // an item on the witness stack + if len(w) >= SecretKeySize { + if containsSecretKey(w) { + return nil + } } } } @@ -1058,57 +1156,57 @@ func (cmd *auditContractCmd) runCommand(c *rpc.Client) error { } func (cmd *auditContractCmd) runOfflineCommand() error { - contractHash160 := btcutil.Hash160(cmd.contract) - contractOut := -1 + contractHash256 := sha256.Sum256(cmd.contract) + contractOutIdx := -1 + contractValue := int64(0) for i, out := range cmd.contractTx.TxOut { sc, addrs, _, err := txscript.ExtractPkScriptAddrs(out.PkScript, chainParams) - if err != nil || sc != txscript.ScriptHashTy { + if err != nil || sc != txscript.WitnessV0ScriptHashTy { // Pay to witness script hash continue } - if bytes.Equal(addrs[0].(*btcutil.AddressScriptHash).Hash160()[:], contractHash160) { - contractOut = i + if bytes.Equal(addrs[0].(*btcutil.AddressWitnessScriptHash).WitnessProgram()[:], contractHash256[:]) { + contractOutIdx = i + contractValue = out.Value break } } - if contractOut == -1 { + if contractOutIdx == -1 { return errors.New("transaction does not contain the contract output") } - pushes, err := txscript.ExtractAtomicSwapDataPushes(0, cmd.contract) + sender, receiver, locktime, secretHash, err := ExtractSwapDetails( + cmd.contract, + true, // segwit + chainParams) if err != nil { return err } - if pushes == nil { - return errors.New("contract is not an atomic swap script recognized by this tool") - } - if pushes.SecretSize != secretSize { - return fmt.Errorf("contract specifies strange secret size %v", pushes.SecretSize) - } - contractAddr, err := btcutil.NewAddressScriptHash(cmd.contract, chainParams) + if len(secretHash) != SecretKeySize { + return fmt.Errorf("contract specifies strange secret size %v", len(secretHash)) + } + contractAddr, err := btcutil.NewAddressWitnessScriptHash(contractHash256[:], chainParams) if err != nil { return err } - recipientAddr, err := btcutil.NewAddressPubKeyHash(pushes.RecipientHash160[:], - chainParams) + recipientAddr, err := btcutil.NewAddressWitnessPubKeyHash(receiver.ScriptAddress(), chainParams) if err != nil { return err } - refundAddr, err := btcutil.NewAddressPubKeyHash(pushes.RefundHash160[:], - chainParams) + refundAddr, err := btcutil.NewAddressWitnessPubKeyHash(sender.ScriptAddress(), chainParams) if err != nil { return err } - fmt.Printf("Contract address: %v\n", contractAddr) - fmt.Printf("Contract value: %v\n", btcutil.Amount(cmd.contractTx.TxOut[contractOut].Value)) - fmt.Printf("Recipient address: %v\n", recipientAddr) + fmt.Printf("Contract address: %v\n", contractAddr.String()) + fmt.Printf("Contract value: %v\n", btcutil.Amount(contractValue)) + fmt.Printf("Recipient address: %v\n", recipientAddr.String()) fmt.Printf("Author's refund address: %v\n\n", refundAddr) - fmt.Printf("Secret hash: %x\n\n", pushes.SecretHash[:]) + fmt.Printf("Secret hash: %x\n\n", secretHash[:]) - if pushes.LockTime >= int64(txscript.LockTimeThreshold) { - t := time.Unix(pushes.LockTime, 0) + if locktime >= txscript.LockTimeThreshold { + t := time.Unix(int64(locktime), 0) fmt.Printf("Locktime: %v\n", t.UTC()) reachedAt := time.Until(t).Truncate(time.Second) if reachedAt > 0 { @@ -1117,92 +1215,181 @@ func (cmd *auditContractCmd) runOfflineCommand() error { fmt.Printf("Contract refund time lock has expired\n") } } else { - fmt.Printf("Locktime: block %v\n", pushes.LockTime) + fmt.Printf("Locktime: block %v\n", locktime) } return nil } -// atomicSwapContract returns an output script that may be redeemed by one of -// two signature scripts: -// -// 1 -// -// 0 -// -// The first signature script is the normal redemption path done by the other -// party and requires the initiator's secret. The second signature script is -// the refund path performed by us, but the refund can only be performed after -// locktime. -func atomicSwapContract(pkhMe, pkhThem *[ripemd160.Size]byte, locktime int64, secretHash []byte) ([]byte, error) { - b := txscript.NewScriptBuilder() - - b.AddOp(txscript.OP_IF) // Normal redeem path - { - // Require initiator's secret to be a known length that the redeeming - // party can audit. This is used to prevent fraud attacks between two - // currencies that have different maximum data sizes. - b.AddOp(txscript.OP_SIZE) - b.AddInt64(secretSize) - b.AddOp(txscript.OP_EQUALVERIFY) - - // Require initiator's secret to be known to redeem the output. - b.AddOp(txscript.OP_SHA256) - b.AddData(secretHash) - b.AddOp(txscript.OP_EQUALVERIFY) - - // Verify their signature is being used to redeem the output. This - // would normally end with OP_EQUALVERIFY OP_CHECKSIG but this has been - // moved outside of the branch to save a couple bytes. - b.AddOp(txscript.OP_DUP) - b.AddOp(txscript.OP_HASH160) - b.AddData(pkhThem[:]) - } - b.AddOp(txscript.OP_ELSE) // Refund path - { - // Verify locktime and drop it off the stack (which is not done by - // CLTV). - b.AddInt64(locktime) - b.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY) - b.AddOp(txscript.OP_DROP) - - // Verify our signature is being used to redeem the output. This would - // normally end with OP_EQUALVERIFY OP_CHECKSIG but this has been moved - // outside of the branch to save a couple bytes. - b.AddOp(txscript.OP_DUP) - b.AddOp(txscript.OP_HASH160) - b.AddData(pkhMe[:]) - } - b.AddOp(txscript.OP_ENDIF) - - // Complete the signature check. - b.AddOp(txscript.OP_EQUALVERIFY) - b.AddOp(txscript.OP_CHECKSIG) - - return b.Script() +// ExtractSwapDetails extacts the sender and receiver addresses, locktime and +// secret hash from a swap contract. If the provided script is not a swap contract +// an error will be returned. +func ExtractSwapDetails(pkScript []byte, segwit bool, chainParams *chaincfg.Params) ( + sender, receiver btcutil.Address, lockTime uint64, secretHash []byte, err error) { + // A swap redemption sigScript is and satisfies the + // following swap contract. + // + // OP_IF + // OP_SIZE OP_DATA_1 secretSize OP_EQUALVERIFY OP_SHA256 OP_DATA_32 secretHash OP_EQUALVERIFY OP_DUP OP_HASH160 OP_DATA20 pkHashReceiver + // 1 + 1 + 1 + 1 + 1 + 1 + 32 + 1 + 1 + 1 + 1 + 20 + // OP_ELSE + // OP_DATA4 lockTime OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 OP_DATA_20 pkHashSender + // 1 + 4 + 1 + 1 + 1 + 1 + 1 + 20 + // OP_ENDIF + // OP_EQUALVERIFY + // OP_CHECKSIG + // + // 5 bytes if-else-endif-equalverify-checksig + // 1 + 1 + 1 + 1 + 1 + 1 + 32 + 1 + 1 + 1 + 1 + 20 = 62 bytes for redeem block + // 1 + 4 + 1 + 1 + 1 + 1 + 1 + 20 = 30 bytes for refund block + // 5 + 62 + 30 = 97 bytes + // + // Note that this allows for a secret size of up to 75 bytes, but the secret + // must be 32 bytes to be considered valid. + if len(pkScript) != SwapContractSize { + err = fmt.Errorf("incorrect swap contract length. expected %d, got %d", + SwapContractSize, len(pkScript)) + return + } + + if pkScript[0] == txscript.OP_IF && + pkScript[1] == txscript.OP_SIZE && + pkScript[2] == txscript.OP_DATA_1 && + // secretSize (1 bytes) + pkScript[4] == txscript.OP_EQUALVERIFY && + pkScript[5] == txscript.OP_SHA256 && + pkScript[6] == txscript.OP_DATA_32 && + // secretHash (32 bytes) + pkScript[39] == txscript.OP_EQUALVERIFY && + pkScript[40] == txscript.OP_DUP && + pkScript[41] == txscript.OP_HASH160 && + pkScript[42] == txscript.OP_DATA_20 && + // receiver's pkh (20 bytes) + pkScript[63] == txscript.OP_ELSE && + pkScript[64] == txscript.OP_DATA_4 && + // lockTime (4 bytes) + pkScript[69] == txscript.OP_CHECKLOCKTIMEVERIFY && + pkScript[70] == txscript.OP_DROP && + pkScript[71] == txscript.OP_DUP && + pkScript[72] == txscript.OP_HASH160 && + pkScript[73] == txscript.OP_DATA_20 && + // sender's pkh (20 bytes) + pkScript[94] == txscript.OP_ENDIF && + pkScript[95] == txscript.OP_EQUALVERIFY && + pkScript[96] == txscript.OP_CHECKSIG { + + if ssz := pkScript[3]; ssz != SecretKeySize { + return nil, nil, 0, nil, fmt.Errorf("invalid secret size %d", ssz) + } + + if segwit { + receiver, err = btcutil.NewAddressWitnessPubKeyHash(pkScript[43:63], chainParams) + if err != nil { + return nil, nil, 0, nil, fmt.Errorf("error decoding address from recipient's pubkey hash") + } + + sender, err = btcutil.NewAddressWitnessPubKeyHash(pkScript[74:94], chainParams) + if err != nil { + return nil, nil, 0, nil, fmt.Errorf("error decoding address from sender's pubkey hash") + } + } else { + receiver, err = btcutil.NewAddressPubKeyHash(pkScript[43:63], chainParams) + if err != nil { + return nil, nil, 0, nil, fmt.Errorf("error decoding address from recipient's pubkey hash") + } + + sender, err = btcutil.NewAddressPubKeyHash(pkScript[74:94], chainParams) + if err != nil { + return nil, nil, 0, nil, fmt.Errorf("error decoding address from sender's pubkey hash") + } + } + + lockTime = uint64(binary.LittleEndian.Uint32(pkScript[65:69])) + secretHash = pkScript[7:39] + + return + } + + err = fmt.Errorf("invalid swap contract") + return } -// redeemP2SHContract returns the signature script to redeem a contract output -// using the redeemer's signature and the initiator's secret. This function -// assumes P2SH and appends the contract as the final data push. -func redeemP2SHContract(contract, sig, pubkey, secret []byte) ([]byte, error) { - b := txscript.NewScriptBuilder() - b.AddData(sig) - b.AddData(pubkey) - b.AddData(secret) - b.AddInt64(1) - b.AddData(contract) - return b.Script() +// MakeContract creates a segwit atomic swap contract. The secretHash MUST +// be computed from a secret of length SecretKeySize bytes or the resulting +// contract will be invalid. +func MakeContract(rAddr, sAddr btcutil.Address, secretHash []byte, lockTime int64, segwit bool, chainParams *chaincfg.Params) ([]byte, error) { + if segwit { + _, ok := rAddr.(*btcutil.AddressWitnessPubKeyHash) + if !ok { + return nil, fmt.Errorf("recipient address %s is not a witness-pubkey-hash address", rAddr.String()) + } + _, ok = sAddr.(*btcutil.AddressWitnessPubKeyHash) + if !ok { + return nil, fmt.Errorf("sender address %s is not a witness-pubkey-hash address", sAddr.String()) + } + } else { + _, ok := rAddr.(*btcutil.AddressPubKeyHash) + if !ok { + return nil, fmt.Errorf("recipient address %s is not a pubkey-hash address", rAddr.String()) + } + _, ok = sAddr.(*btcutil.AddressPubKeyHash) + if !ok { + return nil, fmt.Errorf("sender address %s is not a pubkey-hash address", sAddr.String()) + } + } + if len(secretHash) != SecretHashSize { + return nil, fmt.Errorf("secret hash of length %d not supported", len(secretHash)) + } + + return txscript.NewScriptBuilder(). + AddOps([]byte{ + txscript.OP_IF, + txscript.OP_SIZE, + }).AddInt64(SecretKeySize). + AddOps([]byte{ + txscript.OP_EQUALVERIFY, + txscript.OP_SHA256, + }).AddData(secretHash). + AddOps([]byte{ + txscript.OP_EQUALVERIFY, + txscript.OP_DUP, + txscript.OP_HASH160, + }).AddData(rAddr.ScriptAddress()). + AddOp(txscript.OP_ELSE). + AddInt64(lockTime).AddOps([]byte{ + txscript.OP_CHECKLOCKTIMEVERIFY, + txscript.OP_DROP, + txscript.OP_DUP, + txscript.OP_HASH160, + }).AddData(sAddr.ScriptAddress()). + AddOps([]byte{ + txscript.OP_ENDIF, + txscript.OP_EQUALVERIFY, + txscript.OP_CHECKSIG, + }).Script() } -// refundP2SHContract returns the signature script to refund a contract output +// RefundP2WSHContract returns the witness to refund a contract output // using the contract author's signature after the locktime has been reached. -// This function assumes P2SH and appends the contract as the final data push. -func refundP2SHContract(contract, sig, pubkey []byte) ([]byte, error) { - b := txscript.NewScriptBuilder() - b.AddData(sig) - b.AddData(pubkey) - b.AddInt64(0) - b.AddData(contract) - return b.Script() +// This function assumes P2WSH and appends the contract as the final data push. +func RefundP2WSHContract(contract, sig, pubkey []byte) [][]byte { + return [][]byte{ + sig, + pubkey, + {}, + contract, + } +} + +// RedeemP2WSHContract returns the witness script to redeem a contract output +// using the redeemer's signature and the initiator's secret. This function +// assumes P2WSH and appends the contract as the final data push. +func RedeemP2WSHContract(contract, sig, pubkey, secret []byte) [][]byte { + return [][]byte{ + sig, + pubkey, + secret, + {0x01}, + contract, + } } diff --git a/cmd/btcatomicswap/sizeest.go b/cmd/btcatomicswap/sizeest.go index 799ffde..296f77f 100644 --- a/cmd/btcatomicswap/sizeest.go +++ b/cmd/btcatomicswap/sizeest.go @@ -6,86 +6,260 @@ package main import ( - "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" ) -// Worst case script and input/output size estimates. const ( - // redeemAtomicSwapSigScriptSize is the worst case (largest) serialize size - // of a transaction input script to redeem the atomic swap contract. This - // does not include final push for the contract itself. + // MaxCLTVScriptNum is the largest usable value for a CLTV lockTime. This + // will actually be stored in a 5-byte ScriptNum since they have a sign bit, + // however, it is not 2^39-1 since the spending transaction's nLocktime is + // an unsigned 32-bit integer and it must be at least the CLTV value. This + // establishes a maximum lock time of February 7, 2106. Any later requires + // using a block height instead of a unix epoch time stamp. + MaxCLTVScriptNum = 1<<32 - 1 // 0xffff_ffff a.k.a. 2^32-1 + + // SecretHashSize is the byte-length of the hash of the secret key used in an + // atomic swap. + SecretHashSize = 32 + + // SecretKeySize is the byte-length of the secret key used in an atomic swap. + SecretKeySize = 32 + + // ContractHashSize is the size of the script-hash for an atomic swap + // contract. + ContractHashSize = 20 + + // PubKeyLength is the length of a serialized compressed public key. + PubKeyLength = 33 + + // 4 bytes version + 4 bytes locktime + 2 bytes of varints for the number of + // transaction inputs and outputs + MinimumBlockOverHead = 10 + + // SwapContractSize is the worst case scenario size for a swap contract, + // which is the pk-script of the non-change output of an initialization + // transaction as used in execution of an atomic swap. + // See ExtractSwapDetails for a breakdown of the bytes. + SwapContractSize = 97 + + // DERSigLength is the maximum length of a DER encoded signature with a + // sighash type byte. + DERSigLength = 73 + + // RedeemSwapSigScriptSize is the worst case (largest) serialize size + // of a transaction signature script that redeems atomic swap output contract. + // It is calculated as: // // - OP_DATA_73 // - 72 bytes DER signature + 1 byte sighash // - OP_DATA_33 // - 33 bytes serialized compressed pubkey // - OP_DATA_32 - // - 32 bytes secret - // - OP_TRUE - redeemAtomicSwapSigScriptSize = 1 + 73 + 1 + 33 + 1 + 32 + 1 + // - 32 bytes secret key + // - OP_1 + // - varint 97 + // - 97 bytes redeem script + RedeemSwapSigScriptSize = 1 + DERSigLength + 1 + 33 + 1 + 32 + 1 + 2 + 97 - // refundAtomicSwapSigScriptSize is the worst case (largest) serialize size - // of a transaction input script that refunds a P2SH atomic swap output. - // This does not include final push for the contract itself. + // RefundSigScriptSize is the worst case (largest) serialize size + // of a transaction input script that refunds a compressed P2PKH output. + // It is calculated as: // // - OP_DATA_73 // - 72 bytes DER signature + 1 byte sighash // - OP_DATA_33 // - 33 bytes serialized compressed pubkey - // - OP_FALSE - refundAtomicSwapSigScriptSize = 1 + 73 + 1 + 33 + 1 -) + // - OP_0 + // - varint 97 => OP_PUSHDATA1(0x4c) + 0x61 + // - 97 bytes contract + RefundSigScriptSize = 1 + DERSigLength + 1 + 33 + 1 + 2 + 97 -func sumOutputSerializeSizes(outputs []*wire.TxOut) (serializeSize int) { - for _, txOut := range outputs { - serializeSize += txOut.SerializeSize() - } - return serializeSize -} + // Overhead for a wire.TxIn. See wire.TxIn.SerializeSize. + // hash 32 bytes + index 4 bytes + sequence 4 bytes. + TxInOverhead = 32 + 4 + 4 // 40 + + // TxOutOverhead is the overhead associated with a transaction output. + // 8 bytes value + at least 1 byte varint script size + TxOutOverhead = 8 + 1 + + RedeemP2PKSigScriptSize = 1 + DERSigLength + + // RedeemP2PKHSigScriptSize is the worst case (largest) serialize size + // of a transaction input script that redeems a compressed P2PKH output. + // It is calculated as: + // + // - OP_DATA_73 + // - 72 bytes DER signature + 1 byte sighash + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + RedeemP2PKHSigScriptSize = 1 + DERSigLength + 1 + 33 // 108 + + // RedeemP2SHSigScriptSize does not include the redeem script. + //RedeemP2SHSigScriptSize = 1 + DERSigLength + 1 + 1 + 33 + 1 // + redeem script! + + // P2PKHPkScriptSize is the size of a transaction output script that + // pays to a compressed pubkey hash. It is calculated as: + // + // - OP_DUP + // - OP_HASH160 + // - OP_DATA_20 + // - 20 bytes pubkey hash + // - OP_EQUALVERIFY + // - OP_CHECKSIG + P2PKHPkScriptSize = 1 + 1 + 1 + 20 + 1 + 1 // 25 + + // P2PKHOutputSize is the size of the serialized P2PKH output. + P2PKHOutputSize = TxOutOverhead + P2PKHPkScriptSize // 9 + 25 = 34 + + // P2SHPkScriptSize is the size of a transaction output script that + // pays to a redeem script. It is calculated as: + // + // - OP_HASH160 + // - OP_DATA_20 + // - 20 bytes redeem script hash + // - OP_EQUAL + P2SHPkScriptSize = 1 + 1 + 20 + 1 + + // P2SHOutputSize is the size of the serialized P2SH output. + P2SHOutputSize = TxOutOverhead + P2SHPkScriptSize // 9 + 23 = 32 + + // P2WSHPkScriptSize is the size of a segwit transaction output script that + // pays to a redeem script. It is calculated as: + // + // - OP_0 + // - OP_DATA_32 + // - 32 bytes redeem script hash + P2WSHPkScriptSize = 1 + 1 + 32 + + // P2WSHOutputSize is the size of the serialized P2WSH output. + P2WSHOutputSize = TxOutOverhead + P2WSHPkScriptSize // 9 + 34 = 43 + + // RedeemP2PKHInputSize is the worst case (largest) serialize size of a + // transaction input redeeming a compressed P2PKH output. It is + // calculated as: + // + // - 32 bytes previous tx + // - 4 bytes output index + // - 4 bytes sequence + // - 1 byte compact int encoding value 108 + // - 108 bytes signature script + RedeemP2PKHInputSize = TxInOverhead + 1 + RedeemP2PKHSigScriptSize // 40 + 1 + 108 = 149 + + // RedeemP2WPKHInputSize is the worst case size of a transaction + // input redeeming a P2WPKH output. This does not account for witness data, + // which is considered at a lower weight for fee calculations. It is + // calculated as + // + // - 32 bytes previous tx + // - 4 bytes output index + // - 4 bytes sequence + // - 1 byte encoding empty redeem script + // - 0 bytes signature script + RedeemP2WPKHInputSize = TxInOverhead + 1 + + // RedeemP2WPKHInputWitnessWeight is the worst case weight of + // a witness for spending P2WPKH and nested P2WPKH outputs. It + // is calculated as: + // + // - 1 wu compact int encoding value 2 (number of items) + // - 1 wu compact int encoding value 73 + // - 72 wu DER signature + 1 wu sighash + // - 1 wu compact int encoding value 33 + // - 33 wu serialized compressed pubkey + // NOTE: witness data is not script. + RedeemP2WPKHInputWitnessWeight = 1 + 1 + DERSigLength + 1 + 33 // 109 + + // RedeemP2WPKHInputTotalSize is the worst case size of a transaction + // input redeeming a P2WPKH output and the corresponding witness data. + // It is calculated as: + // + // 41 vbytes base tx input + // 109wu witness = 28 vbytes + // total = 69 vbytes + RedeemP2WPKHInputTotalSize = RedeemP2WPKHInputSize + + (RedeemP2WPKHInputWitnessWeight+(witnessWeight-1))/witnessWeight + + // SigwitMarkerAndFlagWeight is the 2 bytes of overhead witness data + // added to every segwit transaction. + SegwitMarkerAndFlagWeight = 2 + + // RedeemP2WSHInputWitnessWeight depends on the number of redeem script and + // number of signatures. + // version + signatures + length of redeem script + redeem script + // RedeemP2WSHInputWitnessWeight = 1 + N*DERSigLength + 1 + (redeem script bytes) + + // P2WPKHPkScriptSize is the size of a transaction output script that + // pays to a witness pubkey hash. It is calculated as: + // + // - OP_0 + // - OP_DATA_20 + // - 20 bytes pubkey hash + P2WPKHPkScriptSize = 1 + 1 + 20 + + // P2WPKHOutputSize is the serialize size of a transaction output with a + // P2WPKH output script. It is calculated as: + // + // - 8 bytes output value + // - 1 byte compact int encoding value 22 + // - 22 bytes P2PKH output script + P2WPKHOutputSize = TxOutOverhead + P2WPKHPkScriptSize // 31 + + // MinimumTxOverhead is the size of an empty transaction. + // 4 bytes version + 4 bytes locktime + 2 bytes of varints for the number of + // transaction inputs and outputs + MinimumTxOverhead = 4 + 4 + 1 + 1 // 10 + + // InitTxSizeBase is the size of a standard serialized atomic swap + // initialization transaction with one change output and no inputs. This is + // MsgTx overhead + 1 P2PKH change output + 1 P2SH contract output. However, + // the change output might be P2WPKH, in which case it would be smaller. + InitTxSizeBase = MinimumTxOverhead + P2PKHOutputSize + P2SHOutputSize // 10 + 34 + 32 = 76 + // leaner with P2WPKH+P2SH outputs: 10 + 31 + 32 = 73 + + // InitTxSize is InitTxBaseSize + 1 P2PKH input + InitTxSize = InitTxSizeBase + RedeemP2PKHInputSize // 76 + 149 = 225 + // Varies greatly with some other input types, e.g nested witness (p2sh with + // p2wpkh redeem script): 23 byte scriptSig + 108 byte (75 vbyte) witness = ~50 + + // InitTxSizeBaseSegwit is the size of a standard serialized atomic swap + // initialization transaction with one change output and no inputs. The + // change output is assumed to be segwit. 10 + 31 + 43 = 84 + InitTxSizeBaseSegwit = MinimumTxOverhead + P2WPKHOutputSize + P2WSHOutputSize + + // InitTxSizeSegwit is InitTxSizeSegwit + 1 P2WPKH input. + // 84 vbytes base tx + // 41 vbytes base tx input + // 109wu witness + 2wu segwit marker and flag = 28 vbytes + // total = 153 vbytes + InitTxSizeSegwit = InitTxSizeBaseSegwit + RedeemP2WPKHInputSize + + (SegwitMarkerAndFlagWeight+RedeemP2WPKHInputWitnessWeight+(witnessWeight-1))/witnessWeight + + witnessWeight = 4 // github.com/btcsuite/btcd/blockchain.WitnessScaleFactor +) -// inputSize returns the size of the transaction input needed to include a -// signature script with size sigScriptSize. It is calculated as: -// -// - 32 bytes previous tx -// - 4 bytes output index -// - Compact int encoding sigScriptSize -// - sigScriptSize bytes signature script -// - 4 bytes sequence -func inputSize(sigScriptSize int) int { - return 32 + 4 + wire.VarIntSerializeSize(uint64(sigScriptSize)) + sigScriptSize + 4 +// msgTxVBytes retuns vbytes. Call with MsgTx + the input(s) defined but no output yet +func msgTxVBytes(msgTx *wire.MsgTx) uint64 { + baseSize := msgTx.SerializeSizeStripped() + totalSize := msgTx.SerializeSize() + txWeight := baseSize*(witnessWeight-1) + totalSize + // vbytes is ceil(tx_weight/4) + return uint64(txWeight+(witnessWeight-1)) / witnessWeight // +3 before / 4 to round up } -// estimateRedeemSerializeSize returns a worst case serialize size estimates for -// a transaction that redeems an atomic swap P2SH output. -func estimateRedeemSerializeSize(contract []byte, txOuts []*wire.TxOut) int { - contractPush, err := txscript.NewScriptBuilder().AddData(contract).Script() - if err != nil { - // Should never be hit since this script does exceed the limits. - panic(err) - } - contractPushSize := len(contractPush) - - // 12 additional bytes are for version, locktime and expiry. - return 12 + wire.VarIntSerializeSize(1) + - wire.VarIntSerializeSize(uint64(len(txOuts))) + - inputSize(redeemAtomicSwapSigScriptSize+contractPushSize) + - sumOutputSerializeSizes(txOuts) +// Refund worst case serial size estimates for fees - segwit +func refundTxSerialSizeEst(msgTx *wire.MsgTx) int { + size := msgTxVBytes(msgTx) + // include marker and flag weight. + witnessVBytes := uint64((RefundSigScriptSize + 2 + 3) / 4) + size += witnessVBytes + P2WPKHOutputSize + return int(size) } -// estimateRefundSerializeSize returns a worst case serialize size estimates for -// a transaction that refunds an atomic swap P2SH output. -func estimateRefundSerializeSize(contract []byte, txOuts []*wire.TxOut) int { - contractPush, err := txscript.NewScriptBuilder().AddData(contract).Script() - if err != nil { - // Should never be hit since this script does exceed the limits. - panic(err) - } - contractPushSize := len(contractPush) - - // 12 additional bytes are for version, locktime and expiry. - return 12 + wire.VarIntSerializeSize(1) + - wire.VarIntSerializeSize(uint64(len(txOuts))) + - inputSize(refundAtomicSwapSigScriptSize+contractPushSize) + - sumOutputSerializeSizes(txOuts) +// Redeem worst case serial size estimates for fees - segwit +func redeemTxSerialSizeEst(msgTx *wire.MsgTx) int { //241 + size := msgTxVBytes(msgTx) + // include marker and flag weight. + witnessVBytes := uint64((RedeemSwapSigScriptSize + 2 + 3) / 4) + size += witnessVBytes + P2WPKHOutputSize + return int(size) } diff --git a/cmd/btcatomicswap_legacy/go.mod b/cmd/btcatomicswap_legacy/go.mod new file mode 100644 index 0000000..d2c7040 --- /dev/null +++ b/cmd/btcatomicswap_legacy/go.mod @@ -0,0 +1,12 @@ +module github.com/decred/atomicswap/cmd/btcatomicswap + +require ( + github.com/btcsuite/btcd v0.0.0-20181013004428-67e573d211ac + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect + github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a + github.com/btcsuite/btcwallet v0.0.0-20181017015332-c4dd27e481f9 + github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect + github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 +) diff --git a/cmd/btcatomicswap_legacy/go.sum b/cmd/btcatomicswap_legacy/go.sum new file mode 100644 index 0000000..d7f6a30 --- /dev/null +++ b/cmd/btcatomicswap_legacy/go.sum @@ -0,0 +1,16 @@ +github.com/btcsuite/btcd v0.0.0-20181013004428-67e573d211ac h1:/zx+Hglw2JN/pwVam1Z8cTCTl4pWyrbvOn2oooqCQSs= +github.com/btcsuite/btcd v0.0.0-20181013004428-67e573d211ac/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ= +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-20180706230648-ab6388e0c60a h1:RQMUrEILyYJEoAT34XS/kLu40vC0+po/UfxrBBA4qZE= +github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcwallet v0.0.0-20181017015332-c4dd27e481f9 h1:YmgBq9XwdnZGBRF2DXq1a4qh/pfduvdQlzaDCjPV/jc= +github.com/btcsuite/btcwallet v0.0.0-20181017015332-c4dd27e481f9/go.mod h1:+q7/nPeXqu8jJ0ah0fcMOlWGZ2GeL2QoL6c7nCFUEVA= +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/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +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= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/cmd/btcatomicswap_legacy/main.go b/cmd/btcatomicswap_legacy/main.go new file mode 100644 index 0000000..b5b4054 --- /dev/null +++ b/cmd/btcatomicswap_legacy/main.go @@ -0,0 +1,1208 @@ +// Copyright (c) 2017 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import ( + "bufio" + "bytes" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "flag" + "fmt" + "net" + "os" + "strconv" + "strings" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + rpc "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/wallet/txrules" + "golang.org/x/crypto/ripemd160" +) + +const verify = true + +const secretSize = 32 + +const txVersion = 2 + +var ( + chainParams = &chaincfg.MainNetParams +) + +var ( + flagset = flag.NewFlagSet("", flag.ExitOnError) + connectFlag = flagset.String("s", "localhost", "host[:port] of Bitcoin Core wallet RPC server") + rpcuserFlag = flagset.String("rpcuser", "", "username for wallet RPC authentication") + rpcpassFlag = flagset.String("rpcpass", "", "password for wallet RPC authentication") + testnetFlag = flagset.Bool("testnet", false, "use testnet network") +) + +// There are two directions that the atomic swap can be performed, as the +// initiator can be on either chain. This tool only deals with creating the +// Bitcoin transactions for these swaps. A second tool should be used for the +// transaction on the other chain. Any chain can be used so long as it supports +// OP_SHA256 and OP_CHECKLOCKTIMEVERIFY. +// +// Example scenerios using bitcoin as the second chain: +// +// Scenerio 1: +// cp1 initiates (dcr) +// cp2 participates with cp1 H(S) (btc) +// cp1 redeems btc revealing S +// - must verify H(S) in contract is hash of known secret +// cp2 redeems dcr with S +// +// Scenerio 2: +// cp1 initiates (btc) +// cp2 participates with cp1 H(S) (dcr) +// cp1 redeems dcr revealing S +// - must verify H(S) in contract is hash of known secret +// cp2 redeems btc with S + +func init() { + flagset.Usage = func() { + fmt.Println("Usage: btcatomicswap [flags] cmd [cmd args]") + fmt.Println() + fmt.Println("Commands:") + fmt.Println(" initiate ") + fmt.Println(" participate ") + fmt.Println(" redeem ") + fmt.Println(" refund ") + fmt.Println(" extractsecret ") + fmt.Println(" auditcontract ") + fmt.Println() + fmt.Println("Flags:") + flagset.PrintDefaults() + } +} + +type command interface { + runCommand(*rpc.Client) error +} + +// offline commands don't require wallet RPC. +type offlineCommand interface { + command + runOfflineCommand() error +} + +type initiateCmd struct { + cp2Addr *btcutil.AddressPubKeyHash + amount btcutil.Amount +} + +type participateCmd struct { + cp1Addr *btcutil.AddressPubKeyHash + amount btcutil.Amount + secretHash []byte +} + +type redeemCmd struct { + contract []byte + contractTx *wire.MsgTx + secret []byte +} + +type refundCmd struct { + contract []byte + contractTx *wire.MsgTx +} + +type extractSecretCmd struct { + redemptionTx *wire.MsgTx + secretHash []byte +} + +type auditContractCmd struct { + contract []byte + contractTx *wire.MsgTx +} + +func main() { + err, showUsage := run() + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + if showUsage { + flagset.Usage() + } + if err != nil || showUsage { + os.Exit(1) + } +} + +func checkCmdArgLength(args []string, required int) (nArgs int) { + if len(args) < required { + return 0 + } + for i, arg := range args[:required] { + if len(arg) != 1 && strings.HasPrefix(arg, "-") { + return i + } + } + return required +} + +func run() (err error, showUsage bool) { + flagset.Parse(os.Args[1:]) + args := flagset.Args() + if len(args) == 0 { + return nil, true + } + cmdArgs := 0 + switch args[0] { + case "initiate": + cmdArgs = 2 + case "participate": + cmdArgs = 3 + case "redeem": + cmdArgs = 3 + case "refund": + cmdArgs = 2 + case "extractsecret": + cmdArgs = 2 + case "auditcontract": + cmdArgs = 2 + default: + return fmt.Errorf("unknown command %v", args[0]), true + } + nArgs := checkCmdArgLength(args[1:], cmdArgs) + flagset.Parse(args[1+nArgs:]) + if nArgs < cmdArgs { + return fmt.Errorf("%s: too few arguments", args[0]), true + } + if flagset.NArg() != 0 { + return fmt.Errorf("unexpected argument: %s", flagset.Arg(0)), true + } + + if *testnetFlag { + chainParams = &chaincfg.TestNet3Params + } + + var cmd command + switch args[0] { + case "initiate": + cp2Addr, err := btcutil.DecodeAddress(args[1], chainParams) + if err != nil { + return fmt.Errorf("failed to decode participant address: %v", err), true + } + if !cp2Addr.IsForNet(chainParams) { + return fmt.Errorf("participant address is not "+ + "intended for use on %v", chainParams.Name), true + } + cp2AddrP2PKH, ok := cp2Addr.(*btcutil.AddressPubKeyHash) + if !ok { + return errors.New("participant address is not P2PKH"), true + } + + amountF64, err := strconv.ParseFloat(args[2], 64) + if err != nil { + return fmt.Errorf("failed to decode amount: %v", err), true + } + amount, err := btcutil.NewAmount(amountF64) + if err != nil { + return err, true + } + + cmd = &initiateCmd{cp2Addr: cp2AddrP2PKH, amount: amount} + + case "participate": + cp1Addr, err := btcutil.DecodeAddress(args[1], chainParams) + if err != nil { + return fmt.Errorf("failed to decode initiator address: %v", err), true + } + if !cp1Addr.IsForNet(chainParams) { + return fmt.Errorf("initiator address is not "+ + "intended for use on %v", chainParams.Name), true + } + cp1AddrP2PKH, ok := cp1Addr.(*btcutil.AddressPubKeyHash) + if !ok { + return errors.New("initiator address is not P2PKH"), true + } + + amountF64, err := strconv.ParseFloat(args[2], 64) + if err != nil { + return fmt.Errorf("failed to decode amount: %v", err), true + } + amount, err := btcutil.NewAmount(amountF64) + if err != nil { + return err, true + } + + secretHash, err := hex.DecodeString(args[3]) + if err != nil { + return errors.New("secret hash must be hex encoded"), true + } + if len(secretHash) != sha256.Size { + return errors.New("secret hash has wrong size"), true + } + + cmd = &participateCmd{cp1Addr: cp1AddrP2PKH, amount: amount, secretHash: secretHash} + + case "redeem": + contract, err := hex.DecodeString(args[1]) + if err != nil { + return fmt.Errorf("failed to decode contract: %v", err), true + } + + contractTxBytes, err := hex.DecodeString(args[2]) + if err != nil { + return fmt.Errorf("failed to decode contract transaction: %v", err), true + } + var contractTx wire.MsgTx + err = contractTx.Deserialize(bytes.NewReader(contractTxBytes)) + if err != nil { + return fmt.Errorf("failed to decode contract transaction: %v", err), true + } + + secret, err := hex.DecodeString(args[3]) + if err != nil { + return fmt.Errorf("failed to decode secret: %v", err), true + } + + cmd = &redeemCmd{contract: contract, contractTx: &contractTx, secret: secret} + + case "refund": + contract, err := hex.DecodeString(args[1]) + if err != nil { + return fmt.Errorf("failed to decode contract: %v", err), true + } + + contractTxBytes, err := hex.DecodeString(args[2]) + if err != nil { + return fmt.Errorf("failed to decode contract transaction: %v", err), true + } + var contractTx wire.MsgTx + err = contractTx.Deserialize(bytes.NewReader(contractTxBytes)) + if err != nil { + return fmt.Errorf("failed to decode contract transaction: %v", err), true + } + + cmd = &refundCmd{contract: contract, contractTx: &contractTx} + + case "extractsecret": + redemptionTxBytes, err := hex.DecodeString(args[1]) + if err != nil { + return fmt.Errorf("failed to decode redemption transaction: %v", err), true + } + var redemptionTx wire.MsgTx + err = redemptionTx.Deserialize(bytes.NewReader(redemptionTxBytes)) + if err != nil { + return fmt.Errorf("failed to decode redemption transaction: %v", err), true + } + + secretHash, err := hex.DecodeString(args[2]) + if err != nil { + return errors.New("secret hash must be hex encoded"), true + } + if len(secretHash) != sha256.Size { + return errors.New("secret hash has wrong size"), true + } + + cmd = &extractSecretCmd{redemptionTx: &redemptionTx, secretHash: secretHash} + + case "auditcontract": + contract, err := hex.DecodeString(args[1]) + if err != nil { + return fmt.Errorf("failed to decode contract: %v", err), true + } + + contractTxBytes, err := hex.DecodeString(args[2]) + if err != nil { + return fmt.Errorf("failed to decode contract transaction: %v", err), true + } + var contractTx wire.MsgTx + err = contractTx.Deserialize(bytes.NewReader(contractTxBytes)) + if err != nil { + return fmt.Errorf("failed to decode contract transaction: %v", err), true + } + + cmd = &auditContractCmd{contract: contract, contractTx: &contractTx} + } + + // Offline commands don't need to talk to the wallet. + if cmd, ok := cmd.(offlineCommand); ok { + return cmd.runOfflineCommand(), false + } + + connect, err := normalizeAddress(*connectFlag, walletPort(chainParams)) + if err != nil { + return fmt.Errorf("wallet server address: %v", err), true + } + + connConfig := &rpc.ConnConfig{ + Host: connect, + User: *rpcuserFlag, + Pass: *rpcpassFlag, + DisableTLS: true, + HTTPPostMode: true, + } + client, err := rpc.New(connConfig, nil) + if err != nil { + return fmt.Errorf("rpc connect: %v", err), false + } + defer func() { + client.Shutdown() + client.WaitForShutdown() + }() + + err = cmd.runCommand(client) + return err, false +} + +func normalizeAddress(addr string, defaultPort string) (hostport string, err error) { + host, port, origErr := net.SplitHostPort(addr) + if origErr == nil { + return net.JoinHostPort(host, port), nil + } + addr = net.JoinHostPort(addr, defaultPort) + _, _, err = net.SplitHostPort(addr) + if err != nil { + return "", origErr + } + return addr, nil +} + +func walletPort(params *chaincfg.Params) string { + switch params { + case &chaincfg.MainNetParams: + return "8332" + case &chaincfg.TestNet3Params: + return "18332" + default: + return "" + } +} + +// createSig creates and returns the serialized raw signature and compressed +// pubkey for a transaction input signature. Due to limitations of the Bitcoin +// Core RPC API, this requires dumping a private key and signing in the client, +// rather than letting the wallet sign. +func createSig(tx *wire.MsgTx, idx int, pkScript []byte, addr btcutil.Address, + c *rpc.Client) (sig, pubkey []byte, err error) { + + wif, err := c.DumpPrivKey(addr) + if err != nil { + return nil, nil, err + } + sig, err = txscript.RawTxInSignature(tx, idx, pkScript, txscript.SigHashAll, wif.PrivKey) + if err != nil { + return nil, nil, err + } + return sig, wif.PrivKey.PubKey().SerializeCompressed(), nil +} + +// fundRawTransaction calls the fundrawtransaction JSON-RPC method. It is +// implemented manually as client support is currently missing from the +// btcd/rpcclient package. +func fundRawTransaction(c *rpc.Client, tx *wire.MsgTx, feePerKb btcutil.Amount) (fundedTx *wire.MsgTx, fee btcutil.Amount, err error) { + var buf bytes.Buffer + buf.Grow(tx.SerializeSize()) + tx.Serialize(&buf) + param0, err := json.Marshal(hex.EncodeToString(buf.Bytes())) + if err != nil { + return nil, 0, err + } + param1, err := json.Marshal(struct { + FeeRate float64 `json:"feeRate"` + }{ + FeeRate: feePerKb.ToBTC(), + }) + if err != nil { + return nil, 0, err + } + params := []json.RawMessage{param0, param1} + rawResp, err := c.RawRequest("fundrawtransaction", params) + if err != nil { + return nil, 0, err + } + var resp struct { + Hex string `json:"hex"` + Fee float64 `json:"fee"` + ChangePos float64 `json:"changepos"` + } + err = json.Unmarshal(rawResp, &resp) + if err != nil { + return nil, 0, err + } + fundedTxBytes, err := hex.DecodeString(resp.Hex) + if err != nil { + return nil, 0, err + } + fundedTx = &wire.MsgTx{} + err = fundedTx.Deserialize(bytes.NewReader(fundedTxBytes)) + if err != nil { + return nil, 0, err + } + feeAmount, err := btcutil.NewAmount(resp.Fee) + if err != nil { + return nil, 0, err + } + return fundedTx, feeAmount, nil +} + +// signRawTransaction calls the signRawTransaction JSON-RPC method. It is +// implemented manually as client support is currently outdated from the +// btcd/rpcclient package. +func signRawTransaction(c *rpc.Client, tx *wire.MsgTx) (fundedTx *wire.MsgTx, complete bool, err error) { + var buf bytes.Buffer + buf.Grow(tx.SerializeSize()) + tx.Serialize(&buf) + param, err := json.Marshal(hex.EncodeToString(buf.Bytes())) + if err != nil { + return nil, false, err + } + rawResp, err := c.RawRequest("signrawtransactionwithwallet", []json.RawMessage{param}) + if err != nil { + return nil, false, err + } + var resp struct { + Hex string `json:"hex"` + Complete bool `json:"complete"` + } + err = json.Unmarshal(rawResp, &resp) + if err != nil { + return nil, false, err + } + fundedTxBytes, err := hex.DecodeString(resp.Hex) + if err != nil { + return nil, false, err + } + fundedTx = &wire.MsgTx{} + err = fundedTx.Deserialize(bytes.NewReader(fundedTxBytes)) + if err != nil { + return nil, false, err + } + return fundedTx, resp.Complete, nil +} + +// sendRawTransaction calls the signRawTransaction JSON-RPC method. It is +// implemented manually as client support is currently outdated from the +// btcd/rpcclient package. +func sendRawTransaction(c *rpc.Client, tx *wire.MsgTx) (*chainhash.Hash, error) { + var buf bytes.Buffer + buf.Grow(tx.SerializeSize()) + tx.Serialize(&buf) + + param, err := json.Marshal(hex.EncodeToString(buf.Bytes())) + if err != nil { + return nil, err + } + hex, err := c.RawRequest("sendrawtransaction", []json.RawMessage{param}) + if err != nil { + return nil, err + } + s := string(hex) + // we need to remove quotes from the json response + s = s[1 : len(s)-1] + hash, err := chainhash.NewHashFromStr(s) + if err != nil { + return nil, err + } + + return hash, nil +} + +// getFeePerKb queries the wallet for the transaction relay fee/kB to use and +// the minimum mempool relay fee. It first tries to get the user-set fee in the +// wallet. If unset, it attempts to find an estimate using estimatefee 6. If +// both of these fail, it falls back to mempool relay fee policy. +func getFeePerKb(c *rpc.Client) (useFee, relayFee btcutil.Amount, err error) { + var netInfoResp struct { + RelayFee float64 `json:"relayfee"` + } + var walletInfoResp struct { + PayTxFee float64 `json:"paytxfee"` + } + var estimateResp struct { + FeeRate float64 `json:"feerate"` + } + + netInfoRawResp, err := c.RawRequest("getnetworkinfo", nil) + if err == nil { + err = json.Unmarshal(netInfoRawResp, &netInfoResp) + if err != nil { + return 0, 0, err + } + } + walletInfoRawResp, err := c.RawRequest("getwalletinfo", nil) + if err == nil { + err = json.Unmarshal(walletInfoRawResp, &walletInfoResp) + if err != nil { + return 0, 0, err + } + } + + relayFee, err = btcutil.NewAmount(netInfoResp.RelayFee) + if err != nil { + return 0, 0, err + } + payTxFee, err := btcutil.NewAmount(walletInfoResp.PayTxFee) + if err != nil { + return 0, 0, err + } + + // Use user-set wallet fee when set and not lower than the network relay + // fee. + if payTxFee != 0 { + maxFee := payTxFee + if relayFee > maxFee { + maxFee = relayFee + } + return maxFee, relayFee, nil + } + + params := []json.RawMessage{[]byte("6")} + estimateRawResp, err := c.RawRequest("estimatesmartfee", params) + if err != nil { + return 0, 0, err + } + + err = json.Unmarshal(estimateRawResp, &estimateResp) + if err == nil && estimateResp.FeeRate > 0 { + useFee, err = btcutil.NewAmount(estimateResp.FeeRate) + if relayFee > useFee { + useFee = relayFee + } + return useFee, relayFee, err + } + + fmt.Println("warning: falling back to mempool relay fee policy") + return relayFee, relayFee, nil +} + +// getRawChangeAddress calls the getrawchangeaddress JSON-RPC method. It is +// implemented manually as the rpcclient implementation always passes the +// account parameter which was removed in Bitcoin Core 0.15. +func getRawChangeAddress(c *rpc.Client) (btcutil.Address, error) { + params := []json.RawMessage{[]byte(`"legacy"`)} + rawResp, err := c.RawRequest("getrawchangeaddress", params) + if err != nil { + return nil, err + } + var addrStr string + err = json.Unmarshal(rawResp, &addrStr) + if err != nil { + return nil, err + } + addr, err := btcutil.DecodeAddress(addrStr, chainParams) + if err != nil { + return nil, err + } + if !addr.IsForNet(chainParams) { + return nil, fmt.Errorf("address %v is not intended for use on %v", + addrStr, chainParams.Name) + } + if _, ok := addr.(*btcutil.AddressPubKeyHash); !ok { + return nil, fmt.Errorf("getrawchangeaddress: address %v is not P2PKH", + addr) + } + return addr, nil +} + +func promptPublishTx(c *rpc.Client, tx *wire.MsgTx, name string) error { + reader := bufio.NewReader(os.Stdin) + for { + fmt.Printf("Publish %s transaction? [y/N] ", name) + answer, err := reader.ReadString('\n') + if err != nil { + return err + } + answer = strings.TrimSpace(strings.ToLower(answer)) + + switch answer { + case "y", "yes": + case "n", "no", "": + return nil + default: + fmt.Println("please answer y or n") + continue + } + + txHash, err := sendRawTransaction(c, tx) + if err != nil { + return fmt.Errorf("sendrawtransaction: %v", err) + } + fmt.Printf("Published %s transaction (%v)\n", name, txHash) + return nil + } +} + +// contractArgs specifies the common parameters used to create the initiator's +// and participant's contract. +type contractArgs struct { + them *btcutil.AddressPubKeyHash + amount btcutil.Amount + locktime int64 + secretHash []byte +} + +// builtContract houses the details regarding a contract and the contract +// payment transaction, as well as the transaction to perform a refund. +type builtContract struct { + contract []byte + contractP2SH btcutil.Address + contractTxHash *chainhash.Hash + contractTx *wire.MsgTx + contractFee btcutil.Amount + refundTx *wire.MsgTx + refundFee btcutil.Amount +} + +// buildContract creates a contract for the parameters specified in args, using +// wallet RPC to generate an internal address to redeem the refund and to sign +// the payment to the contract transaction. +func buildContract(c *rpc.Client, args *contractArgs) (*builtContract, error) { + refundAddr, err := getRawChangeAddress(c) + if err != nil { + return nil, fmt.Errorf("getrawchangeaddress: %v", err) + } + refundAddrH, ok := refundAddr.(interface { + Hash160() *[ripemd160.Size]byte + }) + if !ok { + return nil, errors.New("unable to create hash160 from change address") + } + + contract, err := atomicSwapContract(refundAddrH.Hash160(), args.them.Hash160(), + args.locktime, args.secretHash) + if err != nil { + return nil, err + } + contractP2SH, err := btcutil.NewAddressScriptHash(contract, chainParams) + if err != nil { + return nil, err + } + contractP2SHPkScript, err := txscript.PayToAddrScript(contractP2SH) + if err != nil { + return nil, err + } + + feePerKb, minFeePerKb, err := getFeePerKb(c) + if err != nil { + return nil, err + } + + unsignedContract := wire.NewMsgTx(txVersion) + unsignedContract.AddTxOut(wire.NewTxOut(int64(args.amount), contractP2SHPkScript)) + unsignedContract, contractFee, err := fundRawTransaction(c, unsignedContract, feePerKb) + if err != nil { + return nil, fmt.Errorf("fundrawtransaction: %v", err) + } + contractTx, complete, err := signRawTransaction(c, unsignedContract) + if err != nil { + return nil, fmt.Errorf("signrawtransaction: %v", err) + } + if !complete { + return nil, errors.New("signrawtransaction: failed to completely sign contract transaction") + } + + contractTxHash := contractTx.TxHash() + + refundTx, refundFee, err := buildRefund(c, contract, contractTx, feePerKb, minFeePerKb) + if err != nil { + return nil, err + } + + return &builtContract{ + contract, + contractP2SH, + &contractTxHash, + contractTx, + contractFee, + refundTx, + refundFee, + }, nil +} + +func buildRefund(c *rpc.Client, contract []byte, contractTx *wire.MsgTx, feePerKb, minFeePerKb btcutil.Amount) ( + refundTx *wire.MsgTx, refundFee btcutil.Amount, err error) { + + contractP2SH, err := btcutil.NewAddressScriptHash(contract, chainParams) + if err != nil { + return nil, 0, err + } + contractP2SHPkScript, err := txscript.PayToAddrScript(contractP2SH) + if err != nil { + return nil, 0, err + } + + contractTxHash := contractTx.TxHash() + contractOutPoint := wire.OutPoint{Hash: contractTxHash, Index: ^uint32(0)} + for i, o := range contractTx.TxOut { + if bytes.Equal(o.PkScript, contractP2SHPkScript) { + contractOutPoint.Index = uint32(i) + break + } + } + if contractOutPoint.Index == ^uint32(0) { + return nil, 0, errors.New("contract tx does not contain a P2SH contract payment") + } + + refundAddress, err := getRawChangeAddress(c) + if err != nil { + return nil, 0, fmt.Errorf("getrawchangeaddress: %v", err) + } + refundOutScript, err := txscript.PayToAddrScript(refundAddress) + if err != nil { + return nil, 0, err + } + + pushes, err := txscript.ExtractAtomicSwapDataPushes(0, contract) + if err != nil { + // expected to only be called with good input + panic(err) + } + + refundAddr, err := btcutil.NewAddressPubKeyHash(pushes.RefundHash160[:], chainParams) + if err != nil { + return nil, 0, err + } + + refundTx = wire.NewMsgTx(txVersion) + refundTx.LockTime = uint32(pushes.LockTime) + refundTx.AddTxOut(wire.NewTxOut(0, refundOutScript)) // amount set below + refundSize := estimateRefundSerializeSize(contract, refundTx.TxOut) + refundFee = txrules.FeeForSerializeSize(feePerKb, refundSize) + refundTx.TxOut[0].Value = contractTx.TxOut[contractOutPoint.Index].Value - int64(refundFee) + if txrules.IsDustOutput(refundTx.TxOut[0], minFeePerKb) { + return nil, 0, fmt.Errorf("refund output value of %v is dust", btcutil.Amount(refundTx.TxOut[0].Value)) + } + + txIn := wire.NewTxIn(&contractOutPoint, nil, nil) + txIn.Sequence = 0 + refundTx.AddTxIn(txIn) + + refundSig, refundPubKey, err := createSig(refundTx, 0, contract, refundAddr, c) + if err != nil { + return nil, 0, err + } + refundSigScript, err := refundP2SHContract(contract, refundSig, refundPubKey) + if err != nil { + return nil, 0, err + } + refundTx.TxIn[0].SignatureScript = refundSigScript + + if verify { + e, err := txscript.NewEngine(contractTx.TxOut[contractOutPoint.Index].PkScript, + refundTx, 0, txscript.StandardVerifyFlags, txscript.NewSigCache(10), + txscript.NewTxSigHashes(refundTx), contractTx.TxOut[contractOutPoint.Index].Value) + if err != nil { + panic(err) + } + err = e.Execute() + if err != nil { + panic(err) + } + } + + return refundTx, refundFee, nil +} + +func sha256Hash(x []byte) []byte { + h := sha256.Sum256(x) + return h[:] +} + +func calcFeePerKb(absoluteFee btcutil.Amount, serializeSize int) float64 { + return float64(absoluteFee) / float64(serializeSize) / 1e5 +} + +func (cmd *initiateCmd) runCommand(c *rpc.Client) error { + var secret [secretSize]byte + _, err := rand.Read(secret[:]) + if err != nil { + return err + } + secretHash := sha256Hash(secret[:]) + + // locktime after 500,000,000 (Tue Nov 5 00:53:20 1985 UTC) is interpreted + // as a unix time rather than a block height. + locktime := time.Now().Add(48 * time.Hour).Unix() + + b, err := buildContract(c, &contractArgs{ + them: cmd.cp2Addr, + amount: cmd.amount, + locktime: locktime, + secretHash: secretHash, + }) + if err != nil { + return err + } + + refundTxHash := b.refundTx.TxHash() + contractFeePerKb := calcFeePerKb(b.contractFee, b.contractTx.SerializeSize()) + refundFeePerKb := calcFeePerKb(b.refundFee, b.refundTx.SerializeSize()) + + fmt.Printf("Secret: %x\n", secret) + fmt.Printf("Secret hash: %x\n\n", secretHash) + fmt.Printf("Contract fee: %v (%0.8f BTC/kB)\n", b.contractFee, contractFeePerKb) + fmt.Printf("Refund fee: %v (%0.8f BTC/kB)\n\n", b.refundFee, refundFeePerKb) + fmt.Printf("Contract (%v):\n", b.contractP2SH) + fmt.Printf("%x\n\n", b.contract) + var contractBuf bytes.Buffer + contractBuf.Grow(b.contractTx.SerializeSize()) + b.contractTx.Serialize(&contractBuf) + fmt.Printf("Contract transaction (%v):\n", b.contractTxHash) + fmt.Printf("%x\n\n", contractBuf.Bytes()) + var refundBuf bytes.Buffer + refundBuf.Grow(b.refundTx.SerializeSize()) + b.refundTx.Serialize(&refundBuf) + fmt.Printf("Refund transaction (%v):\n", &refundTxHash) + fmt.Printf("%x\n\n", refundBuf.Bytes()) + + return promptPublishTx(c, b.contractTx, "contract") +} + +func (cmd *participateCmd) runCommand(c *rpc.Client) error { + // locktime after 500,000,000 (Tue Nov 5 00:53:20 1985 UTC) is interpreted + // as a unix time rather than a block height. + locktime := time.Now().Add(24 * time.Hour).Unix() + + b, err := buildContract(c, &contractArgs{ + them: cmd.cp1Addr, + amount: cmd.amount, + locktime: locktime, + secretHash: secretHash, + }) + if err != nil { + return err + } + + refundTxHash := b.refundTx.TxHash() + contractFeePerKb := calcFeePerKb(b.contractFee, b.contractTx.SerializeSize()) + refundFeePerKb := calcFeePerKb(b.refundFee, b.refundTx.SerializeSize()) + + fmt.Printf("Contract fee: %v (%0.8f BTC/kB)\n", b.contractFee, contractFeePerKb) + fmt.Printf("Refund fee: %v (%0.8f BTC/kB)\n\n", b.refundFee, refundFeePerKb) + fmt.Printf("Contract (%v):\n", b.contractP2SH) + fmt.Printf("%x\n\n", b.contract) + var contractBuf bytes.Buffer + contractBuf.Grow(b.contractTx.SerializeSize()) + b.contractTx.Serialize(&contractBuf) + fmt.Printf("Contract transaction (%v):\n", b.contractTxHash) + fmt.Printf("%x\n\n", contractBuf.Bytes()) + var refundBuf bytes.Buffer + refundBuf.Grow(b.refundTx.SerializeSize()) + b.refundTx.Serialize(&refundBuf) + fmt.Printf("Refund transaction (%v):\n", &refundTxHash) + fmt.Printf("%x\n\n", refundBuf.Bytes()) + + return promptPublishTx(c, b.contractTx, "contract") +} + +func (cmd *redeemCmd) runCommand(c *rpc.Client) error { + pushes, err := txscript.ExtractAtomicSwapDataPushes(0, cmd.contract) + if err != nil { + return err + } + if pushes == nil { + return errors.New("contract is not an atomic swap script recognized by this tool") + } + recipientAddr, err := btcutil.NewAddressPubKeyHash(pushes.RecipientHash160[:], + chainParams) + if err != nil { + return err + } + contractHash := btcutil.Hash160(cmd.contract) + contractOut := -1 + for i, out := range cmd.contractTx.TxOut { + sc, addrs, _, _ := txscript.ExtractPkScriptAddrs(out.PkScript, chainParams) + if sc == txscript.ScriptHashTy && + bytes.Equal(addrs[0].(*btcutil.AddressScriptHash).Hash160()[:], contractHash) { + contractOut = i + break + } + } + if contractOut == -1 { + return errors.New("transaction does not contain a contract output") + } + + addr, err := getRawChangeAddress(c) + if err != nil { + return fmt.Errorf("getrawchangeaddress: %v", err) + } + outScript, err := txscript.PayToAddrScript(addr) + if err != nil { + return err + } + + contractTxHash := cmd.contractTx.TxHash() + contractOutPoint := wire.OutPoint{ + Hash: contractTxHash, + Index: uint32(contractOut), + } + + feePerKb, minFeePerKb, err := getFeePerKb(c) + if err != nil { + return err + } + + redeemTx := wire.NewMsgTx(txVersion) + redeemTx.LockTime = uint32(pushes.LockTime) + redeemTx.AddTxIn(wire.NewTxIn(&contractOutPoint, nil, nil)) + redeemTx.AddTxOut(wire.NewTxOut(0, outScript)) // amount set below + redeemSize := estimateRedeemSerializeSize(cmd.contract, redeemTx.TxOut) + fee := txrules.FeeForSerializeSize(feePerKb, redeemSize) + redeemTx.TxOut[0].Value = cmd.contractTx.TxOut[contractOut].Value - int64(fee) + if txrules.IsDustOutput(redeemTx.TxOut[0], minFeePerKb) { + return fmt.Errorf("redeem output value of %v is dust", btcutil.Amount(redeemTx.TxOut[0].Value)) + } + + redeemSig, redeemPubKey, err := createSig(redeemTx, 0, cmd.contract, recipientAddr, c) + if err != nil { + return err + } + redeemSigScript, err := redeemP2SHContract(cmd.contract, redeemSig, redeemPubKey, cmd.secret) + if err != nil { + return err + } + redeemTx.TxIn[0].SignatureScript = redeemSigScript + + redeemTxHash := redeemTx.TxHash() + redeemFeePerKb := calcFeePerKb(fee, redeemTx.SerializeSize()) + + var buf bytes.Buffer + buf.Grow(redeemTx.SerializeSize()) + redeemTx.Serialize(&buf) + fmt.Printf("Redeem fee: %v (%0.8f BTC/kB)\n\n", fee, redeemFeePerKb) + fmt.Printf("Redeem transaction (%v):\n", &redeemTxHash) + fmt.Printf("%x\n\n", buf.Bytes()) + + if verify { + e, err := txscript.NewEngine(cmd.contractTx.TxOut[contractOutPoint.Index].PkScript, + redeemTx, 0, txscript.StandardVerifyFlags, txscript.NewSigCache(10), + txscript.NewTxSigHashes(redeemTx), cmd.contractTx.TxOut[contractOut].Value) + if err != nil { + panic(err) + } + err = e.Execute() + if err != nil { + panic(err) + } + } + + return promptPublishTx(c, redeemTx, "redeem") +} + +func (cmd *refundCmd) runCommand(c *rpc.Client) error { + pushes, err := txscript.ExtractAtomicSwapDataPushes(0, cmd.contract) + if err != nil { + return err + } + if pushes == nil { + return errors.New("contract is not an atomic swap script recognized by this tool") + } + + feePerKb, minFeePerKb, err := getFeePerKb(c) + if err != nil { + return err + } + + refundTx, refundFee, err := buildRefund(c, cmd.contract, cmd.contractTx, feePerKb, minFeePerKb) + if err != nil { + return err + } + refundTxHash := refundTx.TxHash() + var buf bytes.Buffer + buf.Grow(refundTx.SerializeSize()) + refundTx.Serialize(&buf) + + refundFeePerKb := calcFeePerKb(refundFee, refundTx.SerializeSize()) + + fmt.Printf("Refund fee: %v (%0.8f BTC/kB)\n\n", refundFee, refundFeePerKb) + fmt.Printf("Refund transaction (%v):\n", &refundTxHash) + fmt.Printf("%x\n\n", buf.Bytes()) + + return promptPublishTx(c, refundTx, "refund") +} + +func (cmd *extractSecretCmd) runCommand(c *rpc.Client) error { + return cmd.runOfflineCommand() +} + +func (cmd *extractSecretCmd) runOfflineCommand() error { + // Loop over all pushed data from all inputs, searching for one that hashes + // to the expected hash. By searching through all data pushes, we avoid any + // issues that could be caused by the initiator redeeming the participant's + // contract with some "nonstandard" or unrecognized transaction or script + // type. + for _, in := range cmd.redemptionTx.TxIn { + pushes, err := txscript.PushedData(in.SignatureScript) + if err != nil { + return err + } + for _, push := range pushes { + if bytes.Equal(sha256Hash(push), cmd.secretHash) { + fmt.Printf("Secret: %x\n", push) + return nil + } + } + } + return errors.New("transaction does not contain the secret") +} + +func (cmd *auditContractCmd) runCommand(c *rpc.Client) error { + return cmd.runOfflineCommand() +} + +func (cmd *auditContractCmd) runOfflineCommand() error { + contractHash160 := btcutil.Hash160(cmd.contract) + contractOut := -1 + for i, out := range cmd.contractTx.TxOut { + sc, addrs, _, err := txscript.ExtractPkScriptAddrs(out.PkScript, chainParams) + if err != nil || sc != txscript.ScriptHashTy { + continue + } + if bytes.Equal(addrs[0].(*btcutil.AddressScriptHash).Hash160()[:], contractHash160) { + contractOut = i + break + } + } + if contractOut == -1 { + return errors.New("transaction does not contain the contract output") + } + + pushes, err := txscript.ExtractAtomicSwapDataPushes(0, cmd.contract) + if err != nil { + return err + } + if pushes == nil { + return errors.New("contract is not an atomic swap script recognized by this tool") + } + if pushes.SecretSize != secretSize { + return fmt.Errorf("contract specifies strange secret size %v", pushes.SecretSize) + } + + contractAddr, err := btcutil.NewAddressScriptHash(cmd.contract, chainParams) + if err != nil { + return err + } + recipientAddr, err := btcutil.NewAddressPubKeyHash(pushes.RecipientHash160[:], + chainParams) + if err != nil { + return err + } + refundAddr, err := btcutil.NewAddressPubKeyHash(pushes.RefundHash160[:], + chainParams) + if err != nil { + return err + } + + fmt.Printf("Contract address: %v\n", contractAddr) + fmt.Printf("Contract value: %v\n", btcutil.Amount(cmd.contractTx.TxOut[contractOut].Value)) + fmt.Printf("Recipient address: %v\n", recipientAddr) + fmt.Printf("Author's refund address: %v\n\n", refundAddr) + + fmt.Printf("Secret hash: %x\n\n", pushes.SecretHash[:]) + + if pushes.LockTime >= int64(txscript.LockTimeThreshold) { + t := time.Unix(pushes.LockTime, 0) + fmt.Printf("Locktime: %v\n", t.UTC()) + reachedAt := time.Until(t).Truncate(time.Second) + if reachedAt > 0 { + fmt.Printf("Locktime reached in %v\n", reachedAt) + } else { + fmt.Printf("Contract refund time lock has expired\n") + } + } else { + fmt.Printf("Locktime: block %v\n", pushes.LockTime) + } + + return nil +} + +// atomicSwapContract returns an output script that may be redeemed by one of +// two signature scripts: +// +// 1 +// +// 0 +// +// The first signature script is the normal redemption path done by the other +// party and requires the initiator's secret. The second signature script is +// the refund path performed by us, but the refund can only be performed after +// locktime. +func atomicSwapContract(pkhMe, pkhThem *[ripemd160.Size]byte, locktime int64, secretHash []byte) ([]byte, error) { + b := txscript.NewScriptBuilder() + + b.AddOp(txscript.OP_IF) // Normal redeem path + { + // Require initiator's secret to be a known length that the redeeming + // party can audit. This is used to prevent fraud attacks between two + // currencies that have different maximum data sizes. + b.AddOp(txscript.OP_SIZE) + b.AddInt64(secretSize) + b.AddOp(txscript.OP_EQUALVERIFY) + + // Require initiator's secret to be known to redeem the output. + b.AddOp(txscript.OP_SHA256) + b.AddData(secretHash) + b.AddOp(txscript.OP_EQUALVERIFY) + + // Verify their signature is being used to redeem the output. This + // would normally end with OP_EQUALVERIFY OP_CHECKSIG but this has been + // moved outside of the branch to save a couple bytes. + b.AddOp(txscript.OP_DUP) + b.AddOp(txscript.OP_HASH160) + b.AddData(pkhThem[:]) + } + b.AddOp(txscript.OP_ELSE) // Refund path + { + // Verify locktime and drop it off the stack (which is not done by + // CLTV). + b.AddInt64(locktime) + b.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY) + b.AddOp(txscript.OP_DROP) + + // Verify our signature is being used to redeem the output. This would + // normally end with OP_EQUALVERIFY OP_CHECKSIG but this has been moved + // outside of the branch to save a couple bytes. + b.AddOp(txscript.OP_DUP) + b.AddOp(txscript.OP_HASH160) + b.AddData(pkhMe[:]) + } + b.AddOp(txscript.OP_ENDIF) + + // Complete the signature check. + b.AddOp(txscript.OP_EQUALVERIFY) + b.AddOp(txscript.OP_CHECKSIG) + + return b.Script() +} + +// redeemP2SHContract returns the signature script to redeem a contract output +// using the redeemer's signature and the initiator's secret. This function +// assumes P2SH and appends the contract as the final data push. +func redeemP2SHContract(contract, sig, pubkey, secret []byte) ([]byte, error) { + b := txscript.NewScriptBuilder() + b.AddData(sig) + b.AddData(pubkey) + b.AddData(secret) + b.AddInt64(1) + b.AddData(contract) + return b.Script() +} + +// refundP2SHContract returns the signature script to refund a contract output +// using the contract author's signature after the locktime has been reached. +// This function assumes P2SH and appends the contract as the final data push. +func refundP2SHContract(contract, sig, pubkey []byte) ([]byte, error) { + b := txscript.NewScriptBuilder() + b.AddData(sig) + b.AddData(pubkey) + b.AddInt64(0) + b.AddData(contract) + return b.Script() +} diff --git a/cmd/btcatomicswap_legacy/sizeest.go b/cmd/btcatomicswap_legacy/sizeest.go new file mode 100644 index 0000000..799ffde --- /dev/null +++ b/cmd/btcatomicswap_legacy/sizeest.go @@ -0,0 +1,91 @@ +// Copyright (c) 2016 The btcsuite developers +// Copyright (c) 2016-2017 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import ( + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" +) + +// Worst case script and input/output size estimates. +const ( + // redeemAtomicSwapSigScriptSize is the worst case (largest) serialize size + // of a transaction input script to redeem the atomic swap contract. This + // does not include final push for the contract itself. + // + // - OP_DATA_73 + // - 72 bytes DER signature + 1 byte sighash + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_DATA_32 + // - 32 bytes secret + // - OP_TRUE + redeemAtomicSwapSigScriptSize = 1 + 73 + 1 + 33 + 1 + 32 + 1 + + // refundAtomicSwapSigScriptSize is the worst case (largest) serialize size + // of a transaction input script that refunds a P2SH atomic swap output. + // This does not include final push for the contract itself. + // + // - OP_DATA_73 + // - 72 bytes DER signature + 1 byte sighash + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_FALSE + refundAtomicSwapSigScriptSize = 1 + 73 + 1 + 33 + 1 +) + +func sumOutputSerializeSizes(outputs []*wire.TxOut) (serializeSize int) { + for _, txOut := range outputs { + serializeSize += txOut.SerializeSize() + } + return serializeSize +} + +// inputSize returns the size of the transaction input needed to include a +// signature script with size sigScriptSize. It is calculated as: +// +// - 32 bytes previous tx +// - 4 bytes output index +// - Compact int encoding sigScriptSize +// - sigScriptSize bytes signature script +// - 4 bytes sequence +func inputSize(sigScriptSize int) int { + return 32 + 4 + wire.VarIntSerializeSize(uint64(sigScriptSize)) + sigScriptSize + 4 +} + +// estimateRedeemSerializeSize returns a worst case serialize size estimates for +// a transaction that redeems an atomic swap P2SH output. +func estimateRedeemSerializeSize(contract []byte, txOuts []*wire.TxOut) int { + contractPush, err := txscript.NewScriptBuilder().AddData(contract).Script() + if err != nil { + // Should never be hit since this script does exceed the limits. + panic(err) + } + contractPushSize := len(contractPush) + + // 12 additional bytes are for version, locktime and expiry. + return 12 + wire.VarIntSerializeSize(1) + + wire.VarIntSerializeSize(uint64(len(txOuts))) + + inputSize(redeemAtomicSwapSigScriptSize+contractPushSize) + + sumOutputSerializeSizes(txOuts) +} + +// estimateRefundSerializeSize returns a worst case serialize size estimates for +// a transaction that refunds an atomic swap P2SH output. +func estimateRefundSerializeSize(contract []byte, txOuts []*wire.TxOut) int { + contractPush, err := txscript.NewScriptBuilder().AddData(contract).Script() + if err != nil { + // Should never be hit since this script does exceed the limits. + panic(err) + } + contractPushSize := len(contractPush) + + // 12 additional bytes are for version, locktime and expiry. + return 12 + wire.VarIntSerializeSize(1) + + wire.VarIntSerializeSize(uint64(len(txOuts))) + + inputSize(refundAtomicSwapSigScriptSize+contractPushSize) + + sumOutputSerializeSizes(txOuts) +}