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 all 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
12 changes: 11 additions & 1 deletion client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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)
}
Expand Down
144 changes: 144 additions & 0 deletions client/asset/firo/exx.go
Original file line number Diff line number Diff line change
@@ -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()
}
135 changes: 135 additions & 0 deletions client/asset/firo/exx_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
21 changes: 21 additions & 0 deletions client/asset/firo/firo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
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
Loading
Loading