Skip to content

Commit

Permalink
finish v5 upgrade
Browse files Browse the repository at this point in the history
All txs we create and sign must be version 5. Use new SIGHASH
algos from ZIP-244. Move SIGHASH stuff to methods of dexzec.Tx.

Add live test to scan testnet blocks looking for deserialization
errors.
  • Loading branch information
buck54321 committed May 20, 2022
1 parent e6d296f commit 3d2c1e9
Show file tree
Hide file tree
Showing 26 changed files with 721 additions and 505 deletions.
6 changes: 4 additions & 2 deletions client/asset/bch/bch.go
Expand Up @@ -182,15 +182,17 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network)

// rawTxSigner signs the transaction using Bitcoin Cash's custom signature
// hash and signing algorithm.
func rawTxInSigner(btcTx *wire.MsgTx, idx int, subScript []byte, hashType txscript.SigHashType, btcKey *btcec.PrivateKey, val uint64) ([]byte, error) {
func rawTxInSigner(btcTx *wire.MsgTx, idx int, subScript []byte, hashType txscript.SigHashType,
btcKey *btcec.PrivateKey, vals []int64, _ [][]byte) ([]byte, error) {

bchTx, err := translateTx(btcTx)
if err != nil {
return nil, fmt.Errorf("btc->bch wire.MsgTx translation error: %v", err)
}

bchKey, _ := bchec.PrivKeyFromBytes(bchec.S256(), btcKey.Serialize())

return bchscript.RawTxInECDSASignature(bchTx, idx, subScript, bchscript.SigHashType(uint32(hashType)), bchKey, int64(val))
return bchscript.RawTxInECDSASignature(bchTx, idx, subScript, bchscript.SigHashType(uint32(hashType)), bchKey, vals[idx])
}

// serializeBtcTx serializes the wire.MsgTx.
Expand Down
51 changes: 33 additions & 18 deletions client/asset/btc/btc.go
Expand Up @@ -201,8 +201,11 @@ var (
}
)

// TxInSigner is a transaction input signer.
type TxInSigner func(tx *wire.MsgTx, idx int, subScript []byte, hashType txscript.SigHashType, key *btcec.PrivateKey, val uint64) ([]byte, error)
// TxInSigner is a transaction input signer. In addition to the standard Bitcoin
// arguments, TxInSigner receives all values and pubkey scripts for previous
// outpoints spent in this transaction.
type TxInSigner func(tx *wire.MsgTx, idx int, subScript []byte, hashType txscript.SigHashType,
key *btcec.PrivateKey, val []int64, prevScripts [][]byte) ([]byte, error)

