Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client: Allow Firo to send to an EXX address. #3119

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down
103 changes: 103 additions & 0 deletions client/asset/firo/exx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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 (
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

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, ver, err := base58.CheckDecode(encodedAddr)
if err != nil {
return nil, err
}

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, errInvalidDecodedLength
}

decExtra := decLen - PKH_LEN
pkh := decoded[decExtra:]
addrPKH, err := btcutil.NewAddressPubKeyHash(pkh, net)
if err != nil {
return nil, err
}
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)
}
baseScript, err := txscript.PayToAddrScript(addr)
if err != nil {
return nil, err
}
script := make([]byte, 0, len(baseScript)+1)
script = append(script, OP_EXCHANGEADDR)
script = append(script, baseScript...)
return script, nil
}
114 changes: 114 additions & 0 deletions client/asset/firo/exx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package firo

import (
"bytes"
"encoding/hex"
"fmt"
"testing"

dexfiro "decred.org/dcrdex/dex/networks/firo"
"github.com/btcsuite/btcd/btcutil"
)

const (
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 {
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")
}
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 != 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))
}
}

///////////////////////////////////////////////////////////////////////////////
// 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))
}
}
33 changes: 32 additions & 1 deletion client/asset/firo/firo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand All @@ -220,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 {
Expand Down
2 changes: 1 addition & 1 deletion dex/testing/firo/README_ELECTRUM_HARNESSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
14 changes: 7 additions & 7 deletions dex/testing/firo/electrum.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions dex/testing/firo/electrumx.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -53,7 +57,8 @@ export NET="regtest"
export DB_ENGINE="leveldb"
export DB_DIRECTORY="${DATA_DIR}"
export DAEMON_URL="http://user:[email protected]: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"
Expand Down
Loading
Loading