diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 3a6b48ac2e..49f8296e3f 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -430,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"` @@ -4505,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 := txscript.PayToAddrScript(addr) + 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 new file mode 100644 index 0000000000..26bba7c628 --- /dev/null +++ b/client/asset/firo/exx.go @@ -0,0 +1,144 @@ +package firo + +import ( + "bytes" + "crypto/sha256" + "errors" + "fmt" + + "decred.org/dcrdex/client/asset/btc" + 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. + +// OP_EXCHANGEADDR is an unused bitcoin script opcode used to 'mark' the output +// as an exchange address for the recipient. +const ( + ExxMainnet byte = 0xbb + ExxTestnet byte = 0xb1 + ExxSimnet byte = 0xac + OP_EXCHANGEADDR byte = 0xe0 +) + +var ( + ExxVersionedPrefix = [2]byte{0x01, 0xb9} +) + +// 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) { + const ( + checksumLength = 4 + prefixLength = 3 + decodedLen = prefixLength + ripemd160HashSize + checksumLength // exx prefix + hash + checksum + ) + + decoded := base58.Decode(encodedAddr) + + if len(decoded) != decodedLen { + return nil, fmt.Errorf("base 58 decoded to incorrect length. %d != %d", len(decoded), decodedLen) + } + 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, fmt.Errorf("unrecognized network name %s", expNet) + } + if net.Name != expNet { + return nil, fmt.Errorf("wrong network. expected %s, got %s", net.Name, expNet) + } + csum := decoded[decodedLen-checksumLength:] + expectedCsum := checksum(decoded[:decodedLen-checksumLength]) + if !bytes.Equal(csum, expectedCsum[:]) { + return nil, errors.New("checksum mismatch") + } + var h [ripemd160HashSize]byte + copy(h[:], decoded[prefixLength:decodedLen-checksumLength]) + return &addressEXX{ + hash: h, + netID: netID, + }, nil +} + +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 + } + 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 new file mode 100644 index 0000000000..b429788d6c --- /dev/null +++ b/client/asset/firo/exx_test.go @@ -0,0 +1,135 @@ +package firo + +import ( + "bytes" + "encoding/hex" + "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" + + testnetExtAddress = "EXTSnBDP57YoFRzLwHQoP1grxh9j52FKmRBY" + testnetScriptAddress = "963f2fd5ee2ee37d0b327794fc915d01343a4891" + + // Example: e0 76a914 386ed39285803b1782d0e363897f1a81a5b87421 88ac + scriptLenEXX = 1 + 3 + ripemd160HashSize + 2 +) + +/////////////////////////////////////////////////////////////////////////////// +// Mainnet +/////////////////////////////////////////////////////////////////////////////// + +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, *addressEXX: + fmt.Printf("type=%T\n", ty) + default: + t.Fatalf("invalid type=%T", ty) + } + + if !addr.IsForNet(dexfiro.MainNetParams) { + t.Fatalf("IsForNet failed") + } + scriptAddressB, err := hex.DecodeString(scriptAddress) + if err != nil { + t.Fatalf("hex decode error: %v", err) + } + if !bytes.Equal(addr.ScriptAddress(), scriptAddressB) { + t.Fatalf("ScriptAddress failed") + } + s := addr.String() + 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) + } +} + +func TestBuildExxPayToScript(t *testing.T) { + addr, err := decodeExxAddress(exxAddress, dexfiro.MainNetParams) + if err != nil { + t.Fatalf("addr=%v - %v", addr, 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) != scriptLenEXX { + t.Fatalf("wrong script length - expected %d got %d", scriptLenEXX, 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 != 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) + } +} + +func TestBuildExtPayToScript(t *testing.T) { + addr, err := decodeExxAddress(testnetExtAddress, dexfiro.TestNetParams) + if err != nil { + t.Fatalf("testnet - addr=%v - %v", addr, 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) != 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 b2349da9f0..d1009e309a 100644 --- a/client/asset/firo/firo.go +++ b/client/asset/firo/firo.go @@ -168,6 +168,7 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) AssetID: BipID, FeeEstimator: estimateFee, ExternalFeeEstimator: externalFeeRate, + AddressDecoder: decodeAddress, PrivKeyFunc: nil, // set only for walletTypeRPC below } @@ -195,6 +196,26 @@ 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 +} + // 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" <