// BTCCloneCFG holds clone specific parameters.
type BTCCloneCFG struct {
Expand Down Expand Up @@ -657,7 +660,7 @@ func (btc *ExchangeWalletSPV) Rescan(_ context.Context) error {
func (btc *ExchangeWalletFullNode) FeeRate() uint64 {
rate, err := btc.estimateFee(btc.node, 1)
if err != nil {
btc.log.Errorf("Failed to get fee rate: %v", err)
btc.log.Tracef("Failed to get fee rate: %v", err)
return 0
}
return rate
Expand Down Expand Up @@ -2159,9 +2162,10 @@ func (btc *baseWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin,
// Create a transaction that spends the referenced contract.
msgTx := wire.NewMsgTx(btc.txVersion())
var totalIn uint64
var contracts [][]byte
var addresses []btcutil.Address
var values []uint64
contracts := make([][]byte, 0, len(form.Redemptions))
prevScripts := make([][]byte, 0, len(form.Redemptions))
addresses := make([]btcutil.Address, 0, len(form.Redemptions))
var values []int64
for _, r := range form.Redemptions {
if r.Spends == nil {
return nil, nil, 0, fmt.Errorf("no audit info")
Expand All @@ -2183,11 +2187,16 @@ func (btc *baseWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin,
if !bytes.Equal(checkSecretHash[:], secretHash) {
return nil, nil, 0, fmt.Errorf("secret hash mismatch")
}
pkScript, err := btc.scriptHashScript(contract)
if err != nil {
return nil, nil, 0, fmt.Errorf("error constructs p2sh script: %v", err)
}
prevScripts = append(prevScripts, pkScript)
addresses = append(addresses, receiver)
contracts = append(contracts, contract)
txIn := wire.NewTxIn(cinfo.output.wireOutPoint(), nil, nil)
msgTx.AddTxIn(txIn)
values = append(values, cinfo.output.value)
values = append(values, int64(cinfo.output.value))
totalIn += cinfo.output.value
}

Expand Down Expand Up @@ -2252,7 +2261,7 @@ func (btc *baseWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin,
} else {
for i, r := range form.Redemptions {
contract := contracts[i]
redeemSig, redeemPubKey, err := btc.createSig(msgTx, i, contract, addresses[i], values[i])
redeemSig, redeemPubKey, err := btc.createSig(msgTx, i, contract, addresses[i], values, prevScripts)
if err != nil {
return nil, nil, 0, err
}
Expand Down Expand Up @@ -2804,14 +2813,19 @@ func (btc *baseWallet) refundTx(txHash *chainhash.Hash, vout uint32, contract de

if btc.segwit {
sigHashes := txscript.NewTxSigHashes(msgTx)
refundSig, refundPubKey, err := btc.createWitnessSig(msgTx, 0, contract, sender, val, sigHashes)
refundSig, refundPubKey, err := btc.createWitnessSig(msgTx, 0, contract, sender, int64(val), sigHashes)
if err != nil {
return nil, fmt.Errorf("createWitnessSig: %w", err)
}
txIn.Witness = dexbtc.RefundP2WSHContract(contract, refundSig, refundPubKey)

} else {
refundSig, refundPubKey, err := btc.createSig(msgTx, 0, contract, sender, val)
prevScript, err := btc.scriptHashScript(contract)
if err != nil {
return nil, fmt.Errorf("error constructing p2sh script: %w", err)
}

refundSig, refundPubKey, err := btc.createSig(msgTx, 0, contract, sender, []int64{int64(val)}, [][]byte{prevScript})
if err != nil {
return nil, fmt.Errorf("createSig: %w", err)
}
Expand Down Expand Up @@ -3431,7 +3445,7 @@ func (btc *baseWallet) txOutFromTxBytes(txB []byte, vout uint32) (*wire.TxOut, e

// createSig creates and returns the serialized raw signature and compressed
// pubkey for a transaction input signature.
func (btc *baseWallet) createSig(tx *wire.MsgTx, idx int, pkScript []byte, addr btcutil.Address, val uint64) (sig, pubkey []byte, err error) {
func (btc *baseWallet) createSig(tx *wire.MsgTx, idx int, pkScript []byte, addr btcutil.Address, vals []int64, pkScripts [][]byte) (sig, pubkey []byte, err error) {
addrStr, err := btc.stringAddr(addr, btc.chainParams)
if err != nil {
return nil, nil, err
Expand All @@ -3442,7 +3456,7 @@ func (btc *baseWallet) createSig(tx *wire.MsgTx, idx int, pkScript []byte, addr
return nil, nil, err
}

sig, err = btc.signNonSegwit(tx, idx, pkScript, txscript.SigHashAll, privKey, val)
sig, err = btc.signNonSegwit(tx, idx, pkScript, txscript.SigHashAll, privKey, vals, pkScripts)
if err != nil {
return nil, nil, err
}
Expand All @@ -3453,7 +3467,7 @@ func (btc *baseWallet) createSig(tx *wire.MsgTx, idx int, pkScript []byte, addr
// createWitnessSig creates and returns a signature for the witness of a segwit
// input and the pubkey associated with the address.
func (btc *baseWallet) createWitnessSig(tx *wire.MsgTx, idx int, pkScript []byte,
addr btcutil.Address, val uint64, sigHashes *txscript.TxSigHashes) (sig, pubkey []byte, err error) {
addr btcutil.Address, val int64, sigHashes *txscript.TxSigHashes) (sig, pubkey []byte, err error) {
addrStr, err := btc.stringAddr(addr, btc.chainParams)
if err != nil {
return nil, nil, err
Expand All @@ -3462,7 +3476,7 @@ func (btc *baseWallet) createWitnessSig(tx *wire.MsgTx, idx int, pkScript []byte
if err != nil {
return nil, nil, err
}
sig, err = txscript.RawTxInWitnessSignature(tx, sigHashes, idx, int64(val),
sig, err = txscript.RawTxInWitnessSignature(tx, sigHashes, idx, val,
pkScript, txscript.SigHashAll, privKey)

if err != nil {
Expand Down Expand Up @@ -3594,15 +3608,14 @@ func (btc *baseWallet) lockedSats() (uint64, error) {

// wireBytes dumps the serialized transaction bytes.
func (btc *baseWallet) wireBytes(tx *wire.MsgTx) []byte {
buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize()))
err := tx.Serialize(buf)
b, err := btc.serializeTx(tx)
// wireBytes is just used for logging, and a serialization error is
// extremely unlikely, so just log the error and return the nil bytes.
if err != nil {
btc.log.Errorf("error serializing %s transaction: %v", btc.symbol, err)
return nil
}
return buf.Bytes()
return b
}

// GetBestBlockHeight is exported for use by clone wallets. Not part of the
Expand Down Expand Up @@ -3728,7 +3741,9 @@ func toBTC(v uint64) float64 {

// rawTxInSig signs the transaction in input using the standard bitcoin
// signature hash and ECDSA algorithm.
func rawTxInSig(tx *wire.MsgTx, idx int, pkScript []byte, hashType txscript.SigHashType, key *btcec.PrivateKey, _ uint64) ([]byte, error) {
func rawTxInSig(tx *wire.MsgTx, idx int, pkScript []byte, hashType txscript.SigHashType,
key *btcec.PrivateKey, _ []int64, _ [][]byte) ([]byte, error) {

return txscript.RawTxInSignature(tx, idx, pkScript, txscript.SigHashAll, key)
}

Expand Down
4 changes: 3 additions & 1 deletion client/asset/btc/rpcclient.go
Expand Up @@ -116,6 +116,8 @@ func (wc *rpcClient) connect(ctx context.Context, _ *sync.WaitGroup) error {
if netVer < wc.minNetworkVersion {
return fmt.Errorf("reported node version %d is less than minimum %d", netVer, wc.minNetworkVersion)
}
// TODO: codeVer is actually asset-dependent. ZCash, for example, is at
// 170100. So we're just lucking out here, really.
if codeVer < minProtocolVersion {
return fmt.Errorf("node software out of date. version %d is less than minimum %d", codeVer, minProtocolVersion)
}
Expand Down Expand Up @@ -149,7 +151,6 @@ func (wc *rpcClient) SendRawTransactionLegacy(tx *wire.MsgTx) (*chainhash.Hash,
if err != nil {
return nil, err
}

return wc.callHashGetter(methodSendRawTransaction, anylist{
hex.EncodeToString(txBytes), false})
}
Expand Down Expand Up @@ -418,6 +419,7 @@ func (wc *rpcClient) signTx(inTx *wire.MsgTx) (*wire.MsgTx, error) {
if wc.legacySignTx {
method = methodSignTxLegacy
}

err = wc.call(method, anylist{hex.EncodeToString(txBytes)}, res)
if err != nil {
return nil, fmt.Errorf("tx signing error: %w", err)
Expand Down
5 changes: 5 additions & 0 deletions client/asset/interface.go
Expand Up @@ -104,6 +104,11 @@ type WalletDefinition struct {
// description for each option. This can be used to request config info from
// users e.g. via dynamically generated GUI forms.
ConfigOpts []*ConfigOption `json:"configopts"`
// NoAuth can be set true to hide the wallet password field during wallet
// creation.
// TODO: Use an asset.Authenticator interface and WalletTraits to do this
// instead.
NoAuth bool `json:"noauth"`
}

// Token combines the generic dex.Token with a WalletDefinition.
Expand Down
121 changes: 119 additions & 2 deletions client/asset/zec/regnet_test.go
Expand Up @@ -5,11 +5,18 @@ package zec
// Regnet tests expect the ZEC test harness to be running.

import (
"context"
"encoding/json"
"fmt"
"os/user"
"path/filepath"
"testing"

"decred.org/dcrdex/client/asset/btc/livetest"
"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/config"
dexzec "decred.org/dcrdex/dex/networks/zec"
"github.com/decred/dcrd/rpcclient/v7"
)

var (
Expand All @@ -31,12 +38,122 @@ func TestWallet(t *testing.T) {
Asset: tZEC,
FirstWallet: &livetest.WalletName{
Node: "alpha",
Filename: "zcash.conf",
Filename: "alpha.conf",
},
SecondWallet: &livetest.WalletName{
Node: "beta",
Filename: "zcash.conf",
Filename: "beta.conf",
},
Unencrypted: true,
})
}

// TestDeserializeTestnet must be run against a full RPC node.
func TestDeserializeTestnetBlocks(t *testing.T) {
cfg := struct {
RPCUser string `ini:"rpcuser"`
RPCPass string `ini:"rpcpassword"`
}{}

usr, _ := user.Current()
if err := config.ParseInto(filepath.Join(usr.HomeDir, ".zcash", "zcash.conf"), &cfg); err != nil {
t.Fatalf("config.Parse error: %v", err)
}

cl, err := rpcclient.New(&rpcclient.ConnConfig{
HTTPPostMode: true,
DisableTLS: true,
Host: "localhost:18232",
User: cfg.RPCUser,
Pass: cfg.RPCPass,
}, nil)
if err != nil {
t.Fatalf("error creating client: %v", err)
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

tipHash, err := cl.GetBestBlockHash(ctx)
if err != nil {
t.Fatalf("GetBestBlockHash error: %v", err)
}

lastV4Block, err := cl.GetBlockHash(ctx, testnetNU5ActivationHeight-1)
if err != nil {
t.Fatalf("GetBlockHash(%d) error: %v", testnetNU5ActivationHeight-1, err)
}

lastV3Block, err := cl.GetBlockHash(ctx, testnetSaplingActivationHeight-1)
if err != nil {
t.Fatalf("GetBlockHash(%d) error: %v", testnetSaplingActivationHeight-1, err)
}

lastV2Block, err := cl.GetBlockHash(ctx, testnetOverwinterActivationHeight-1)
if err != nil {
t.Fatalf("GetBlockHash(%d) error: %v", testnetOverwinterActivationHeight-1, err)
}

mustMarshal := func(thing interface{}) json.RawMessage {
b, err := json.Marshal(thing)
if err != nil {
t.Fatalf("Failed to marshal %T thing: %v", thing, err)
}
return b
}

blockBytes := func(hashStr string) (blockB dex.Bytes) {
raw, err := cl.RawRequest(ctx, "getblock", []json.RawMessage{mustMarshal(hashStr), mustMarshal(0)})
if err != nil {
t.Fatalf("Failed to fetch block hash for %s: %v", hashStr, err)
}
if err := json.Unmarshal(raw, &blockB); err != nil {
t.Fatalf("Error unmarshaling block bytes for %s: %v", hashStr, err)
}
return
}

nBlocksFromHash := func(hashStr string, n int) {
for i := 0; i < n; i++ {
zecBlock, err := dexzec.DeserializeBlock(blockBytes(hashStr))
if err != nil {
t.Fatalf("Error deserializing %s: %v", hashStr, err)
}

// for i, tx := range zecBlock.Transactions {
// switch {
// case tx.NActionsOrchard > 0 && tx.NOutputsSapling > 0:
// fmt.Printf("orchard + sapling shielded tx: %s:%d \n", hashStr, i)
// case tx.NActionsOrchard > 0:
// fmt.Printf("orchard shielded tx: %s:%d \n", hashStr, i)
// case tx.NOutputsSapling > 0 || tx.NSpendsSapling > 0:
// fmt.Printf("sapling shielded tx: %s:%d \n", hashStr, i)
// case tx.NJoinSplit > 0:
// fmt.Printf("joinsplit tx: %s:%d \n", hashStr, i)
// default:
// if i > 0 {
// fmt.Printf("unshielded tx: %s:%d \n", hashStr, i)
// }
// }
// }

hashStr = zecBlock.Header.PrevBlock.String()
}
}

// Test version 5 blocks.
fmt.Println("Testing version 5 blocks")
nBlocksFromHash(tipHash.String(), 1000)

// Test version 4 blocks.
fmt.Println("Testing version 4 blocks")
nBlocksFromHash(lastV4Block.String(), 1000)

// Test version 3 blocks.
fmt.Println("Testing version 3 blocks")
nBlocksFromHash(lastV3Block.String(), 1000)

// Test version 2 blocks.
fmt.Println("Testing version 2 blocks")
nBlocksFromHash(lastV2Block.String(), 1000)
}

0 comments on commit 3d2c1e9

Please sign in to comment.