From 5f6a49590110833253b1b070d2f9160893319ad8 Mon Sep 17 00:00:00 2001 From: dev-warrior777 <> Date: Tue, 17 Dec 2024 20:49:47 +0800 Subject: [PATCH] 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 {