From 4f5d1f077b02d8ad8787162b3053d3142b67c71e Mon Sep 17 00:00:00 2001 From: Thanee Charattrakool <37617738+Planxnx@users.noreply.github.com> Date: Fri, 14 Jun 2024 16:48:22 +0700 Subject: [PATCH 1/2] feat(btcutils): add bitcoin utility functions (#26) * feat(btcutils): add bitcoin utility functions * feat(btcutils): add bitcoin signature verification --- go.mod | 20 +- go.sum | 48 ++-- pkg/btcutils/address.go | 203 ++++++++++++++++ pkg/btcutils/address_bench_test.go | 80 +++++++ pkg/btcutils/address_test.go | 363 +++++++++++++++++++++++++++++ pkg/btcutils/btc.go | 44 ++++ pkg/btcutils/btc_network.go | 23 ++ pkg/btcutils/pk_script.go | 54 +++++ pkg/btcutils/pk_script_test.go | 205 ++++++++++++++++ pkg/btcutils/psbtutils/encoding.go | 92 ++++++++ pkg/btcutils/psbtutils/fee.go | 110 +++++++++ pkg/btcutils/psbtutils/fee_test.go | 131 +++++++++++ pkg/btcutils/psbtutils/is_ready.go | 35 +++ pkg/btcutils/signature.go | 19 ++ pkg/btcutils/signature_test.go | 59 +++++ pkg/btcutils/transaction.go | 10 + 16 files changed, 1467 insertions(+), 29 deletions(-) create mode 100644 pkg/btcutils/address.go create mode 100644 pkg/btcutils/address_bench_test.go create mode 100644 pkg/btcutils/address_test.go create mode 100644 pkg/btcutils/btc.go create mode 100644 pkg/btcutils/btc_network.go create mode 100644 pkg/btcutils/pk_script.go create mode 100644 pkg/btcutils/pk_script_test.go create mode 100644 pkg/btcutils/psbtutils/encoding.go create mode 100644 pkg/btcutils/psbtutils/fee.go create mode 100644 pkg/btcutils/psbtutils/fee_test.go create mode 100644 pkg/btcutils/psbtutils/is_ready.go create mode 100644 pkg/btcutils/signature.go create mode 100644 pkg/btcutils/signature_test.go create mode 100644 pkg/btcutils/transaction.go diff --git a/go.mod b/go.mod index 4fcf7c9..cf8cf88 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Cleverse/go-utilities/utils v0.0.0-20240119201306-d71eb577ef11 github.com/btcsuite/btcd v0.24.0 github.com/btcsuite/btcd/btcutil v1.1.5 + github.com/btcsuite/btcd/btcutil/psbt v1.1.9 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 github.com/cockroachdb/errors v1.11.1 github.com/gaze-network/uint128 v1.3.0 @@ -20,23 +21,24 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.18.2 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/valyala/fasthttp v1.51.0 go.uber.org/automaxprocs v1.5.3 - golang.org/x/sync v0.5.0 + golang.org/x/sync v0.7.0 ) require ( github.com/andybalholm/brotli v1.0.5 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect + github.com/bitonicnl/verify-signed-message v0.7.1 + github.com/btcsuite/btcd/btcec/v2 v2.3.3 // indirect github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect github.com/cockroachdb/redact v1.1.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/getsentry/sentry-go v0.18.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -74,10 +76,10 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.20.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f0fa253..c5d7abb 100644 --- a/go.sum +++ b/go.sum @@ -7,18 +7,23 @@ github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5 github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/bitonicnl/verify-signed-message v0.7.1 h1:1Qku9k9WgzobjqBY7tT3CLjWxtTJZxkYNhOV6QeCTjY= +github.com/bitonicnl/verify-signed-message v0.7.1/go.mod h1:PR60twfJIaHEo9Wb6eJBh8nBHEZIQQx8CvRwh0YmEPk= 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 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0= +github.com/btcsuite/btcd/btcec/v2 v2.3.3/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/btcutil/psbt v1.1.9 h1:UmfOIiWMZcVMOLaN+lxbbLSuoINGS1WmK1TZNI0b4yk= +github.com/btcsuite/btcd/btcutil/psbt v1.1.9/go.mod h1:ehBEvU91lxSlXtA+zZz3iFYx7Yq9eqnKx4/kSrnsvMY= 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= @@ -50,10 +55,12 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/dhui/dktest v0.4.1 h1:/w+IWuDXVymg3IrRJCHHOkMK10m9aNVMOyD0X12YVTg= github.com/dhui/dktest v0.4.1/go.mod h1:DdOqcUpL7vgyP4GlF3X3w7HbSlz8cEQzwewPveYEQbA= @@ -96,8 +103,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW 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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= @@ -218,8 +225,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= @@ -243,14 +251,14 @@ golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= -golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4= +golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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= @@ -265,8 +273,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= @@ -279,19 +287,19 @@ golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.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/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/btcutils/address.go b/pkg/btcutils/address.go new file mode 100644 index 0000000..a81812c --- /dev/null +++ b/pkg/btcutils/address.go @@ -0,0 +1,203 @@ +package btcutils + +import ( + "encoding/json" + "reflect" + + "github.com/Cleverse/go-utilities/utils" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/cockroachdb/errors" + "github.com/gaze-network/indexer-network/common/errs" + "github.com/gaze-network/indexer-network/pkg/logger" + "github.com/gaze-network/indexer-network/pkg/logger/slogx" +) + +// IsAddress returns whether or not the passed string is a valid bitcoin address and valid supported type. +// +// NetParams is optional. If provided, we only check for that network, +// otherwise, we check for all supported networks. +func IsAddress(address string, defaultNet ...*chaincfg.Params) bool { + if len(address) == 0 { + return false + } + + // If defaultNet is provided, we only check for that network. + net, ok := utils.Optional(defaultNet) + if ok { + _, _, err := parseAddress(address, net) + return err == nil + } + + // Otherwise, we check for all supported networks. + for _, net := range supportedNetworks { + _, _, err := parseAddress(address, net) + if err == nil { + return true + } + } + return false +} + +// TODO: create GetAddressNetwork +// check `Bech32HRPSegwit` prefix or netID for P2SH/P2PKH is equal to `PubKeyHashAddrID/ScriptHashAddrID` + +// GetAddressType returns the address type of the passed address. +func GetAddressType(address string, net *chaincfg.Params) (AddressType, error) { + _, addrType, err := parseAddress(address, net) + return addrType, errors.WithStack(err) +} + +type Address struct { + decoded btcutil.Address + net *chaincfg.Params + encoded string + encodedType AddressType + scriptPubKey []byte +} + +// NewAddress creates a new address from the given address string. +// +// defaultNet is required if your address is P2SH or P2PKH (legacy or nested segwit) +// If your address is P2WSH, P2WPKH or P2TR, defaultNet is not required. +func NewAddress(address string, defaultNet ...*chaincfg.Params) Address { + addr, err := SafeNewAddress(address, defaultNet...) + if err != nil { + logger.Panic("can't create parse address", slogx.Error(err), slogx.String("package", "btcutils")) + } + return addr +} + +// SafeNewAddress creates a new address from the given address string. +// It returns an error if the address is invalid. +// +// defaultNet is required if your address is P2SH or P2PKH (legacy or nested segwit) +// If your address is P2WSH, P2WPKH or P2TR, defaultNet is not required. +func SafeNewAddress(address string, defaultNet ...*chaincfg.Params) (Address, error) { + net := utils.DefaultOptional(defaultNet, &chaincfg.MainNetParams) + + decoded, addrType, err := parseAddress(address, net) + if err != nil { + return Address{}, errors.Wrap(err, "can't parse address") + } + + scriptPubkey, err := txscript.PayToAddrScript(decoded) + if err != nil { + return Address{}, errors.Wrap(err, "can't get script pubkey") + } + + return Address{ + decoded: decoded, + net: net, + encoded: decoded.EncodeAddress(), + encodedType: addrType, + scriptPubKey: scriptPubkey, + }, nil +} + +// String returns the address string. +func (a Address) String() string { + return a.encoded +} + +// Type returns the address type. +func (a Address) Type() AddressType { + return a.encodedType +} + +// Decoded returns the btcutil.Address +func (a Address) Decoded() btcutil.Address { + return a.decoded +} + +// IsForNet returns whether or not the address is associated with the passed bitcoin network. +func (a Address) IsForNet(net *chaincfg.Params) bool { + return a.decoded.IsForNet(net) +} + +// ScriptAddress returns the raw bytes of the address to be used when inserting the address into a txout's script. +func (a Address) ScriptAddress() []byte { + return a.decoded.ScriptAddress() +} + +// Net returns the address network params. +func (a Address) Net() *chaincfg.Params { + return a.net +} + +// NetworkName +func (a Address) NetworkName() string { + return a.net.Name +} + +// ScriptPubKey or pubkey script +func (a Address) ScriptPubKey() []byte { + return a.scriptPubKey +} + +// Equal return true if addresses are equal +func (a Address) Equal(b Address) bool { + return a.encoded == b.encoded +} + +// MarshalText implements the encoding.TextMarshaler interface. +func (a Address) MarshalText() ([]byte, error) { + return []byte(a.encoded), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +func (a *Address) UnmarshalText(input []byte) error { + address := string(input) + addr, err := SafeNewAddress(address) + if err == nil { + *a = addr + return nil + } + return errors.Wrapf(errs.InvalidArgument, "invalid address `%s`", address) +} + +// MarshalJSON implements the json.Marshaler interface. +func (a Address) MarshalJSON() ([]byte, error) { + t, err := a.MarshalText() + if err != nil { + return nil, &json.MarshalerError{Type: reflect.TypeOf(a), Err: err} + } + b := make([]byte, len(t)+2) + b[0], b[len(b)-1] = '"', '"' // add quotes + copy(b[1:], t) + return b, nil +} + +// UnmarshalJSON parses a hash in hex syntax. +func (a *Address) UnmarshalJSON(input []byte) error { + if !(len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"') { + return &json.UnmarshalTypeError{Value: "non-string", Type: reflect.TypeOf(Address{})} + } + if err := a.UnmarshalText(input[1 : len(input)-1]); err != nil { + return err + } + return nil +} + +func parseAddress(address string, params *chaincfg.Params) (btcutil.Address, AddressType, error) { + decoded, err := btcutil.DecodeAddress(address, params) + if err != nil { + return nil, 0, errors.Wrapf(err, "can't decode address `%s` for network `%s`", address, params.Name) + } + + switch decoded.(type) { + case *btcutil.AddressWitnessPubKeyHash: + return decoded, AddressP2WPKH, nil + case *btcutil.AddressTaproot: + return decoded, AddressP2TR, nil + case *btcutil.AddressScriptHash: + return decoded, AddressP2SH, nil + case *btcutil.AddressPubKeyHash: + return decoded, AddressP2PKH, nil + case *btcutil.AddressWitnessScriptHash: + return decoded, AddressP2WSH, nil + default: + return nil, 0, errors.Wrap(errs.Unsupported, "unsupported address type") + } +} diff --git a/pkg/btcutils/address_bench_test.go b/pkg/btcutils/address_bench_test.go new file mode 100644 index 0000000..bf144ff --- /dev/null +++ b/pkg/btcutils/address_bench_test.go @@ -0,0 +1,80 @@ +package btcutils_test + +import ( + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/gaze-network/indexer-network/pkg/btcutils" +) + +/* +NOTE: + +# Compare this benchmark to go-ethereum/common.Address utils +- go-ethereum/common.HexToAddress speed: 45 ns/op, 48 B/op, 1 allocs/op +- go-ethereum/common.IsHexAddress speed: 25 ns/op, 0 B/op, 0 allocs/op + +It's slower than go-ethereum/common.Address utils because ethereum wallet address is Hex string 20 bytes, +but Bitcoin has many types of address and each type has complex algorithm to solve (can't solve and validate address type directly from address string) + +20/Jan/2024 @Planxnx Macbook Air M1 16GB +BenchmarkIsAddress/specific-network/mainnet/P2WPKH-8 1776146 625.6 ns/op 120 B/op 3 allocs/op +BenchmarkIsAddress/specific-network/testnet3/P2WPKH-8 1917876 623.2 ns/op 120 B/op 3 allocs/op +BenchmarkIsAddress/specific-network/mainnet/P2TR-8 1330348 915.4 ns/op 160 B/op 3 allocs/op +BenchmarkIsAddress/specific-network/testnet3/P2TR-8 1235806 931.1 ns/op 160 B/op 3 allocs/op +BenchmarkIsAddress/specific-network/mainnet/P2WSH-8 1261730 960.9 ns/op 160 B/op 3 allocs/op +BenchmarkIsAddress/specific-network/testnet3/P2WSH-8 1307851 916.1 ns/op 160 B/op 3 allocs/op +BenchmarkIsAddress/specific-network/mainnet/P2SH-8 3081762 402.0 ns/op 192 B/op 8 allocs/op +BenchmarkIsAddress/specific-network/testnet3/P2SH-8 3245838 344.9 ns/op 176 B/op 7 allocs/op +BenchmarkIsAddress/specific-network/mainnet/P2PKH-8 2904252 410.4 ns/op 184 B/op 8 allocs/op +BenchmarkIsAddress/specific-network/testnet3/P2PKH-8 3522332 342.8 ns/op 176 B/op 7 allocs/op +BenchmarkIsAddress/automate-network/mainnet/P2WPKH-8 1882059 637.6 ns/op 120 B/op 3 allocs/op +BenchmarkIsAddress/automate-network/testnet3/P2WPKH-8 1626151 664.8 ns/op 120 B/op 3 allocs/op +BenchmarkIsAddress/automate-network/mainnet/P2TR-8 1250253 952.1 ns/op 160 B/op 3 allocs/op +BenchmarkIsAddress/automate-network/testnet3/P2TR-8 1257901 993.7 ns/op 160 B/op 3 allocs/op +BenchmarkIsAddress/automate-network/mainnet/P2WSH-8 1000000 1005 ns/op 160 B/op 3 allocs/op +BenchmarkIsAddress/automate-network/testnet3/P2WSH-8 1209108 971.2 ns/op 160 B/op 3 allocs/op +BenchmarkIsAddress/automate-network/mainnet/P2SH-8 1869075 625.0 ns/op 268 B/op 9 allocs/op +BenchmarkIsAddress/automate-network/testnet3/P2SH-8 779496 1609 ns/op 694 B/op 17 allocs/op +BenchmarkIsAddress/automate-network/mainnet/P2PKH-8 1924058 650.6 ns/op 259 B/op 9 allocs/op +BenchmarkIsAddress/automate-network/testnet3/P2PKH-8 721510 1690 ns/op 694 B/op 17 allocs/op +*/ +func BenchmarkIsAddress(b *testing.B) { + cases := []btcutils.Address{ + /* P2WPKH */ btcutils.NewAddress("bc1qfpgdxtpl7kz5qdus2pmexyjaza99c28q8uyczh", &chaincfg.MainNetParams), + /* P2WPKH */ btcutils.NewAddress("tb1qfpgdxtpl7kz5qdus2pmexyjaza99c28qd6ltey", &chaincfg.TestNet3Params), + /* P2TR */ btcutils.NewAddress("bc1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qvz5d38", &chaincfg.MainNetParams), + /* P2TR */ btcutils.NewAddress("tb1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qm2zztg", &chaincfg.TestNet3Params), + /* P2WSH */ btcutils.NewAddress("bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak", &chaincfg.MainNetParams), + /* P2WSH */ btcutils.NewAddress("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", &chaincfg.TestNet3Params), + /* P2SH */ btcutils.NewAddress("3Ccte7SJz71tcssLPZy3TdWz5DTPeNRbPw", &chaincfg.MainNetParams), + /* P2SH */ btcutils.NewAddress("2NCxMvHPTduZcCuUeAiWUpuwHga7Y66y9XJ", &chaincfg.TestNet3Params), + /* P2PKH */ btcutils.NewAddress("1KrRZSShVkdc8J71CtY4wdw46Rx3BRLKyH", &chaincfg.MainNetParams), + /* P2PKH */ btcutils.NewAddress("migbBPcDajPfffrhoLpYFTQNXQFbWbhpz3", &chaincfg.TestNet3Params), + } + + b.Run("specific-network", func(b *testing.B) { + for _, c := range cases { + b.Run(c.NetworkName()+"/"+c.Type().String(), func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = btcutils.IsAddress(c.String(), c.Net()) + } + }) + } + }) + + b.Run("automate-network", func(b *testing.B) { + for _, c := range cases { + b.Run(c.NetworkName()+"/"+c.Type().String(), func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + ok := btcutils.IsAddress(c.String()) + if !ok { + b.Error("IsAddress returned false") + } + } + }) + } + }) +} diff --git a/pkg/btcutils/address_test.go b/pkg/btcutils/address_test.go new file mode 100644 index 0000000..d69f615 --- /dev/null +++ b/pkg/btcutils/address_test.go @@ -0,0 +1,363 @@ +package btcutils_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/gaze-network/indexer-network/pkg/btcutils" + "github.com/stretchr/testify/assert" +) + +func TestGetAddressType(t *testing.T) { + type Spec struct { + Address string + DefaultNet *chaincfg.Params + + ExpectedError error + ExpectedAddressType btcutils.AddressType + } + + specs := []Spec{ + { + Address: "bc1qfpgdxtpl7kz5qdus2pmexyjaza99c28q8uyczh", + DefaultNet: &chaincfg.MainNetParams, + + ExpectedError: nil, + ExpectedAddressType: btcutils.AddressP2WPKH, + }, + { + Address: "tb1qfpgdxtpl7kz5qdus2pmexyjaza99c28qd6ltey", + DefaultNet: &chaincfg.MainNetParams, + + ExpectedError: nil, + ExpectedAddressType: btcutils.AddressP2WPKH, + }, + { + Address: "bc1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qvz5d38", + DefaultNet: &chaincfg.MainNetParams, + + ExpectedError: nil, + ExpectedAddressType: btcutils.AddressP2TR, + }, + { + Address: "tb1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qm2zztg", + DefaultNet: &chaincfg.MainNetParams, + + ExpectedError: nil, + ExpectedAddressType: btcutils.AddressP2TR, + }, + { + Address: "3Ccte7SJz71tcssLPZy3TdWz5DTPeNRbPw", + DefaultNet: &chaincfg.MainNetParams, + + ExpectedError: nil, + ExpectedAddressType: btcutils.AddressP2SH, + }, + { + Address: "1KrRZSShVkdc8J71CtY4wdw46Rx3BRLKyH", + DefaultNet: &chaincfg.MainNetParams, + + ExpectedError: nil, + ExpectedAddressType: btcutils.AddressP2PKH, + }, + { + Address: "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak", + DefaultNet: &chaincfg.MainNetParams, + + ExpectedError: nil, + ExpectedAddressType: btcutils.AddressP2WSH, + }, + { + Address: "migbBPcDajPfffrhoLpYFTQNXQFbWbhpz3", + DefaultNet: &chaincfg.TestNet3Params, + + ExpectedError: nil, + ExpectedAddressType: btcutils.AddressP2PKH, + }, + { + Address: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + DefaultNet: &chaincfg.MainNetParams, + + ExpectedError: nil, + ExpectedAddressType: btcutils.AddressP2WSH, + }, + { + Address: "2NCxMvHPTduZcCuUeAiWUpuwHga7Y66y9XJ", + DefaultNet: &chaincfg.TestNet3Params, + + ExpectedError: nil, + ExpectedAddressType: btcutils.AddressP2SH, + }, + } + + for _, spec := range specs { + t.Run(fmt.Sprintf("address:%s", spec.Address), func(t *testing.T) { + actualAddressType, actualError := btcutils.GetAddressType(spec.Address, spec.DefaultNet) + if spec.ExpectedError != nil { + assert.ErrorIs(t, actualError, spec.ExpectedError) + } else { + assert.Equal(t, spec.ExpectedAddressType, actualAddressType) + } + }) + } +} + +func TestNewAddress(t *testing.T) { + type Spec struct { + Address string + DefaultNet *chaincfg.Params + + ExpectedAddressType btcutils.AddressType + } + + specs := []Spec{ + { + Address: "bc1qfpgdxtpl7kz5qdus2pmexyjaza99c28q8uyczh", + // DefaultNet: &chaincfg.MainNetParams, // Optional + + ExpectedAddressType: btcutils.AddressP2WPKH, + }, + { + Address: "tb1qfpgdxtpl7kz5qdus2pmexyjaza99c28qd6ltey", + // DefaultNet: &chaincfg.MainNetParams, // Optional + + ExpectedAddressType: btcutils.AddressP2WPKH, + }, + { + Address: "bc1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qvz5d38", + // DefaultNet: &chaincfg.MainNetParams, // Optional + + ExpectedAddressType: btcutils.AddressP2TR, + }, + { + Address: "tb1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qm2zztg", + // DefaultNet: &chaincfg.MainNetParams, // Optional + + ExpectedAddressType: btcutils.AddressP2TR, + }, + { + Address: "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak", + // DefaultNet: &chaincfg.MainNetParams, // Optional + + ExpectedAddressType: btcutils.AddressP2WSH, + }, + { + Address: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + // DefaultNet: &chaincfg.MainNetParams, // Optional + + ExpectedAddressType: btcutils.AddressP2WSH, + }, + { + Address: "3Ccte7SJz71tcssLPZy3TdWz5DTPeNRbPw", + DefaultNet: &chaincfg.MainNetParams, + + ExpectedAddressType: btcutils.AddressP2SH, + }, + { + Address: "2NCxMvHPTduZcCuUeAiWUpuwHga7Y66y9XJ", + DefaultNet: &chaincfg.TestNet3Params, + + ExpectedAddressType: btcutils.AddressP2SH, + }, + { + Address: "1KrRZSShVkdc8J71CtY4wdw46Rx3BRLKyH", + DefaultNet: &chaincfg.MainNetParams, + + ExpectedAddressType: btcutils.AddressP2PKH, + }, + { + Address: "migbBPcDajPfffrhoLpYFTQNXQFbWbhpz3", + DefaultNet: &chaincfg.TestNet3Params, + + ExpectedAddressType: btcutils.AddressP2PKH, + }, + } + + for _, spec := range specs { + t.Run(fmt.Sprintf("address:%s,type:%s", spec.Address, spec.ExpectedAddressType), func(t *testing.T) { + addr := btcutils.NewAddress(spec.Address, spec.DefaultNet) + + assert.Equal(t, spec.ExpectedAddressType, addr.Type()) + assert.Equal(t, spec.Address, addr.String()) + }) + } +} + +func TestIsAddress(t *testing.T) { + type Spec struct { + Address string + Expected bool + } + + specs := []Spec{ + { + Address: "bc1qfpgdxtpl7kz5qdus2pmexyjaza99c28q8uyczh", + + Expected: true, + }, + { + Address: "tb1qfpgdxtpl7kz5qdus2pmexyjaza99c28qd6ltey", + + Expected: true, + }, + { + Address: "bc1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qvz5d38", + + Expected: true, + }, + { + Address: "tb1p7h87kqsmpzatddzhdhuy9gmxdpvn5kvar6hhqlgau8d2ffa0pa3qm2zztg", + + Expected: true, + }, + { + Address: "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak", + + Expected: true, + }, + { + Address: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + + Expected: true, + }, + { + Address: "3Ccte7SJz71tcssLPZy3TdWz5DTPeNRbPw", + + Expected: true, + }, + { + Address: "2NCxMvHPTduZcCuUeAiWUpuwHga7Y66y9XJ", + + Expected: true, + }, + { + Address: "1KrRZSShVkdc8J71CtY4wdw46Rx3BRLKyH", + + Expected: true, + }, + { + Address: "migbBPcDajPfffrhoLpYFTQNXQFbWbhpz3", + + Expected: true, + }, + { + Address: "", + + Expected: false, + }, + { + Address: "migbBPcDajPfffrhoLpYFTQNXQFbWbhpz2", + + Expected: false, + }, + { + Address: "bc1qfpgdxtpl7kz5qdus2pmexyjaza99c28q8uyczz", + + Expected: false, + }, + } + + for _, spec := range specs { + t.Run(fmt.Sprintf("address:%s", spec.Address), func(t *testing.T) { + ok := btcutils.IsAddress(spec.Address) + assert.Equal(t, spec.Expected, ok) + }) + } +} + +func TestAddressEncoding(t *testing.T) { + rawAddress := "bc1qfpgdxtpl7kz5qdus2pmexyjaza99c28q8uyczh" + address := btcutils.NewAddress(rawAddress, &chaincfg.MainNetParams) + + type Spec struct { + Data interface{} + Expected string + } + + specs := []Spec{ + { + Data: address, + Expected: fmt.Sprintf(`"%s"`, rawAddress), + }, + { + Data: map[string]interface{}{ + "address": rawAddress, + }, + Expected: fmt.Sprintf(`{"address":"%s"}`, rawAddress), + }, + } + + for i, spec := range specs { + t.Run(fmt.Sprint(i+1), func(t *testing.T) { + actual, err := json.Marshal(spec.Data) + assert.NoError(t, err) + assert.Equal(t, spec.Expected, string(actual)) + }) + } +} + +func TestAddressDecoding(t *testing.T) { + rawAddress := "bc1qfpgdxtpl7kz5qdus2pmexyjaza99c28q8uyczh" + address := btcutils.NewAddress(rawAddress, &chaincfg.MainNetParams) + + // Case #1: address is a string + t.Run("from_string", func(t *testing.T) { + input := fmt.Sprintf(`"%s"`, rawAddress) + expected := address + actual := btcutils.Address{} + + err := json.Unmarshal([]byte(input), &actual) + if !assert.NoError(t, err) { + t.FailNow() + } + assert.Equal(t, expected, actual) + }) + + // Case #2: address is a field of a struct + t.Run("from_field_string", func(t *testing.T) { + type Data struct { + Address btcutils.Address `json:"address"` + } + input := fmt.Sprintf(`{"address":"%s"}`, rawAddress) + expected := Data{Address: address} + actual := Data{} + err := json.Unmarshal([]byte(input), &actual) + if !assert.NoError(t, err) { + t.FailNow() + } + assert.Equal(t, expected, actual) + }) + + // Case #3: address is an element of an array + t.Run("from_array", func(t *testing.T) { + input := fmt.Sprintf(`["%s"]`, rawAddress) + expected := []btcutils.Address{address} + actual := []btcutils.Address{} + err := json.Unmarshal([]byte(input), &actual) + if !assert.NoError(t, err) { + t.FailNow() + } + assert.Equal(t, expected, actual) + }) + + // Case #4: not supported address type + t.Run("from_string/not_address", func(t *testing.T) { + input := fmt.Sprintf(`"%s"`, "THIS_IS_NOT_SUPPORTED_ADDRESS") + actual := btcutils.Address{} + err := json.Unmarshal([]byte(input), &actual) + assert.Error(t, err) + }) + + // Case #5: invalid field type + t.Run("from_number", func(t *testing.T) { + type Data struct { + Address btcutils.Address `json:"address"` + } + input := fmt.Sprintf(`{"address":%d}`, 123) + actual := Data{} + err := json.Unmarshal([]byte(input), &actual) + assert.Error(t, err) + }) +} diff --git a/pkg/btcutils/btc.go b/pkg/btcutils/btc.go new file mode 100644 index 0000000..612283a --- /dev/null +++ b/pkg/btcutils/btc.go @@ -0,0 +1,44 @@ +package btcutils + +import ( + "github.com/Cleverse/go-utilities/utils" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" +) + +var ( + // NullAddress is an address that script address is all zeros. + NullAddress = NewAddress("1111111111111111111114oLvT2", &chaincfg.MainNetParams) + + // NullHash is a hash that all bytes are zero. + NullHash = utils.Must(chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000")) +) + +// TransactionType is the type of bitcoin transaction +// It's an alias of txscript.ScriptClass +type TransactionType = txscript.ScriptClass + +// AddressType is the type of bitcoin address. +// It's an alias of txscript.ScriptClass +type AddressType = txscript.ScriptClass + +// Types of bitcoin transaction +const ( + TransactionP2WPKH = txscript.WitnessV0PubKeyHashTy + TransactionP2TR = txscript.WitnessV1TaprootTy + TransactionTaproot = TransactionP2TR // Alias of P2TR + TransactionP2SH = txscript.ScriptHashTy + TransactionP2PKH = txscript.PubKeyHashTy + TransactionP2WSH = txscript.WitnessV0ScriptHashTy +) + +// Types of bitcoin address +const ( + AddressP2WPKH = txscript.WitnessV0PubKeyHashTy + AddressP2TR = txscript.WitnessV1TaprootTy + AddressTaproot = AddressP2TR // Alias of P2TR + AddressP2SH = txscript.ScriptHashTy + AddressP2PKH = txscript.PubKeyHashTy + AddressP2WSH = txscript.WitnessV0ScriptHashTy +) diff --git a/pkg/btcutils/btc_network.go b/pkg/btcutils/btc_network.go new file mode 100644 index 0000000..d961c49 --- /dev/null +++ b/pkg/btcutils/btc_network.go @@ -0,0 +1,23 @@ +package btcutils + +import ( + "github.com/btcsuite/btcd/chaincfg" +) + +var supportedNetworks = map[string]*chaincfg.Params{ + "mainnet": &chaincfg.MainNetParams, + "testnet": &chaincfg.TestNet3Params, +} + +// IsSupportedNetwork returns true if the given network is supported. +// +// TODO: create enum for network +func IsSupportedNetwork(network string) bool { + _, ok := supportedNetworks[network] + return ok +} + +// GetNetParams returns the *chaincfg.Params for the given network. +func GetNetParams(network string) *chaincfg.Params { + return supportedNetworks[network] +} diff --git a/pkg/btcutils/pk_script.go b/pkg/btcutils/pk_script.go new file mode 100644 index 0000000..c5f37fd --- /dev/null +++ b/pkg/btcutils/pk_script.go @@ -0,0 +1,54 @@ +package btcutils + +import ( + "github.com/Cleverse/go-utilities/utils" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/cockroachdb/errors" +) + +// NewPkScript creates a pubkey script(or witness program) from the given address string +// +// see: https://en.bitcoin.it/wiki/Script +func NewPkScript(address string, defaultNet ...*chaincfg.Params) ([]byte, error) { + net := utils.DefaultOptional(defaultNet, &chaincfg.MainNetParams) + decoded, _, err := parseAddress(address, net) + if err != nil { + return nil, errors.Wrap(err, "can't parse address") + } + scriptPubkey, err := txscript.PayToAddrScript(decoded) + if err != nil { + return nil, errors.Wrap(err, "can't get script pubkey") + } + return scriptPubkey, nil +} + +// GetAddressTypeFromPkScript returns the address type from the given pubkey script/script pubkey. +func GetAddressTypeFromPkScript(pkScript []byte, defaultNet ...*chaincfg.Params) (AddressType, error) { + net := utils.DefaultOptional(defaultNet, &chaincfg.MainNetParams) + scriptClass, _, _, err := txscript.ExtractPkScriptAddrs(pkScript, net) + if err != nil { + return txscript.NonStandardTy, errors.Wrap(err, "can't parse pkScript") + } + return scriptClass, nil +} + +// ExtractAddressFromPkScript extracts address from the given pubkey script/script pubkey. +// multi-signature script not supported +func ExtractAddressFromPkScript(pkScript []byte, defaultNet ...*chaincfg.Params) (Address, error) { + net := utils.DefaultOptional(defaultNet, &chaincfg.MainNetParams) + addrType, addrs, _, err := txscript.ExtractPkScriptAddrs(pkScript, net) + if err != nil { + return Address{}, errors.Wrap(err, "can't parse pkScript") + } + if len(addrs) == 0 { + return Address{}, errors.New("can't extract address from pkScript") + } + return Address{ + decoded: addrs[0], + net: net, + encoded: addrs[0].EncodeAddress(), + encodedType: addrType, + scriptPubKey: pkScript, + }, nil +} diff --git a/pkg/btcutils/pk_script_test.go b/pkg/btcutils/pk_script_test.go new file mode 100644 index 0000000..f27a764 --- /dev/null +++ b/pkg/btcutils/pk_script_test.go @@ -0,0 +1,205 @@ +package btcutils_test + +import ( + "encoding/hex" + "fmt" + "testing" + + "github.com/Cleverse/go-utilities/utils" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/cockroachdb/errors" + "github.com/gaze-network/indexer-network/pkg/btcutils" + "github.com/stretchr/testify/assert" +) + +func TestNewPkScript(t *testing.T) { + anyError := errors.New("any error") + + type Spec struct { + Address string + DefaultNet *chaincfg.Params + ExpectedError error + ExpectedPkScript string // hex encoded + } + + specs := []Spec{ + { + Address: "some_invalid_address", + DefaultNet: &chaincfg.MainNetParams, + ExpectedError: anyError, + ExpectedPkScript: "", + }, + { + // P2WPKH + Address: "bc1qdx72th7e3z8zc5wdrdxweswfcne974pjneyjln", + DefaultNet: &chaincfg.MainNetParams, + ExpectedError: nil, + ExpectedPkScript: "001469bca5dfd9888e2c51cd1b4cecc1c9c4f25f5432", + }, + { + // P2WPKH + Address: "bc1q7cj6gz6t3d28qg7kxhrc7h5t3h0re34fqqalga", + DefaultNet: &chaincfg.MainNetParams, + ExpectedError: nil, + ExpectedPkScript: "0014f625a40b4b8b547023d635c78f5e8b8dde3cc6a9", + }, + { + // P2TR + Address: "bc1pfd0zw2jwlpn4xckpr3dxpt7x0gw6wetuftxvrc4dt2qgn9azjuus65fug6", + DefaultNet: &chaincfg.MainNetParams, + ExpectedError: nil, + ExpectedPkScript: "51204b5e272a4ef8675362c11c5a60afc67a1da7657c4accc1e2ad5a808997a29739", + }, + { + // P2TR + Address: "bc1pxpumml545tqum5afarzlmnnez2npd35nvf0j0vnrp88nemqsn54qle05sm", + DefaultNet: &chaincfg.MainNetParams, + ExpectedError: nil, + ExpectedPkScript: "51203079bdfe95a2c1cdd3a9e8c5fdce7912a616c693625f27b26309cf3cec109d2a", + }, + { + // P2SH + Address: "3Ccte7SJz71tcssLPZy3TdWz5DTPeNRbPw", + DefaultNet: &chaincfg.MainNetParams, + ExpectedError: nil, + ExpectedPkScript: "a91477e1a3d54f545d83869ae3a6b28b071422801d7b87", + }, + { + // P2PKH + Address: "1KrRZSShVkdc8J71CtY4wdw46Rx3BRLKyH", + DefaultNet: &chaincfg.MainNetParams, + ExpectedError: nil, + ExpectedPkScript: "76a914cecb25b53809991c7beef2d27bc2be49e78c684388ac", + }, + { + // P2WSH + Address: "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak", + DefaultNet: &chaincfg.MainNetParams, + ExpectedError: nil, + ExpectedPkScript: "0020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", + }, + } + + for _, spec := range specs { + t.Run(fmt.Sprintf("address:%s", spec.Address), func(t *testing.T) { + // Validate Expected PkScript + if spec.ExpectedError == nil { + { + expectedPkScriptRaw, err := hex.DecodeString(spec.ExpectedPkScript) + if err != nil { + t.Fatalf("can't decode expected pkscript %s, Reason: %s", spec.ExpectedPkScript, err) + } + expectedPkScript, err := txscript.ParsePkScript(expectedPkScriptRaw) + if err != nil { + t.Fatalf("invalid expected pkscript %s, Reason: %s", spec.ExpectedPkScript, err) + } + + expectedAddress, err := expectedPkScript.Address(spec.DefaultNet) + if err != nil { + t.Fatalf("can't get address from expected pkscript %s, Reason: %s", spec.ExpectedPkScript, err) + } + assert.Equal(t, spec.Address, expectedAddress.EncodeAddress()) + } + { + address, err := btcutil.DecodeAddress(spec.Address, spec.DefaultNet) + if err != nil { + t.Fatalf("can't decode address %s(%s),Reason: %s", spec.Address, spec.DefaultNet.Name, err) + } + + pkScript, err := txscript.PayToAddrScript(address) + if err != nil { + t.Fatalf("can't get pkscript from address %s(%s),Reason: %s", spec.Address, spec.DefaultNet.Name, err) + } + + pkScriptStr := hex.EncodeToString(pkScript) + assert.Equal(t, spec.ExpectedPkScript, pkScriptStr) + } + } + + pkScript, err := btcutils.NewPkScript(spec.Address, spec.DefaultNet) + if spec.ExpectedError == anyError { + assert.Error(t, err) + } else if spec.ExpectedError != nil { + assert.ErrorIs(t, err, spec.ExpectedError) + } else { + address, err := btcutils.SafeNewAddress(spec.Address, spec.DefaultNet) + if err != nil { + t.Fatalf("can't create address %s(%s),Reason: %s", spec.Address, spec.DefaultNet.Name, err) + } + + // ScriptPubKey from address and from NewPkScript should be the same + assert.Equal(t, address.ScriptPubKey(), pkScript) + + // Expected PkScript and New PkScript should be the same + pkScriptStr := hex.EncodeToString(pkScript) + assert.Equal(t, spec.ExpectedPkScript, pkScriptStr) + + // Can convert PkScript back to same address + acualPkScript, err := txscript.ParsePkScript(address.ScriptPubKey()) + if !assert.NoError(t, err) { + t.Fail() + } + assert.Equal(t, address.Decoded().String(), utils.Must(acualPkScript.Address(spec.DefaultNet)).String()) + } + }) + } +} + +func TestGetAddressTypeFromPkScript(t *testing.T) { + type Spec struct { + PubkeyScript string + + ExpectedError error + ExpectedAddressType btcutils.AddressType + } + + specs := []Spec{ + { + PubkeyScript: "0014602181cc89f7c9f54cb6d7607a3445e3e022895d", + + ExpectedError: nil, + ExpectedAddressType: btcutils.AddressP2WPKH, + }, + { + PubkeyScript: "5120ef8d59038dd51093fbfff794f658a07a3697b94d9e6d24e45b28abd88f10e33d", + + ExpectedError: nil, + ExpectedAddressType: btcutils.AddressP2TR, + }, + { + PubkeyScript: "a91416eef7e84fb9821db1341b6ccef1c4a4e5ec21e487", + + ExpectedError: nil, + ExpectedAddressType: btcutils.AddressP2SH, + }, + { + PubkeyScript: "76a914cecb25b53809991c7beef2d27bc2be49e78c684388ac", + + ExpectedError: nil, + ExpectedAddressType: btcutils.AddressP2PKH, + }, + { + PubkeyScript: "0020cdbf909e935c855d3e8d1b61aeb9c5e3c03ae8021b286839b1a72f2e48fdba70", + + ExpectedError: nil, + ExpectedAddressType: btcutils.AddressP2WSH, + }, + } + + for _, spec := range specs { + t.Run(fmt.Sprintf("PkScript:%s", spec.PubkeyScript), func(t *testing.T) { + pkScript, err := hex.DecodeString(spec.PubkeyScript) + if err != nil { + t.Fail() + } + actualAddressType, actualError := btcutils.GetAddressTypeFromPkScript(pkScript) + if spec.ExpectedError != nil { + assert.ErrorIs(t, actualError, spec.ExpectedError) + } else { + assert.Equal(t, spec.ExpectedAddressType, actualAddressType) + } + }) + } +} diff --git a/pkg/btcutils/psbtutils/encoding.go b/pkg/btcutils/psbtutils/encoding.go new file mode 100644 index 0000000..457ce08 --- /dev/null +++ b/pkg/btcutils/psbtutils/encoding.go @@ -0,0 +1,92 @@ +package psbtutils + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + + "github.com/Cleverse/go-utilities/utils" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/cockroachdb/errors" + "github.com/gaze-network/indexer-network/common/errs" +) + +const ( + // default psbt encoding is hex + DefaultEncoding = EncodingHex +) + +type Encoding string + +const ( + EncodingBase64 Encoding = "base64" + EncodingHex Encoding = "hex" +) + +// DecodeString decodes a psbt hex/base64 string into a psbt.Packet +// +// encoding is optional, default is EncodingHex +func DecodeString(psbtStr string, encoding ...Encoding) (*psbt.Packet, error) { + pC, err := Decode([]byte(psbtStr), encoding...) + return pC, errors.WithStack(err) +} + +// Decode decodes a psbt hex/base64 byte into a psbt.Packet +// +// encoding is optional, default is EncodingHex +func Decode(psbtB []byte, encoding ...Encoding) (*psbt.Packet, error) { + enc, ok := utils.Optional(encoding) + if !ok { + enc = DefaultEncoding + } + + var ( + psbtBytes []byte + err error + ) + + switch enc { + case EncodingBase64, "b64": + psbtBytes = make([]byte, base64.StdEncoding.DecodedLen(len(psbtB))) + _, err = base64.StdEncoding.Decode(psbtBytes, psbtB) + case EncodingHex: + psbtBytes = make([]byte, hex.DecodedLen(len(psbtB))) + _, err = hex.Decode(psbtBytes, psbtB) + default: + return nil, errors.Wrap(errs.Unsupported, "invalid encoding") + } + if err != nil { + return nil, errors.Wrap(err, "can't decode psbt string") + } + + pC, err := psbt.NewFromRawBytes(bytes.NewReader(psbtBytes), false) + if err != nil { + return nil, errors.Wrap(err, "can't create psbt from given psbt") + } + + return pC, nil +} + +// EncodeToString encodes a psbt.Packet into a psbt hex/base64 string +// +// encoding is optional, default is EncodingHex +func EncodeToString(pC *psbt.Packet, encoding ...Encoding) (string, error) { + enc, ok := utils.Optional(encoding) + if !ok { + enc = DefaultEncoding + } + + var buf bytes.Buffer + if err := pC.Serialize(&buf); err != nil { + return "", errors.Wrap(err, "can't serialize psbt") + } + + switch enc { + case EncodingBase64, "b64": + return base64.StdEncoding.EncodeToString(buf.Bytes()), nil + case EncodingHex: + return hex.EncodeToString(buf.Bytes()), nil + default: + return "", errors.Wrap(errs.Unsupported, "invalid encoding") + } +} diff --git a/pkg/btcutils/psbtutils/fee.go b/pkg/btcutils/psbtutils/fee.go new file mode 100644 index 0000000..6c59036 --- /dev/null +++ b/pkg/btcutils/psbtutils/fee.go @@ -0,0 +1,110 @@ +package psbtutils + +import ( + "math" + + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/cockroachdb/errors" + "github.com/gaze-network/indexer-network/common/errs" + "github.com/gaze-network/indexer-network/pkg/btcutils" +) + +// TxFee returns satoshis fee of a transaction given the fee rate (sat/vB) +// and the number of inputs and outputs. +func TxFee(feeRate int64, p *psbt.Packet) (int64, error) { + size, err := PSBTSize(p) + if err != nil { + return 0, errors.Wrap(err, "psbt size") + } + return int64(math.Ceil(size * float64(feeRate))), nil +} + +func PredictTxFee(feeRate int64, inputs, outputs int) int64 { + /** + TODO: handle edge cases like: + 1. when we predict that we need to use unnecessary UTXOs + 2. when we predict that we need to use more value than user have, but user do have enough for the actual transaction + + Idea for solving this: + - When trying to find the best UTXOs to use, we: + - Will not reject when user's balance is not enough, instead we will return all UTXOs even if it's not enough. + - Will be okay returning excessive UTXOs (say we predict we need 10K satoshis, but actually we only need 5K satoshis, then we will return UTXOs enough for 10K satoshis) + - And then we: + - Construct the actual PSBT, then select UTXOs to use accordingly, + - If the user's balance is not enough, then we will return an error, + - Or if when we predict we expect to use more UTXOs than the actual transaction, then we will just use what's needed. + */ + size := defaultOverhead + 148*float64(inputs) + 43*float64(outputs) + return int64(math.Ceil(size * float64(feeRate))) +} + +type txSize struct { + Overhead float64 + Inputs float64 + Outputs float64 +} + +const defaultOverhead = 10.5 + +// Transaction Virtual Sizes Bytes +// +// Reference: https://bitcoinops.org/en/tools/calc-size/ +var txSizes = map[btcutils.TransactionType]txSize{ + btcutils.TransactionP2WPKH: { + Inputs: 68, + Outputs: 31, + }, + btcutils.TransactionP2TR: { + Inputs: 57.5, + Outputs: 43, + }, + btcutils.TransactionP2SH: { + Inputs: 91, + Outputs: 32, + }, + btcutils.TransactionP2PKH: { + Inputs: 148, + Outputs: 34, + }, + btcutils.TransactionP2WSH: { + Inputs: 104.5, + Outputs: 43, + }, +} + +func PSBTSize(psbt *psbt.Packet) (float64, error) { + if err := psbt.SanityCheck(); err != nil { + return 0, errors.Wrap(errors.Join(err, errs.InvalidArgument), "psbt sanity check") + } + + inputs := map[btcutils.TransactionType]int{} + outputs := map[btcutils.TransactionType]int{} + + for _, input := range psbt.Inputs { + addrType, err := btcutils.GetAddressTypeFromPkScript(input.WitnessUtxo.PkScript) + if err != nil { + return 0, errors.Wrap(err, "get address type from pk script") + } + inputs[addrType]++ + } + + for _, output := range psbt.UnsignedTx.TxOut { + addrType, err := btcutils.GetAddressTypeFromPkScript(output.PkScript) + if err != nil { + return 0, errors.Wrap(err, "get address type from pk script") + } + outputs[addrType]++ + } + + totalSize := defaultOverhead + for txType, txSizeData := range txSizes { + if inputCount, ok := inputs[txType]; ok { + totalSize += txSizeData.Inputs * float64(inputCount) + } + if outputCount, ok := outputs[txType]; ok { + totalSize += txSizeData.Outputs * float64(outputCount) + } + } + + return totalSize, nil +} diff --git a/pkg/btcutils/psbtutils/fee_test.go b/pkg/btcutils/psbtutils/fee_test.go new file mode 100644 index 0000000..cd67316 --- /dev/null +++ b/pkg/btcutils/psbtutils/fee_test.go @@ -0,0 +1,131 @@ +package psbtutils_test + +import ( + "fmt" + "math" + "testing" + + "github.com/gaze-network/indexer-network/pkg/btcutils/psbtutils" + "github.com/stretchr/testify/assert" +) + +func TestPSBTSize(t *testing.T) { + type Spec struct { + Name string + PSBTString string + ExpectedError error + ExpectedSize float64 + } + + specs := []Spec{ + { + Name: "3-inputs-3-outputs-taproot", + PSBTString: "70736274ff0100fd06010100000003866c72cfeef533940eaee49b68778e6223914ea671411ec387bdb61f620889910000000000ffffffff866c72cfeef533940eaee49b68778e6223914ea671411ec387bdb61f620889910100000000ffffffff866c72cfeef533940eaee49b68778e6223914ea671411ec387bdb61f620889910200000000ffffffff03b0040000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f22020000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f4d370f00000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f000000000001012b58020000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f0001012b58020000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f0001012bcb3c0f00000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f00000000", + ExpectedError: nil, + ExpectedSize: 312, + }, + { + Name: "mixed-segwit-taproot", + PSBTString: "70736274ff0100fd230202000000061f34960fef4e73c3c4c023f303c16e06f0eebb268bc0d3bac99fa78c031a45b90300000000ffffffff1f34960fef4e73c3c4c023f303c16e06f0eebb268bc0d3bac99fa78c031a45b90400000000ffffffff21c8ec368f2aff1a7baf4964e4070f52e7247ae39edfbda3976f8df4da1b72a00000000000ffffffff969e65b705e3d5071f1743a63381b3aa1ec31e1dbbbd63ab594a19ca399a58af0000000000ffffffffcca5cfd28bd6c54a851d97d029560b3047f7c6482fda7b2f2603d56ade8c95890000000000ffffffff1f34960fef4e73c3c4c023f303c16e06f0eebb268bc0d3bac99fa78c031a45b90500000000ffffffff0908070000000000001600144850d32c3ff585403790507793125d174a5c28e022020000000000001600144850d32c3ff585403790507793125d174a5c28e022020000000000001600144850d32c3ff585403790507793125d174a5c28e0b03600000000000016001459805fc1fdb9f05e190db569987c95c4f9deaa532a680000000000002251203a9ddeb6a2a327fed0f50d18778b28168e3ddb7fdfd4b05f4e438c9174d76a8d58020000000000001600144850d32c3ff585403790507793125d174a5c28e058020000000000001600144850d32c3ff585403790507793125d174a5c28e058020000000000001600144850d32c3ff585403790507793125d174a5c28e0b21f1e00000000001600144850d32c3ff585403790507793125d174a5c28e0000000000001011f58020000000000001600144850d32c3ff585403790507793125d174a5c28e00001011f58020000000000001600144850d32c3ff585403790507793125d174a5c28e00001011f58020000000000001600144850d32c3ff585403790507793125d174a5c28e00001011f220200000000000016001459805fc1fdb9f05e190db569987c95c4f9deaa53010304830000000001012b22020000000000002251203a9ddeb6a2a327fed0f50d18778b28168e3ddb7fdfd4b05f4e438c9174d76a8d010304830000000001011f06432000000000001600144850d32c3ff585403790507793125d174a5c28e000000000000000000000", + ExpectedError: nil, + ExpectedSize: 699, + }, + { + Name: "segwit-transfer-to-legacy", + PSBTString: "70736274ff010074020000000124ba4becfc732f3b4729784a3dd0cc2494ae890d826377fd98aeb0607feb1ace0100000000ffffffff0210270000000000001976a91422bae94117be666b593916527d55bdaf030d756e88ac25f62e000000000016001476d1e072c9b8a18fa1e4be697c175e0c642026ac000000000001011fc51d2f000000000016001476d1e072c9b8a18fa1e4be697c175e0c642026ac01086b024730440220759df9d109298a1ef69b9faa1786f4118f0d4d63a68cd2061e217b6090573f62022053ffa117fc21e5bf20e7d16bb786de52dc0214c9a21af87b4e92a639ef66e997012103e0cb213a46a68b1f463a4858635ee44694ce4b512788833d629840341b1219c9000000", + ExpectedError: nil, + ExpectedSize: 143.5, + }, + } + + for _, spec := range specs { + t.Run(spec.Name, func(t *testing.T) { + p, err := psbtutils.DecodeString(spec.PSBTString) + assert.NoError(t, err) + size, err := psbtutils.PSBTSize(p) + if spec.ExpectedError != nil { + assert.ErrorIs(t, err, spec.ExpectedError) + } else { + assert.Equal(t, spec.ExpectedSize, size) + } + }) + } +} + +func TestPredictTxFee(t *testing.T) { + type Spec struct { + FeeRate int64 + InputsCount int + OutputsCount int + ExpectedFee int64 + } + + specs := []Spec{ + { + FeeRate: 100, + InputsCount: 1, + OutputsCount: 1, + ExpectedFee: int64(math.Ceil((10.5 + 148 + 43) * 100)), + }, + { + FeeRate: 1, + InputsCount: 99, + OutputsCount: 99, + ExpectedFee: int64(math.Ceil((10.5 + (99 * 148) + (99 * 43)) * 1)), + }, + } + + for _, spec := range specs { + t.Run(fmt.Sprintf("feeRate=%d:inputs=%d:outputs=%d", spec.FeeRate, spec.InputsCount, spec.OutputsCount), func(t *testing.T) { + fee := psbtutils.PredictTxFee(spec.FeeRate, spec.InputsCount, spec.OutputsCount) + assert.Equal(t, spec.ExpectedFee, fee) + }) + } +} + +func TestTxFee(t *testing.T) { + type Spec struct { + Name string + FeeRate int64 + PSBTString string + ExpectedError error + ExpectedFee int64 + } + + specs := []Spec{ + { + Name: "3-inputs-3-outputs-taproot", + FeeRate: 10, + PSBTString: "70736274ff0100fd06010100000003866c72cfeef533940eaee49b68778e6223914ea671411ec387bdb61f620889910000000000ffffffff866c72cfeef533940eaee49b68778e6223914ea671411ec387bdb61f620889910100000000ffffffff866c72cfeef533940eaee49b68778e6223914ea671411ec387bdb61f620889910200000000ffffffff03b0040000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f22020000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f4d370f00000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f000000000001012b58020000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f0001012b58020000000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f0001012bcb3c0f00000000002251205b954b2f91ded08c553551037bc71265a69a7586855ba4fdcf785a2494f0c37f00000000", + ExpectedError: nil, + ExpectedFee: 312 * 10, + }, + { + Name: "mixed-segwit-taproot", + FeeRate: 20, + PSBTString: "70736274ff0100fd230202000000061f34960fef4e73c3c4c023f303c16e06f0eebb268bc0d3bac99fa78c031a45b90300000000ffffffff1f34960fef4e73c3c4c023f303c16e06f0eebb268bc0d3bac99fa78c031a45b90400000000ffffffff21c8ec368f2aff1a7baf4964e4070f52e7247ae39edfbda3976f8df4da1b72a00000000000ffffffff969e65b705e3d5071f1743a63381b3aa1ec31e1dbbbd63ab594a19ca399a58af0000000000ffffffffcca5cfd28bd6c54a851d97d029560b3047f7c6482fda7b2f2603d56ade8c95890000000000ffffffff1f34960fef4e73c3c4c023f303c16e06f0eebb268bc0d3bac99fa78c031a45b90500000000ffffffff0908070000000000001600144850d32c3ff585403790507793125d174a5c28e022020000000000001600144850d32c3ff585403790507793125d174a5c28e022020000000000001600144850d32c3ff585403790507793125d174a5c28e0b03600000000000016001459805fc1fdb9f05e190db569987c95c4f9deaa532a680000000000002251203a9ddeb6a2a327fed0f50d18778b28168e3ddb7fdfd4b05f4e438c9174d76a8d58020000000000001600144850d32c3ff585403790507793125d174a5c28e058020000000000001600144850d32c3ff585403790507793125d174a5c28e058020000000000001600144850d32c3ff585403790507793125d174a5c28e0b21f1e00000000001600144850d32c3ff585403790507793125d174a5c28e0000000000001011f58020000000000001600144850d32c3ff585403790507793125d174a5c28e00001011f58020000000000001600144850d32c3ff585403790507793125d174a5c28e00001011f58020000000000001600144850d32c3ff585403790507793125d174a5c28e00001011f220200000000000016001459805fc1fdb9f05e190db569987c95c4f9deaa53010304830000000001012b22020000000000002251203a9ddeb6a2a327fed0f50d18778b28168e3ddb7fdfd4b05f4e438c9174d76a8d010304830000000001011f06432000000000001600144850d32c3ff585403790507793125d174a5c28e000000000000000000000", + ExpectedError: nil, + ExpectedFee: 699 * 20, + }, + { + Name: "segwit-transfer-to-legacy", + FeeRate: 99, + PSBTString: "70736274ff010074020000000124ba4becfc732f3b4729784a3dd0cc2494ae890d826377fd98aeb0607feb1ace0100000000ffffffff0210270000000000001976a91422bae94117be666b593916527d55bdaf030d756e88ac25f62e000000000016001476d1e072c9b8a18fa1e4be697c175e0c642026ac000000000001011fc51d2f000000000016001476d1e072c9b8a18fa1e4be697c175e0c642026ac01086b024730440220759df9d109298a1ef69b9faa1786f4118f0d4d63a68cd2061e217b6090573f62022053ffa117fc21e5bf20e7d16bb786de52dc0214c9a21af87b4e92a639ef66e997012103e0cb213a46a68b1f463a4858635ee44694ce4b512788833d629840341b1219c9000000", + ExpectedError: nil, + ExpectedFee: int64(math.Ceil((143.5) * 99)), + }, + } + + for _, spec := range specs { + t.Run(spec.Name, func(t *testing.T) { + p, err := psbtutils.DecodeString(spec.PSBTString) + assert.NoError(t, err) + fee, err := psbtutils.TxFee(spec.FeeRate, p) + if spec.ExpectedError != nil { + assert.ErrorIs(t, err, spec.ExpectedError) + } else { + assert.Equal(t, spec.ExpectedFee, fee) + } + }) + } +} diff --git a/pkg/btcutils/psbtutils/is_ready.go b/pkg/btcutils/psbtutils/is_ready.go new file mode 100644 index 0000000..f775759 --- /dev/null +++ b/pkg/btcutils/psbtutils/is_ready.go @@ -0,0 +1,35 @@ +package psbtutils + +import ( + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/wire" + "github.com/cockroachdb/errors" + "github.com/samber/lo" +) + +func IsReadyPSBT(pC *psbt.Packet, feeRate int64) (bool, error) { + // if input = output + fee then it's ready + + // Calculate tx fee + fee, err := TxFee(feeRate, pC) + if err != nil { + return false, errors.Wrap(err, "calculate fee") + } + + // sum total input and output + totalInputValue := lo.SumBy(pC.Inputs, func(input psbt.PInput) int64 { return input.WitnessUtxo.Value }) + totalOutputValue := lo.SumBy(pC.UnsignedTx.TxOut, func(txout *wire.TxOut) int64 { return txout.Value }) + fee + + // it's perfect match + if totalInputValue == totalOutputValue { + return true, nil + } + + // if input is more than output + fee but not more than 1000 satoshi, + // then it's ready + if totalInputValue > totalOutputValue && totalInputValue-totalOutputValue < 1000 { + return true, nil + } + + return false, nil +} diff --git a/pkg/btcutils/signature.go b/pkg/btcutils/signature.go new file mode 100644 index 0000000..bdc3da8 --- /dev/null +++ b/pkg/btcutils/signature.go @@ -0,0 +1,19 @@ +package btcutils + +import ( + verifier "github.com/bitonicnl/verify-signed-message/pkg" + "github.com/cockroachdb/errors" + "github.com/gaze-network/indexer-network/common" +) + +func VerifySignature(address string, message string, sigBase64 string, network common.Network) error { + _, err := verifier.VerifyWithChain(verifier.SignedMessage{ + Address: address, + Message: message, + Signature: sigBase64, + }, network.ChainParams()) + if err != nil { + return errors.WithStack(err) + } + return nil +} diff --git a/pkg/btcutils/signature_test.go b/pkg/btcutils/signature_test.go new file mode 100644 index 0000000..30e6fd3 --- /dev/null +++ b/pkg/btcutils/signature_test.go @@ -0,0 +1,59 @@ +package btcutils + +import ( + "testing" + + "github.com/gaze-network/indexer-network/common" + "github.com/stretchr/testify/assert" +) + +func TestVerifySignature(t *testing.T) { + { + message := "Test123" + address := "18J72YSM9pKLvyXX1XAjFXA98zeEvxBYmw" + signature := "Gzhfsw0ItSrrTCChykFhPujeTyAcvVxiXwywxpHmkwFiKuUR2ETbaoFcocmcSshrtdIjfm8oXlJoTOLosZp3Yc8=" + network := common.NetworkMainnet + + err := VerifySignature(address, message, signature, network) + assert.NoError(t, err) + } + { + address := "tb1qr97cuq4kvq7plfetmxnl6kls46xaka78n2288z" + message := "The outage comes at a time when bitcoin has been fast approaching new highs not seen since June 26, 2019." + signature := "H/bSByRH7BW1YydfZlEx9x/nt4EAx/4A691CFlK1URbPEU5tJnTIu4emuzkgZFwC0ptvKuCnyBThnyLDCqPqT10=" + network := common.NetworkTestnet + + err := VerifySignature(address, message, signature, network) + assert.NoError(t, err) + } + { + // Missmatch address + address := "tb1qp7y2ywgrv8a4t9h47yphtgj8w759rk6vgd9ran" + message := "The outage comes at a time when bitcoin has been fast approaching new highs not seen since June 26, 2019." + signature := "H/bSByRH7BW1YydfZlEx9x/nt4EAx/4A691CFlK1URbPEU5tJnTIu4emuzkgZFwC0ptvKuCnyBThnyLDCqPqT10=" + network := common.NetworkTestnet + + err := VerifySignature(address, message, signature, network) + assert.Error(t, err) + } + { + // Missmatch signature + address := "tb1qr97cuq4kvq7plfetmxnl6kls46xaka78n2288z" + message := "The outage comes at a time when bitcoin has been fast approaching new highs not seen since June 26, 2019." + signature := "Gzhfsw0ItSrrTCChykFhPujeTyAcvVxiXwywxpHmkwFiKuUR2ETbaoFcocmcSshrtdIjfm8oXlJoTOLosZp3Yc8=" + network := common.NetworkTestnet + + err := VerifySignature(address, message, signature, network) + assert.Error(t, err) + } + { + // Missmatch message + address := "tb1qr97cuq4kvq7plfetmxnl6kls46xaka78n2288z" + message := "Hello World" + signature := "H/bSByRH7BW1YydfZlEx9x/nt4EAx/4A691CFlK1URbPEU5tJnTIu4emuzkgZFwC0ptvKuCnyBThnyLDCqPqT10=" + network := common.NetworkTestnet + + err := VerifySignature(address, message, signature, network) + assert.Error(t, err) + } +} diff --git a/pkg/btcutils/transaction.go b/pkg/btcutils/transaction.go new file mode 100644 index 0000000..843ecab --- /dev/null +++ b/pkg/btcutils/transaction.go @@ -0,0 +1,10 @@ +package btcutils + +const ( + // TxVersion is the current latest supported transaction version. + TxVersion = 2 + + // MaxTxInSequenceNum is the maximum sequence number the sequence field + // of a transaction input can be. + MaxTxInSequenceNum uint32 = 0xffffffff +) From f63505e1733867d659ed08502b57dfd9a8eebfe5 Mon Sep 17 00:00:00 2001 From: Gaze Date: Fri, 14 Jun 2024 16:55:28 +0700 Subject: [PATCH 2/2] feat(btcutils): use chain params instead common.network --- pkg/btcutils/signature.go | 8 +++++--- pkg/btcutils/signature_test.go | 22 ++++++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pkg/btcutils/signature.go b/pkg/btcutils/signature.go index bdc3da8..33634e6 100644 --- a/pkg/btcutils/signature.go +++ b/pkg/btcutils/signature.go @@ -1,17 +1,19 @@ package btcutils import ( + "github.com/Cleverse/go-utilities/utils" verifier "github.com/bitonicnl/verify-signed-message/pkg" + "github.com/btcsuite/btcd/chaincfg" "github.com/cockroachdb/errors" - "github.com/gaze-network/indexer-network/common" ) -func VerifySignature(address string, message string, sigBase64 string, network common.Network) error { +func VerifySignature(address string, message string, sigBase64 string, defaultNet ...*chaincfg.Params) error { + net := utils.DefaultOptional(defaultNet, &chaincfg.MainNetParams) _, err := verifier.VerifyWithChain(verifier.SignedMessage{ Address: address, Message: message, Signature: sigBase64, - }, network.ChainParams()) + }, net) if err != nil { return errors.WithStack(err) } diff --git a/pkg/btcutils/signature_test.go b/pkg/btcutils/signature_test.go index 30e6fd3..304746a 100644 --- a/pkg/btcutils/signature_test.go +++ b/pkg/btcutils/signature_test.go @@ -3,7 +3,7 @@ package btcutils import ( "testing" - "github.com/gaze-network/indexer-network/common" + "github.com/btcsuite/btcd/chaincfg" "github.com/stretchr/testify/assert" ) @@ -12,7 +12,7 @@ func TestVerifySignature(t *testing.T) { message := "Test123" address := "18J72YSM9pKLvyXX1XAjFXA98zeEvxBYmw" signature := "Gzhfsw0ItSrrTCChykFhPujeTyAcvVxiXwywxpHmkwFiKuUR2ETbaoFcocmcSshrtdIjfm8oXlJoTOLosZp3Yc8=" - network := common.NetworkMainnet + network := &chaincfg.MainNetParams err := VerifySignature(address, message, signature, network) assert.NoError(t, err) @@ -21,7 +21,7 @@ func TestVerifySignature(t *testing.T) { address := "tb1qr97cuq4kvq7plfetmxnl6kls46xaka78n2288z" message := "The outage comes at a time when bitcoin has been fast approaching new highs not seen since June 26, 2019." signature := "H/bSByRH7BW1YydfZlEx9x/nt4EAx/4A691CFlK1URbPEU5tJnTIu4emuzkgZFwC0ptvKuCnyBThnyLDCqPqT10=" - network := common.NetworkTestnet + network := &chaincfg.TestNet3Params err := VerifySignature(address, message, signature, network) assert.NoError(t, err) @@ -31,7 +31,7 @@ func TestVerifySignature(t *testing.T) { address := "tb1qp7y2ywgrv8a4t9h47yphtgj8w759rk6vgd9ran" message := "The outage comes at a time when bitcoin has been fast approaching new highs not seen since June 26, 2019." signature := "H/bSByRH7BW1YydfZlEx9x/nt4EAx/4A691CFlK1URbPEU5tJnTIu4emuzkgZFwC0ptvKuCnyBThnyLDCqPqT10=" - network := common.NetworkTestnet + network := &chaincfg.TestNet3Params err := VerifySignature(address, message, signature, network) assert.Error(t, err) @@ -41,7 +41,7 @@ func TestVerifySignature(t *testing.T) { address := "tb1qr97cuq4kvq7plfetmxnl6kls46xaka78n2288z" message := "The outage comes at a time when bitcoin has been fast approaching new highs not seen since June 26, 2019." signature := "Gzhfsw0ItSrrTCChykFhPujeTyAcvVxiXwywxpHmkwFiKuUR2ETbaoFcocmcSshrtdIjfm8oXlJoTOLosZp3Yc8=" - network := common.NetworkTestnet + network := &chaincfg.TestNet3Params err := VerifySignature(address, message, signature, network) assert.Error(t, err) @@ -51,7 +51,17 @@ func TestVerifySignature(t *testing.T) { address := "tb1qr97cuq4kvq7plfetmxnl6kls46xaka78n2288z" message := "Hello World" signature := "H/bSByRH7BW1YydfZlEx9x/nt4EAx/4A691CFlK1URbPEU5tJnTIu4emuzkgZFwC0ptvKuCnyBThnyLDCqPqT10=" - network := common.NetworkTestnet + network := &chaincfg.TestNet3Params + + err := VerifySignature(address, message, signature, network) + assert.Error(t, err) + } + { + // Missmatch network + address := "tb1qr97cuq4kvq7plfetmxnl6kls46xaka78n2288z" + message := "The outage comes at a time when bitcoin has been fast approaching new highs not seen since June 26, 2019." + signature := "H/bSByRH7BW1YydfZlEx9x/nt4EAx/4A691CFlK1URbPEU5tJnTIu4emuzkgZFwC0ptvKuCnyBThnyLDCqPqT10=" + network := &chaincfg.MainNetParams err := VerifySignature(address, message, signature, network) assert.Error(t, err)