Skip to content

Commit

Permalink
client/asset: re-implement using btc.PaymentScripter interface.
Browse files Browse the repository at this point in the history
  • Loading branch information
dev-warrior777 committed Dec 17, 2024
1 parent 7a86357 commit 5f6a495
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 111 deletions.
25 changes: 11 additions & 14 deletions client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down
173 changes: 107 additions & 66 deletions client/asset/firo/exx.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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()
}
61 changes: 41 additions & 20 deletions client/asset/firo/exx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

///////////////////////////////////////////////////////////////////////////////
Expand All @@ -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)
Expand All @@ -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)
}
}

Expand All @@ -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))
}
}

Expand Down Expand Up @@ -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)
}
}

Expand All @@ -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))
}
}
Loading

0 comments on commit 5f6a495

Please sign in to comment.