From c46f871ff815a25f42dc85895cc04321a4ecdfa0 Mon Sep 17 00:00:00 2001 From: dev-warrior777 <> Date: Thu, 12 Dec 2024 01:14:11 +0800 Subject: [PATCH 1/4] client: Allow Firo to send to an EXX address. EXX addresses are used for Firo and other privacy coins to send funds to Binance and other exchanges due to last US administration regulations. --- client/asset/btc/btc.go | 15 ++++- client/asset/firo/exx.go | 64 +++++++++++++++++++ client/asset/firo/exx_test.go | 63 ++++++++++++++++++ client/asset/firo/firo.go | 32 ++++++++++ dex/testing/firo/README_ELECTRUM_HARNESSES.md | 2 +- dex/testing/firo/electrum.sh | 14 ++-- dex/testing/firo/electrumx.sh | 9 ++- dex/testing/firo/harness.sh | 3 +- go.mod | 1 + go.sum | 3 + 10 files changed, 194 insertions(+), 12 deletions(-) create mode 100644 client/asset/firo/exx.go create mode 100644 client/asset/firo/exx_test.go diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 3a6b48ac2e..901bca5a9b 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -364,6 +364,10 @@ type BTCCloneCFG struct { // into an address string. If AddressStringer is not supplied, the // (btcutil.Address).String method will be used. AddressStringer dexbtc.AddressStringer // btcutil.Address => string, may be an override or just the String method + // PayToAddressScript is an optional argument that can make non-standard tx + // outputs. If PayToAddressScript is not supplied the (txscript).PayToAddrScript + // method will be used. Note the extra paramaeter for a string address. + PayToAddressScript func(btcutil.Address, string) ([]byte, error) // BlockDeserializer can be used in place of (*wire.MsgBlock).Deserialize. BlockDeserializer func([]byte) (*wire.MsgBlock, error) // ArglessChangeAddrRPC can be true if the getrawchangeaddress takes no @@ -801,6 +805,7 @@ type baseWallet struct { localFeeRate func(context.Context, RawRequester, uint64) (uint64, error) feeCache *feeRateCache decodeAddr dexbtc.AddressDecoder + payToAddress func(btcutil.Address, string) ([]byte, error) walletDir string deserializeTx func([]byte) (*wire.MsgTx, error) @@ -1317,6 +1322,13 @@ func newUnconnectedWallet(cfg *BTCCloneCFG, walletCfg *WalletConfig) (*baseWalle } } + addressPayer := cfg.PayToAddressScript + if addressPayer == nil { + addressPayer = func(addr btcutil.Address, _ string) ([]byte, error) { + return txscript.PayToAddrScript(addr) + } + } + w := &baseWallet{ symbol: cfg.Symbol, chainParams: cfg.ChainParams, @@ -1336,6 +1348,7 @@ func newUnconnectedWallet(cfg *BTCCloneCFG, walletCfg *WalletConfig) (*baseWalle feeCache: feeCache, decodeAddr: addrDecoder, stringAddr: addrStringer, + payToAddress: addressPayer, walletInfo: cfg.WalletInfo, deserializeTx: txDeserializer, serializeTx: txSerializer, @@ -4505,7 +4518,7 @@ func (btc *baseWallet) send(address string, val uint64, feeRate uint64, subtract if err != nil { return nil, 0, 0, fmt.Errorf("invalid address: %s", address) } - pay2script, err := txscript.PayToAddrScript(addr) + pay2script, err := btc.payToAddress(addr, address) if err != nil { return nil, 0, 0, fmt.Errorf("PayToAddrScript error: %w", err) } diff --git a/client/asset/firo/exx.go b/client/asset/firo/exx.go new file mode 100644 index 0000000000..6503031568 --- /dev/null +++ b/client/asset/firo/exx.go @@ -0,0 +1,64 @@ +package firo + +import ( + "fmt" + "strings" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil/base58" +) + +const ( + PKH_LEN = 20 + MAINNET_VER_BYTE_PKH = 0x52 + MAINNET_VER_BYTE_EXX = 0x01 + TESTNET_VER_BYTE_PKH = 0x41 + TESTNET_VER_BYTE_EXT = 0x01 + ALLNETS_EXTRA_BYTE_EX_ONE = 0xb9 // python: ADDRTYPE_EXP2PKH + MAINNET_EXTRA_BYTE_EXX_TWO = 0xbb + TESTNET_EXTRA_BYTE_EXT_TWO = 0xb1 + + EXCHANGE_MARKER_BYTE = 0xe0 // python: OP_EXCHANGEADDR + SCRIPT_LEN = 26 +) + +func isExxAddress(address string) bool { + return strings.HasPrefix(address, "EXX") || strings.HasPrefix(address, "EXT") +} + +// decodeExxAddress decodes a Firo exchange address. +func decodeExxAddress(encodedAddr string, net *chaincfg.Params) (btcutil.Address, error) { + decoded, _, err := base58.CheckDecode(encodedAddr) + if err != nil { + return nil, err + } + // pull out the pkh - Mainnet: (ver=0x01) <0xb9> <0xbb> + decLen := len(decoded) + if decLen < PKH_LEN+1 { + return nil, fmt.Errorf("bad decoded len %d", decLen) + } + decExtra := decLen - PKH_LEN + pkh := decoded[decExtra:] + + addrPKH, err := btcutil.NewAddressPubKeyHash(pkh, net) + if err != nil { + return nil, err + } + return btcutil.Address(addrPKH), nil +} + +func buildExxPayToScript(addr btcutil.Address, address string) ([]byte, error) { + if _, isPKH := addr.(*btcutil.AddressPubKeyHash); !isPKH { + return nil, fmt.Errorf("address %s does not contain a pubkey hash", address) + } + baseScript, err := txscript.PayToAddrScript(addr) + if err != nil { + return nil, err + } + script := make([]byte, 0, len(baseScript)+1) + script = append(script, EXCHANGE_MARKER_BYTE) + script = append(script, baseScript...) + return script, nil +} diff --git a/client/asset/firo/exx_test.go b/client/asset/firo/exx_test.go new file mode 100644 index 0000000000..f99c39469f --- /dev/null +++ b/client/asset/firo/exx_test.go @@ -0,0 +1,63 @@ +package firo + +import ( + "bytes" + "encoding/hex" + "fmt" + "testing" + + dexfiro "decred.org/dcrdex/dex/networks/firo" + "github.com/btcsuite/btcd/btcutil" +) + +const ( + exxAddress = "EXXKcAcVWXeG7S9aiXXGuGNZkWdB9XuSbJ1z" + decoded = "386ed39285803b1782d0e363897f1a81a5b87421" + // 386ed39285803b1782d0e363897f1a81a5b87421 + // e0 76a914 386ed39285803b1782d0e363897f1a81a5b87421 88ac + // validateaddress firo-qt, electrum-firo just says 'true' + encodedAsPKH = "a5rrM1DY9XTRucbNrJQDtDc6GiEbcX7jRd" +) + +func TestDecodeExxAddress(t *testing.T) { + addr, err := decodeExxAddress(exxAddress, dexfiro.MainNetParams) + if err != nil { + t.Fatalf("addr=%v - %v", addr, err) + } + + switch ty := addr.(type) { + case btcutil.Address: + fmt.Printf("type=%T\n", ty) + default: + t.Fatalf("invalid type=%T", ty) + } + + if !addr.IsForNet(dexfiro.MainNetParams) { + t.Fatalf("IsForNet failed") + } + decodedB, err := hex.DecodeString(decoded) + if err != nil { + t.Fatalf("hex decode error: %v", err) + } + if !bytes.Equal(addr.ScriptAddress(), decodedB) { + t.Fatalf("ScriptAddress failed") + } + s := addr.String() + if s != encodedAsPKH { + t.Fatalf("String failed expected %s got %s", encodedAsPKH, s) + } +} + +func TestBuildExxPayToScript(t *testing.T) { + addr, err := decodeExxAddress(exxAddress, dexfiro.MainNetParams) + if err != nil { + t.Fatalf("addr=%v - %v", addr, err) + } + script, err := buildExxPayToScript(addr, exxAddress) + if err != nil { + t.Fatal(err) + } + if len(script) != SCRIPT_LEN { + t.Fatalf("wrong script length - expected %d got %d", SCRIPT_LEN, len(script)) + } +} diff --git a/client/asset/firo/firo.go b/client/asset/firo/firo.go index b2349da9f0..3016fcb57d 100644 --- a/client/asset/firo/firo.go +++ b/client/asset/firo/firo.go @@ -22,6 +22,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" ) const ( @@ -168,6 +169,8 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) AssetID: BipID, FeeEstimator: estimateFee, ExternalFeeEstimator: externalFeeRate, + AddressDecoder: decodeAddress, + PayToAddressScript: payToAddressScript, PrivKeyFunc: nil, // set only for walletTypeRPC below } @@ -195,6 +198,35 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) } } +/****************************************************************************** + Helper Functions +******************************************************************************/ + +// decodeAddress decodes a firo address. For normal transparent addresses this +// just uses btcd: btcutil.DecodeAddress. +func decodeAddress(address string, net *chaincfg.Params) (btcutil.Address, error) { + if isExxAddress(address) { + return decodeExxAddress(address, net) + } + decAddr, err := btcutil.DecodeAddress(address, net) + if err != nil { + return nil, err + } + if !decAddr.IsForNet(net) { + return nil, errors.New("wrong network") + } + return decAddr, nil +} + +// payToAddressScript builds a P2PKH script for a Firo output. For normal transparent +// addresses btcd: txscript.PayToAddrScript is used. +func payToAddressScript(addr btcutil.Address, address string) ([]byte, error) { + if isExxAddress(address) { + return buildExxPayToScript(addr, address) + } + return txscript.PayToAddrScript(addr) +} + // rpcCaller is satisfied by ExchangeWalletFullNode (baseWallet), providing // direct RPC requests. type rpcCaller interface { diff --git a/dex/testing/firo/README_ELECTRUM_HARNESSES.md b/dex/testing/firo/README_ELECTRUM_HARNESSES.md index 5a64bd0a50..89032151d0 100644 --- a/dex/testing/firo/README_ELECTRUM_HARNESSES.md +++ b/dex/testing/firo/README_ELECTRUM_HARNESSES.md @@ -12,7 +12,7 @@ See Also: README_HARNESS.md ## 2. ElectrumX-Firo Test Harness The harness is a script named **electrumx.sh** which downloads a git repo -containing a release version of ElectrumX-Firo server. +containing a specific commit of ElectrumX-Firo server. No external releases. It requires **harness.sh** Firo chain server harness running. diff --git a/dex/testing/firo/electrum.sh b/dex/testing/firo/electrum.sh index 3c721ddee3..af0e4414df 100755 --- a/dex/testing/firo/electrum.sh +++ b/dex/testing/firo/electrum.sh @@ -22,7 +22,10 @@ export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python SCRIPT_DIR=$(pwd) # Electrum-Firo Version 4.1.5.2 -COMMIT=a3f64386efc9069cae83e23c241331de6f418b2f +# COMMIT=a3f64386efc9069cae83e23c241331de6f418b2f + +# Electrum-Firo Version 4.1.5.5 +COMMIT=b99e9594bddeecba82a2531bbf0769bd589f3a34 GENESIS=a42b98f04cc2916e8adfb5d9db8a2227c4629bc205748ed2f33180b636ee885b # regtest RPCPORT=8001 @@ -56,15 +59,12 @@ fi git remote -v -CURRENT_COMMIT=$(git rev-parse HEAD) -if [ ! "${CURRENT_COMMIT}" == "${COMMIT}" ]; then - git fetch --depth 1 origin ${COMMIT} - git reset --hard FETCH_HEAD -fi +git fetch --depth 1 origin ${COMMIT} +git reset --hard FETCH_HEAD if [ ! -d "${ELECTRUM_DIR}/venv" ]; then # The venv interpreter will be this python version, e.g. python3.10 - python3 -m venv ${ELECTRUM_DIR}/venv + python3.7 -m venv ${ELECTRUM_DIR}/venv fi source ${ELECTRUM_DIR}/venv/bin/activate python --version diff --git a/dex/testing/firo/electrumx.sh b/dex/testing/firo/electrumx.sh index 684762c05b..aae2aebccd 100755 --- a/dex/testing/firo/electrumx.sh +++ b/dex/testing/firo/electrumx.sh @@ -16,7 +16,11 @@ set -ex -COMMIT=c0cdcc0dfcaa057058fd1ed281557dede924cd27 +# No external releases - just master +# May 11, 2023 +# COMMIT=c0cdcc0dfcaa057058fd1ed281557dede924cd27 +# Jul 8, 2024 +COMMIT=937e4bb3d8802317b64231844b698d8758029ca5 ELECTRUMX_DIR=~/dextest/electrum/firo/server REPO_DIR=${ELECTRUMX_DIR}/electrumx-repo @@ -53,7 +57,8 @@ export NET="regtest" export DB_ENGINE="leveldb" export DB_DIRECTORY="${DATA_DIR}" export DAEMON_URL="http://user:pass@127.0.0.1:53768" # harness:alpha:rpc -export SERVICES="ssl://localhost:50002,rpc://" +# export SERVICES="ssl://localhost:50002,rpc://" +export SERVICES="tcp://localhost:50001,rpc://" export SSL_CERTFILE="${DATA_DIR}/ssl.cert" export SSL_KEYFILE="${DATA_DIR}/ssl.key" export PEER_DISCOVERY="off" diff --git a/dex/testing/firo/harness.sh b/dex/testing/firo/harness.sh index b1d9d62f93..87ec504ace 100755 --- a/dex/testing/firo/harness.sh +++ b/dex/testing/firo/harness.sh @@ -76,7 +76,8 @@ cd ${NODES_ROOT} && tmux new-session -d -s $SESSION $SHELL # Write config files. ################################################################################ echo "Writing node config files" -# These config files aren't actually used here, but can be used by other programs. +# These config files aren't actually used here, but can be used by other programs +# such as loadbot. cat > "${ALPHA_DIR}/alpha.conf" < Date: Thu, 12 Dec 2024 15:54:28 +0800 Subject: [PATCH 2/4] imports: update go.mod for btcsuite/btcd/btcutil/base58 --- client/asset/firo/exx.go | 2 +- go.mod | 4 ---- go.sum | 33 --------------------------------- 3 files changed, 1 insertion(+), 38 deletions(-) diff --git a/client/asset/firo/exx.go b/client/asset/firo/exx.go index 6503031568..26e882b867 100644 --- a/client/asset/firo/exx.go +++ b/client/asset/firo/exx.go @@ -5,9 +5,9 @@ import ( "strings" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/btcutil/base58" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcutil/base58" ) const ( diff --git a/go.mod b/go.mod index 2aeb3d4372..c899ea4c76 100644 --- a/go.mod +++ b/go.mod @@ -84,7 +84,6 @@ require ( github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect - github.com/btcsuite/btcutil v1.0.2 github.com/btcsuite/btcwallet/wallet/txrules v1.2.1 // indirect github.com/btcsuite/btcwallet/wallet/txsizes v1.2.4 // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect @@ -111,7 +110,6 @@ require ( github.com/decred/dcrd/database/v3 v3.0.2 // indirect github.com/decred/dcrd/lru v1.1.2 // indirect github.com/decred/dcrd/mixing v0.4.2 // indirect - github.com/decred/vspd/client/v3 v3.0.0 // indirect github.com/decred/vspd/client/v4 v4.0.0 // indirect github.com/decred/vspd/types/v3 v3.0.0 // indirect github.com/dgraph-io/ristretto v0.0.2 // indirect @@ -178,8 +176,6 @@ require ( github.com/zquestz/grab v0.0.0-20190224022517-abcee96e61b1 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.23.0 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.56.3 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect diff --git a/go.sum b/go.sum index 2dc32a85e9..5c52b6c3e7 100644 --- a/go.sum +++ b/go.sum @@ -38,12 +38,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= -decred.org/cspp/v2 v2.2.0 h1:VSOUC1w0Wo+QOGS0r1XO6TLnO16X67KuvpDmRRYyr08= -decred.org/cspp/v2 v2.2.0/go.mod h1:9nO3bfvCheOPIFZw5f6sRQ42CjBFB5RKSaJ9Iq6G4MA= decred.org/cspp/v2 v2.3.0 h1:GC8emJnLbOVAkgBTHK/1wy6o/m0AVsN1r4m1ZnZZWjo= decred.org/cspp/v2 v2.3.0/go.mod h1:9nO3bfvCheOPIFZw5f6sRQ42CjBFB5RKSaJ9Iq6G4MA= -decred.org/dcrwallet/v4 v4.1.1 h1:imwPBboytp1PH6V8q7/JLTHiKgj/Scq9a3I1WmnJv0Y= -decred.org/dcrwallet/v4 v4.1.1/go.mod h1:WxerkRcUGVreJsAI0ptCBPUujPUmWncbdYbme8Kl5r0= decred.org/dcrwallet/v4 v4.2.0 h1:V+RO80FnRuJCPoOQIFKb26JM5Yn1+oXS11x8zZPR1T4= decred.org/dcrwallet/v4 v4.2.0/go.mod h1:VLK+FIBD4n/K7v5Sc+rYcSB4j3PiGm1MKDpxXjrlE1s= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -145,8 +141,6 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtyd github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= -github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= github.com/btcsuite/btcwallet v0.16.10-0.20240815225602-6ecae9c12fde h1:NURMAR/mat4V2ZwZE/dXIu+pfHc3SKN/kvAcKlfs/CU= github.com/btcsuite/btcwallet v0.16.10-0.20240815225602-6ecae9c12fde/go.mod h1:X2xDre+j1QphTRo54y2TikUzeSvreL1t1aMXrD8Kc5A= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4 h1:poyHFf7+5+RdxNp5r2T6IBRD7RyraUsYARYbp/7t4D8= @@ -275,8 +269,6 @@ github.com/decred/dcrd/blockchain/standalone/v2 v2.2.1 h1:zeI9CHkLM9be4QOBmIAtoP github.com/decred/dcrd/blockchain/standalone/v2 v2.2.1/go.mod h1:yXZz/EgWdGw5nqMEvyKj/iXZ9I2VSyO95xKj6mRUMIM= github.com/decred/dcrd/blockchain/v5 v5.0.1 h1:IGr8rJsgBVKDBI8STzeuGF6Mej0xbIX4gVVBA9yEMRU= github.com/decred/dcrd/blockchain/v5 v5.0.1/go.mod h1:LtSV1+u8aBQzlExAQcl4HIJ6Bfi5f6Rvws/9euH4mDA= -github.com/decred/dcrd/certgen v1.1.3 h1:MYENpBWVSP6FkkLBSSnaBGEOWobPcgYBLDDo88szi9c= -github.com/decred/dcrd/certgen v1.1.3/go.mod h1:Od5y39J+r2ZlvrizyWu2cylcYu0+emTTVm3eix4W8bw= github.com/decred/dcrd/certgen v1.2.0 h1:FF6XXV//5q38/c6QbGQdR35ZJz0GPIkejsZZU3oHuBQ= github.com/decred/dcrd/certgen v1.2.0/go.mod h1:LRh6dF2WPQeDA6QQSZE+SfK7AL6FuFtCRDHZf8DyGzg= github.com/decred/dcrd/chaincfg/chainhash v1.0.4 h1:zRCv6tdncLfLTKYqu7hrXvs7hW+8FO/NvwoFvGsrluU= @@ -288,8 +280,6 @@ github.com/decred/dcrd/connmgr/v3 v3.1.2/go.mod h1:tdbErFiNOuy/sHrX2mwaOk+r1HLs3 github.com/decred/dcrd/container/lru v1.0.0 h1:7foQymtbu18aQWYiY9RnNIeE+kvpiN+fiBQ3+viyJjI= github.com/decred/dcrd/container/lru v1.0.0/go.mod h1:vlPwj0l+IzAHhQSsbgQnJgO5Cte78+yI065V+Mc5PRQ= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -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/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/crypto/rand v1.0.0 h1:Ah9Asl36OZt09sGSMbJZuL1HfwGdlC38q/ZUeLDVKRg= @@ -316,8 +306,6 @@ github.com/decred/dcrd/hdkeychain/v3 v3.1.2/go.mod h1:FnNJmZ7jqUDeAo6/c/xkQi5cux github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/decred/dcrd/lru v1.1.2 h1:KdCzlkxppuoIDGEvCGah1fZRicrDH36IipvlB1ROkFY= github.com/decred/dcrd/lru v1.1.2/go.mod h1:gEdCVgXs1/YoBvFWt7Scgknbhwik3FgVSzlnCcXL2N8= -github.com/decred/dcrd/mixing v0.3.0 h1:eUHpTdwTqXUllnn1ZYLfxucW/2UOkMmx4CyztipTJ9g= -github.com/decred/dcrd/mixing v0.3.0/go.mod h1:W3K7yJKmoI03G2U5Yw+HSRNe6lLBegi63ZR6fFLnM9c= github.com/decred/dcrd/mixing v0.4.2 h1:mpt2pNIFTI6L1hXrieAWJTQJv5t9WzHcNnhI+tnAG90= github.com/decred/dcrd/mixing v0.4.2/go.mod h1:VF87lOn41kitgWVOwmXoB4qMYF7+bxItZXyw4JfW3EQ= github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.3.0 h1:l0DnCcILTNrpy8APF3FLN312ChpkQaAuW30aC/RgBaw= @@ -332,8 +320,6 @@ github.com/decred/go-socks v1.1.0 h1:dnENcc0KIqQo3HSXdgboXAHgqsCIutkqq6ntQjYtm2U github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0= github.com/decred/slog v1.2.0 h1:soHAxV52B54Di3WtKLfPum9OFfWqwtf/ygf9njdfnPM= github.com/decred/slog v1.2.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0= -github.com/decred/vspd/client/v3 v3.0.0 h1:4gAGDTeIU0r4quCxmV5Ez7T2J+P+OLPSibkCF+/Yb6w= -github.com/decred/vspd/client/v3 v3.0.0/go.mod h1:5pfPvIa6V38AmophMrKUCl3KMpEIxcltWtgL2R+wsW8= github.com/decred/vspd/client/v4 v4.0.0 h1:8qFB3t5CyBFuUU0LarJFsq/+twfUMPALwzOsB99r818= github.com/decred/vspd/client/v4 v4.0.0/go.mod h1:jhqu4KGGOskQcPVZ3XZLVZ1Wgkc9GQo+oEipr3gGODg= github.com/decred/vspd/types/v2 v2.1.0 h1:cUVlmHPeLVsksPRnr2WHsmC2t1Skl6g1WH0HmpcPS7w= @@ -712,8 +698,6 @@ github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= @@ -1190,7 +1174,6 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -1199,8 +1182,6 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -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/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1297,8 +1278,6 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1319,8 +1298,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1409,16 +1386,12 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.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/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1431,8 +1404,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -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/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1614,8 +1585,6 @@ google.golang.org/genproto v0.0.0-20210207032614-bba0dbe2a9ea/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210426193834-eac7f76ac494/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210521181308-5ccab8a35a9a/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1644,8 +1613,6 @@ google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= -google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.0.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= 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= From 7a8635756005cac005f5158f296aa8cdf8b79bf1 Mon Sep 17 00:00:00 2001 From: dev-warrior777 <> Date: Mon, 16 Dec 2024 23:33:26 +0800 Subject: [PATCH 3/4] client: Add tests and tidy. - Add testnet tests - Add more Documentation --- client/asset/firo/exx.go | 69 +++++++++++++++++++++++++++-------- client/asset/firo/exx_test.go | 67 ++++++++++++++++++++++++++++++---- client/asset/firo/firo.go | 3 +- 3 files changed, 114 insertions(+), 25 deletions(-) diff --git a/client/asset/firo/exx.go b/client/asset/firo/exx.go index 26e882b867..9aff931282 100644 --- a/client/asset/firo/exx.go +++ b/client/asset/firo/exx.go @@ -1,47 +1,85 @@ package firo import ( + "errors" "fmt" "strings" + dexfiro "decred.org/dcrdex/dex/networks/firo" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/base58" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" ) +// An EXX Address, also called an Exchange Address, is a re-encoding of a +// transparent Firo P2PKH address. It is required in order to send funds to +// Binance and some other centralized exchanges. + +const ( + PKH_LEN = 20 + SCRIPT_LEN = 26 +) + const ( - PKH_LEN = 20 - MAINNET_VER_BYTE_PKH = 0x52 - MAINNET_VER_BYTE_EXX = 0x01 - TESTNET_VER_BYTE_PKH = 0x41 - TESTNET_VER_BYTE_EXT = 0x01 - ALLNETS_EXTRA_BYTE_EX_ONE = 0xb9 // python: ADDRTYPE_EXP2PKH - MAINNET_EXTRA_BYTE_EXX_TWO = 0xbb - TESTNET_EXTRA_BYTE_EXT_TWO = 0xb1 + VERSION_01 = 0x01 + MAINNET_VER_BYTE_PKH = 0x52 + MAINNET_VER_BYTE_EXX = 0x01 + TESTNET_VER_BYTE_PKH = 0x41 + TESTNET_VER_BYTE_EXT = 0x01 + ALLNETS_EXTRA_BYTE_ONE = 0xb9 + MAINNET_EXTRA_BYTE_TWO = 0xbb + TESTNET_EXTRA_BYTE_TWO = 0xb1 +) + +// OP_EXCHANGEADDR is an unused bitcoin script opcode used to 'mark' the output +// as an exchange address for the recipient. +const OP_EXCHANGEADDR = 0xe0 - EXCHANGE_MARKER_BYTE = 0xe0 // python: OP_EXCHANGEADDR - SCRIPT_LEN = 26 +var ( + errInvalidVersion = errors.New("invalid version") + errInvalidExtra = errors.New("invalid extra") + errInvalidDecodedLength = errors.New("invalid decoded length") ) +// isExxAddress determines whether the address encoding is a mainnet EXX address +// or a testnet EXT address. func isExxAddress(address string) bool { return strings.HasPrefix(address, "EXX") || strings.HasPrefix(address, "EXT") } // decodeExxAddress decodes a Firo exchange address. func decodeExxAddress(encodedAddr string, net *chaincfg.Params) (btcutil.Address, error) { - decoded, _, err := base58.CheckDecode(encodedAddr) + decoded, ver, err := base58.CheckDecode(encodedAddr) if err != nil { return nil, err } - // pull out the pkh - Mainnet: (ver=0x01) <0xb9> <0xbb> + + if ver != VERSION_01 { + return nil, errInvalidVersion + } + if decoded[0] != ALLNETS_EXTRA_BYTE_ONE { + return nil, errInvalidExtra + } + switch net { + case dexfiro.MainNetParams: + if decoded[1] != MAINNET_EXTRA_BYTE_TWO { + return nil, errInvalidExtra + } + case dexfiro.TestNetParams: + if decoded[1] != TESTNET_EXTRA_BYTE_TWO { + return nil, errInvalidExtra + } + default: + return nil, errInvalidExtra + } decLen := len(decoded) if decLen < PKH_LEN+1 { - return nil, fmt.Errorf("bad decoded len %d", decLen) + return nil, errInvalidDecodedLength } + decExtra := decLen - PKH_LEN pkh := decoded[decExtra:] - addrPKH, err := btcutil.NewAddressPubKeyHash(pkh, net) if err != nil { return nil, err @@ -49,6 +87,7 @@ func decodeExxAddress(encodedAddr string, net *chaincfg.Params) (btcutil.Address return btcutil.Address(addrPKH), nil } +// buildExxPayToScript builds a P2PKH output script for a Firo exchange address. func buildExxPayToScript(addr btcutil.Address, address string) ([]byte, error) { if _, isPKH := addr.(*btcutil.AddressPubKeyHash); !isPKH { return nil, fmt.Errorf("address %s does not contain a pubkey hash", address) @@ -58,7 +97,7 @@ func buildExxPayToScript(addr btcutil.Address, address string) ([]byte, error) { return nil, err } script := make([]byte, 0, len(baseScript)+1) - script = append(script, EXCHANGE_MARKER_BYTE) + script = append(script, OP_EXCHANGEADDR) script = append(script, baseScript...) return script, nil } diff --git a/client/asset/firo/exx_test.go b/client/asset/firo/exx_test.go index f99c39469f..b80715bafa 100644 --- a/client/asset/firo/exx_test.go +++ b/client/asset/firo/exx_test.go @@ -11,14 +11,18 @@ import ( ) const ( - exxAddress = "EXXKcAcVWXeG7S9aiXXGuGNZkWdB9XuSbJ1z" - decoded = "386ed39285803b1782d0e363897f1a81a5b87421" - // 386ed39285803b1782d0e363897f1a81a5b87421 - // e0 76a914 386ed39285803b1782d0e363897f1a81a5b87421 88ac - // validateaddress firo-qt, electrum-firo just says 'true' - encodedAsPKH = "a5rrM1DY9XTRucbNrJQDtDc6GiEbcX7jRd" + exxAddress = "EXXKcAcVWXeG7S9aiXXGuGNZkWdB9XuSbJ1z" + scriptAddress = "386ed39285803b1782d0e363897f1a81a5b87421" + encodedAsPKH = "a5rrM1DY9XTRucbNrJQDtDc6GiEbcX7jRd" + testnetExtAddress = "EXTSnBDP57YoFRzLwHQoP1grxh9j52FKmRBY" + testnetScriptAddress = "963f2fd5ee2ee37d0b327794fc915d01343a4891" + testnetEncodedAsPKH = "TPfe48h75oMJ2LqXZtYjodumPjMUx64PGK" ) +/////////////////////////////////////////////////////////////////////////////// +// Mainnet +/////////////////////////////////////////////////////////////////////////////// + func TestDecodeExxAddress(t *testing.T) { addr, err := decodeExxAddress(exxAddress, dexfiro.MainNetParams) if err != nil { @@ -35,11 +39,11 @@ func TestDecodeExxAddress(t *testing.T) { if !addr.IsForNet(dexfiro.MainNetParams) { t.Fatalf("IsForNet failed") } - decodedB, err := hex.DecodeString(decoded) + scriptAddressB, err := hex.DecodeString(scriptAddress) if err != nil { t.Fatalf("hex decode error: %v", err) } - if !bytes.Equal(addr.ScriptAddress(), decodedB) { + if !bytes.Equal(addr.ScriptAddress(), scriptAddressB) { t.Fatalf("ScriptAddress failed") } s := addr.String() @@ -61,3 +65,50 @@ func TestBuildExxPayToScript(t *testing.T) { t.Fatalf("wrong script length - expected %d got %d", SCRIPT_LEN, len(script)) } } + +/////////////////////////////////////////////////////////////////////////////// +// Testnet +/////////////////////////////////////////////////////////////////////////////// + +func TestDecodeExtAddress(t *testing.T) { + addr, err := decodeExxAddress(testnetExtAddress, dexfiro.TestNetParams) + if err != nil { + t.Fatalf("testnet - addr=%v - %v", addr, err) + } + + switch ty := addr.(type) { + case btcutil.Address: + fmt.Printf("testnet - type=%T\n", ty) + default: + t.Fatalf("testnet - invalid type=%T", ty) + } + + if !addr.IsForNet(dexfiro.TestNetParams) { + t.Fatalf("testnet - IsForNet failed") + } + testnetScriptAddressB, err := hex.DecodeString(testnetScriptAddress) + if err != nil { + t.Fatalf("testnet - hex decode error: %v", err) + } + if !bytes.Equal(addr.ScriptAddress(), testnetScriptAddressB) { + t.Fatalf("testnet - ScriptAddress failed") + } + s := addr.String() + if s != testnetEncodedAsPKH { + t.Fatalf("testnet - String failed expected %s got %s", encodedAsPKH, s) + } +} + +func TestBuildExtPayToScript(t *testing.T) { + addr, err := decodeExxAddress(testnetExtAddress, dexfiro.TestNetParams) + if err != nil { + t.Fatalf("testnet - addr=%v - %v", addr, err) + } + script, err := buildExxPayToScript(addr, testnetExtAddress) + if err != nil { + t.Fatal(err) + } + if len(script) != SCRIPT_LEN { + t.Fatalf("testnet - wrong script length - expected %d got %d", SCRIPT_LEN, len(script)) + } +} diff --git a/client/asset/firo/firo.go b/client/asset/firo/firo.go index 3016fcb57d..b566828d16 100644 --- a/client/asset/firo/firo.go +++ b/client/asset/firo/firo.go @@ -202,7 +202,7 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) Helper Functions ******************************************************************************/ -// decodeAddress decodes a firo address. For normal transparent addresses this +// decodeAddress decodes a Firo address. For normal transparent addresses this // just uses btcd: btcutil.DecodeAddress. func decodeAddress(address string, net *chaincfg.Params) (btcutil.Address, error) { if isExxAddress(address) { @@ -252,7 +252,6 @@ func privKeyForAddress(c rpcCaller, addr string) (*btcec.PrivateKey, error) { } i := i0 + len(searchStr) auth := errStr[i : i+4] - /// fmt.Printf("OTA: %s\n", auth) err = c.CallRPC(methodDumpPrivKey, []any{addr, auth}, &privkeyStr) if err != nil { From 5f6a49590110833253b1b070d2f9160893319ad8 Mon Sep 17 00:00:00 2001 From: dev-warrior777 <> Date: Tue, 17 Dec 2024 20:49:47 +0800 Subject: [PATCH 4/4] client/asset: re-implement using btc.PaymentScripter interface. --- client/asset/btc/btc.go | 25 +++-- client/asset/firo/exx.go | 173 +++++++++++++++++++++------------- client/asset/firo/exx_test.go | 61 ++++++++---- client/asset/firo/firo.go | 12 +-- 4 files changed, 160 insertions(+), 111 deletions(-) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 901bca5a9b..49f8296e3f 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -364,10 +364,6 @@ type BTCCloneCFG struct { // into an address string. If AddressStringer is not supplied, the // (btcutil.Address).String method will be used. AddressStringer dexbtc.AddressStringer // btcutil.Address => string, may be an override or just the String method - // PayToAddressScript is an optional argument that can make non-standard tx - // outputs. If PayToAddressScript is not supplied the (txscript).PayToAddrScript - // method will be used. Note the extra paramaeter for a string address. - PayToAddressScript func(btcutil.Address, string) ([]byte, error) // BlockDeserializer can be used in place of (*wire.MsgBlock).Deserialize. BlockDeserializer func([]byte) (*wire.MsgBlock, error) // ArglessChangeAddrRPC can be true if the getrawchangeaddress takes no @@ -434,6 +430,11 @@ type BTCCloneCFG struct { AssetID uint32 } +// PaymentScripter can be implemented to make non-standard payment scripts. +type PaymentScripter interface { + PaymentScript() ([]byte, error) +} + // RPCConfig adds a wallet name to the basic configuration. type RPCConfig struct { dexbtc.RPCConfig `ini:",extends"` @@ -805,7 +806,6 @@ type baseWallet struct { localFeeRate func(context.Context, RawRequester, uint64) (uint64, error) feeCache *feeRateCache decodeAddr dexbtc.AddressDecoder - payToAddress func(btcutil.Address, string) ([]byte, error) walletDir string deserializeTx func([]byte) (*wire.MsgTx, error) @@ -1322,13 +1322,6 @@ func newUnconnectedWallet(cfg *BTCCloneCFG, walletCfg *WalletConfig) (*baseWalle } } - addressPayer := cfg.PayToAddressScript - if addressPayer == nil { - addressPayer = func(addr btcutil.Address, _ string) ([]byte, error) { - return txscript.PayToAddrScript(addr) - } - } - w := &baseWallet{ symbol: cfg.Symbol, chainParams: cfg.ChainParams, @@ -1348,7 +1341,6 @@ func newUnconnectedWallet(cfg *BTCCloneCFG, walletCfg *WalletConfig) (*baseWalle feeCache: feeCache, decodeAddr: addrDecoder, stringAddr: addrStringer, - payToAddress: addressPayer, walletInfo: cfg.WalletInfo, deserializeTx: txDeserializer, serializeTx: txSerializer, @@ -4518,7 +4510,12 @@ func (btc *baseWallet) send(address string, val uint64, feeRate uint64, subtract if err != nil { return nil, 0, 0, fmt.Errorf("invalid address: %s", address) } - pay2script, err := btc.payToAddress(addr, address) + var pay2script []byte + if scripter, is := addr.(PaymentScripter); is { + pay2script, err = scripter.PaymentScript() + } else { + pay2script, err = txscript.PayToAddrScript(addr) + } if err != nil { return nil, 0, 0, fmt.Errorf("PayToAddrScript error: %w", err) } diff --git a/client/asset/firo/exx.go b/client/asset/firo/exx.go index 9aff931282..26bba7c628 100644 --- a/client/asset/firo/exx.go +++ b/client/asset/firo/exx.go @@ -1,10 +1,12 @@ package firo import ( + "bytes" + "crypto/sha256" "errors" "fmt" - "strings" + "decred.org/dcrdex/client/asset/btc" dexfiro "decred.org/dcrdex/dex/networks/firo" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/base58" @@ -16,88 +18,127 @@ import ( // transparent Firo P2PKH address. It is required in order to send funds to // Binance and some other centralized exchanges. -const ( - PKH_LEN = 20 - SCRIPT_LEN = 26 -) - -const ( - VERSION_01 = 0x01 - MAINNET_VER_BYTE_PKH = 0x52 - MAINNET_VER_BYTE_EXX = 0x01 - TESTNET_VER_BYTE_PKH = 0x41 - TESTNET_VER_BYTE_EXT = 0x01 - ALLNETS_EXTRA_BYTE_ONE = 0xb9 - MAINNET_EXTRA_BYTE_TWO = 0xbb - TESTNET_EXTRA_BYTE_TWO = 0xb1 -) - // OP_EXCHANGEADDR is an unused bitcoin script opcode used to 'mark' the output // as an exchange address for the recipient. -const OP_EXCHANGEADDR = 0xe0 +const ( + ExxMainnet byte = 0xbb + ExxTestnet byte = 0xb1 + ExxSimnet byte = 0xac + OP_EXCHANGEADDR byte = 0xe0 +) var ( - errInvalidVersion = errors.New("invalid version") - errInvalidExtra = errors.New("invalid extra") - errInvalidDecodedLength = errors.New("invalid decoded length") + ExxVersionedPrefix = [2]byte{0x01, 0xb9} ) -// isExxAddress determines whether the address encoding is a mainnet EXX address -// or a testnet EXT address. -func isExxAddress(address string) bool { - return strings.HasPrefix(address, "EXX") || strings.HasPrefix(address, "EXT") +// isExxAddress determines whether the address encoding is an EXX, EXT address +// for mainnet, testnet or regtest networks. +func isExxAddress(addr string) bool { + b, ver, err := base58.CheckDecode(addr) + switch { + case err != nil: + return false + case ver != ExxVersionedPrefix[0]: + return false + case len(b) != ripemd160HashSize+2: + return false + case b[0] != ExxVersionedPrefix[1]: + return false + } + return true +} + +func checksum(input []byte) (csum [4]byte) { + h0 := sha256.Sum256(input) + h1 := sha256.Sum256(h0[:]) + copy(csum[:], h1[:]) + return } // decodeExxAddress decodes a Firo exchange address. func decodeExxAddress(encodedAddr string, net *chaincfg.Params) (btcutil.Address, error) { - decoded, ver, err := base58.CheckDecode(encodedAddr) - if err != nil { - return nil, err - } + const ( + checksumLength = 4 + prefixLength = 3 + decodedLen = prefixLength + ripemd160HashSize + checksumLength // exx prefix + hash + checksum + ) - if ver != VERSION_01 { - return nil, errInvalidVersion - } - if decoded[0] != ALLNETS_EXTRA_BYTE_ONE { - return nil, errInvalidExtra + decoded := base58.Decode(encodedAddr) + + if len(decoded) != decodedLen { + return nil, fmt.Errorf("base 58 decoded to incorrect length. %d != %d", len(decoded), decodedLen) } - switch net { - case dexfiro.MainNetParams: - if decoded[1] != MAINNET_EXTRA_BYTE_TWO { - return nil, errInvalidExtra - } - case dexfiro.TestNetParams: - if decoded[1] != TESTNET_EXTRA_BYTE_TWO { - return nil, errInvalidExtra - } + netID := decoded[2] + var expNet string + switch netID { + case ExxMainnet: + expNet = dexfiro.MainNetParams.Name + case ExxTestnet: + expNet = dexfiro.TestNetParams.Name + case ExxSimnet: + expNet = dexfiro.RegressionNetParams.Name default: - return nil, errInvalidExtra + return nil, fmt.Errorf("unrecognized network name %s", expNet) } - decLen := len(decoded) - if decLen < PKH_LEN+1 { - return nil, errInvalidDecodedLength + if net.Name != expNet { + return nil, fmt.Errorf("wrong network. expected %s, got %s", net.Name, expNet) } - - decExtra := decLen - PKH_LEN - pkh := decoded[decExtra:] - addrPKH, err := btcutil.NewAddressPubKeyHash(pkh, net) - if err != nil { - return nil, err + csum := decoded[decodedLen-checksumLength:] + expectedCsum := checksum(decoded[:decodedLen-checksumLength]) + if !bytes.Equal(csum, expectedCsum[:]) { + return nil, errors.New("checksum mismatch") } - return btcutil.Address(addrPKH), nil + var h [ripemd160HashSize]byte + copy(h[:], decoded[prefixLength:decodedLen-checksumLength]) + return &addressEXX{ + hash: h, + netID: netID, + }, nil } -// buildExxPayToScript builds a P2PKH output script for a Firo exchange address. -func buildExxPayToScript(addr btcutil.Address, address string) ([]byte, error) { - if _, isPKH := addr.(*btcutil.AddressPubKeyHash); !isPKH { - return nil, fmt.Errorf("address %s does not contain a pubkey hash", address) - } - baseScript, err := txscript.PayToAddrScript(addr) - if err != nil { - return nil, err +const ripemd160HashSize = 20 + +// addressEXX implements btcutil.Address and btc.PaymentScripter +type addressEXX struct { + hash [ripemd160HashSize]byte + netID byte +} + +var _ btcutil.Address = (*addressEXX)(nil) +var _ btc.PaymentScripter = (*addressEXX)(nil) + +func (a *addressEXX) String() string { + return a.EncodeAddress() +} + +func (a *addressEXX) EncodeAddress() string { + return base58.CheckEncode(append([]byte{ExxVersionedPrefix[1], a.netID}, a.hash[:]...), ExxVersionedPrefix[0]) +} + +func (a *addressEXX) ScriptAddress() []byte { + return a.hash[:] +} + +func (a *addressEXX) IsForNet(chainParams *chaincfg.Params) bool { + switch a.netID { + case ExxMainnet: + return chainParams.Name == dexfiro.MainNetParams.Name + case ExxTestnet: + return chainParams.Name == dexfiro.TestNetParams.Name + case ExxSimnet: + return chainParams.Name == dexfiro.RegressionNetParams.Name } - script := make([]byte, 0, len(baseScript)+1) - script = append(script, OP_EXCHANGEADDR) - script = append(script, baseScript...) - return script, nil + return false +} + +func (a *addressEXX) PaymentScript() ([]byte, error) { + // OP_EXCHANGEADDR << OP_DUP << OP_HASH160 << ToByteVector(keyID) << OP_EQUALVERIFY << OP_CHECKSIG; + return txscript.NewScriptBuilder(). + AddOp(OP_EXCHANGEADDR). + AddOp(txscript.OP_DUP). + AddOp(txscript.OP_HASH160). + AddData(a.hash[:]). + AddOp(txscript.OP_EQUALVERIFY). + AddOp(txscript.OP_CHECKSIG). + Script() } diff --git a/client/asset/firo/exx_test.go b/client/asset/firo/exx_test.go index b80715bafa..b429788d6c 100644 --- a/client/asset/firo/exx_test.go +++ b/client/asset/firo/exx_test.go @@ -6,17 +6,20 @@ import ( "fmt" "testing" + "decred.org/dcrdex/client/asset/btc" dexfiro "decred.org/dcrdex/dex/networks/firo" "github.com/btcsuite/btcd/btcutil" ) const ( - exxAddress = "EXXKcAcVWXeG7S9aiXXGuGNZkWdB9XuSbJ1z" - scriptAddress = "386ed39285803b1782d0e363897f1a81a5b87421" - encodedAsPKH = "a5rrM1DY9XTRucbNrJQDtDc6GiEbcX7jRd" + exxAddress = "EXXKcAcVWXeG7S9aiXXGuGNZkWdB9XuSbJ1z" + scriptAddress = "386ed39285803b1782d0e363897f1a81a5b87421" + testnetExtAddress = "EXTSnBDP57YoFRzLwHQoP1grxh9j52FKmRBY" testnetScriptAddress = "963f2fd5ee2ee37d0b327794fc915d01343a4891" - testnetEncodedAsPKH = "TPfe48h75oMJ2LqXZtYjodumPjMUx64PGK" + + // Example: e0 76a914 386ed39285803b1782d0e363897f1a81a5b87421 88ac + scriptLenEXX = 1 + 3 + ripemd160HashSize + 2 ) /////////////////////////////////////////////////////////////////////////////// @@ -30,7 +33,7 @@ func TestDecodeExxAddress(t *testing.T) { } switch ty := addr.(type) { - case btcutil.Address: + case btcutil.Address, *addressEXX: fmt.Printf("type=%T\n", ty) default: t.Fatalf("invalid type=%T", ty) @@ -47,8 +50,12 @@ func TestDecodeExxAddress(t *testing.T) { t.Fatalf("ScriptAddress failed") } s := addr.String() - if s != encodedAsPKH { - t.Fatalf("String failed expected %s got %s", encodedAsPKH, s) + if s != exxAddress { + t.Fatalf("String failed expected %s got %s", exxAddress, s) + } + enc := addr.EncodeAddress() + if enc != exxAddress { + t.Fatalf("EncodeAddress failed expected %s got %s", exxAddress, enc) } } @@ -57,12 +64,17 @@ func TestBuildExxPayToScript(t *testing.T) { if err != nil { t.Fatalf("addr=%v - %v", addr, err) } - script, err := buildExxPayToScript(addr, exxAddress) - if err != nil { - t.Fatal(err) + var script []byte + if scripter, is := addr.(btc.PaymentScripter); is { + script, err = scripter.PaymentScript() + if err != nil { + t.Fatal(err) + } + } else { + t.Fatal("addr does not implement btc.PaymentScripter") } - if len(script) != SCRIPT_LEN { - t.Fatalf("wrong script length - expected %d got %d", SCRIPT_LEN, len(script)) + if len(script) != scriptLenEXX { + t.Fatalf("wrong script length - expected %d got %d", scriptLenEXX, len(script)) } } @@ -94,8 +106,12 @@ func TestDecodeExtAddress(t *testing.T) { t.Fatalf("testnet - ScriptAddress failed") } s := addr.String() - if s != testnetEncodedAsPKH { - t.Fatalf("testnet - String failed expected %s got %s", encodedAsPKH, s) + if s != testnetExtAddress { + t.Fatalf("testnet - String failed expected %s got %s", testnetExtAddress, s) + } + enc := addr.EncodeAddress() + if enc != testnetExtAddress { + t.Fatalf("EncodeAddress failed expected %s got %s", testnetExtAddress, enc) } } @@ -104,11 +120,16 @@ func TestBuildExtPayToScript(t *testing.T) { if err != nil { t.Fatalf("testnet - addr=%v - %v", addr, err) } - script, err := buildExxPayToScript(addr, testnetExtAddress) - if err != nil { - t.Fatal(err) - } - if len(script) != SCRIPT_LEN { - t.Fatalf("testnet - wrong script length - expected %d got %d", SCRIPT_LEN, len(script)) + var script []byte + if scripter, is := addr.(btc.PaymentScripter); is { + script, err = scripter.PaymentScript() + if err != nil { + t.Fatal(err) + } + } else { + t.Fatal("addr does not implement btc.PaymentScripter") + } + if len(script) != scriptLenEXX { + t.Fatalf("wrong script length - expected %d got %d", scriptLenEXX, len(script)) } } diff --git a/client/asset/firo/firo.go b/client/asset/firo/firo.go index b566828d16..d1009e309a 100644 --- a/client/asset/firo/firo.go +++ b/client/asset/firo/firo.go @@ -22,7 +22,6 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/txscript" ) const ( @@ -170,7 +169,6 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) FeeEstimator: estimateFee, ExternalFeeEstimator: externalFeeRate, AddressDecoder: decodeAddress, - PayToAddressScript: payToAddressScript, PrivKeyFunc: nil, // set only for walletTypeRPC below } @@ -218,15 +216,6 @@ func decodeAddress(address string, net *chaincfg.Params) (btcutil.Address, error return decAddr, nil } -// payToAddressScript builds a P2PKH script for a Firo output. For normal transparent -// addresses btcd: txscript.PayToAddrScript is used. -func payToAddressScript(addr btcutil.Address, address string) ([]byte, error) { - if isExxAddress(address) { - return buildExxPayToScript(addr, address) - } - return txscript.PayToAddrScript(addr) -} - // rpcCaller is satisfied by ExchangeWalletFullNode (baseWallet), providing // direct RPC requests. type rpcCaller interface { @@ -252,6 +241,7 @@ func privKeyForAddress(c rpcCaller, addr string) (*btcec.PrivateKey, error) { } i := i0 + len(searchStr) auth := errStr[i : i+4] + /// fmt.Printf("OTA: %s\n", auth) err = c.CallRPC(methodDumpPrivKey, []any{addr, auth}, &privkeyStr) if err != nil {