From f6cccaa6a3d29059613ca02f4bbcefa0fb35d7e1 Mon Sep 17 00:00:00 2001 From: buck54321 Date: Mon, 13 Jun 2022 09:27:22 -0500 Subject: [PATCH] multi: add support for ZCash Implement block deserialization, tx deserialization/serialization and input signing for ZCash and generalize those functions in the client and server btc packages. Implementation notes 1. zcashd does not support encrypted wallets. No passwords allowed. 2. After starting the harness, it takes a few minutes for beta to get caught up. 3. zcashd can take a very long time to get it's fee estimates primed. 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. --- client/asset/bch/bch.go | 26 +- client/asset/btc/btc.go | 290 ++++-- client/asset/btc/btc_test.go | 10 + client/asset/btc/livetest/livetest.go | 62 +- client/asset/btc/rpcclient.go | 120 ++- client/asset/btc/spv.go | 54 +- client/asset/btc/wallet.go | 43 + client/asset/interface.go | 5 + client/asset/zec/regnet_test.go | 159 +++ client/asset/zec/zec.go | 260 +++++ client/cmd/dexc/main.go | 1 + client/cmd/simnet-trade-tests/run | 15 +- client/core/simnet_trade.go | 41 +- client/webserver/site/src/img/coins/zec.png | Bin 0 -> 4061 bytes client/webserver/site/src/js/doc.ts | 3 +- client/webserver/site/src/js/forms.ts | 3 +- client/webserver/site/src/js/registry.ts | 2 + dex/networks/zec/addr.go | 97 ++ dex/networks/zec/addr_test.go | 30 + dex/networks/zec/block.go | 126 +++ dex/networks/zec/block_test.go | 101 ++ dex/networks/zec/params.go | 85 ++ dex/networks/zec/test-data/block_1624455.dat | Bin 0 -> 1721 bytes .../zec/test-data/shielded_sapling_tx.dat | Bin 0 -> 2373 bytes dex/networks/zec/test-data/simnet_block.dat | Bin 0 -> 177 bytes .../zec/test-data/simnet_block_header.dat | Bin 0 -> 177 bytes .../zec/test-data/solution_1624455.dat | Bin 0 -> 1344 bytes .../zec/test-data/unshielded_orchard_tx.dat | Bin 0 -> 244 bytes .../zec/test-data/unshielded_sapling_tx.dat | Bin 0 -> 244 bytes .../zec/test-data/v2_joinsplit_tx.dat | Bin 0 -> 1943 bytes dex/networks/zec/test-data/v3_tx.dat | Bin 0 -> 132 bytes dex/networks/zec/tx.go | 973 ++++++++++++++++++ dex/networks/zec/tx_test.go | 241 +++++ dex/testing/dcrdex/harness.sh | 29 + dex/testing/zec/alphawallet | 127 +++ dex/testing/zec/betawallet | 116 +++ dex/testing/zec/harness.sh | 322 ++++++ go.mod | 3 +- go.sum | 5 +- run_tests.sh | 2 + server/asset/btc/btc.go | 151 ++- server/asset/btc/cache.go | 3 +- server/asset/btc/live_test.go | 26 +- server/asset/btc/rpcclient.go | 189 +++- server/asset/btc/tx.go | 66 +- server/asset/dcr/live_test.go | 2 +- server/asset/zec/live_test.go | 81 ++ server/asset/zec/zec.go | 261 +++++ server/asset/zec/zec_test.go | 41 + server/cmd/dcrdex/main.go | 1 + server/cmd/dexcoin/main.go | 1 + 51 files changed, 3844 insertions(+), 329 deletions(-) create mode 100644 client/asset/zec/regnet_test.go create mode 100644 client/asset/zec/zec.go create mode 100644 client/webserver/site/src/img/coins/zec.png create mode 100644 dex/networks/zec/addr.go create mode 100644 dex/networks/zec/addr_test.go create mode 100644 dex/networks/zec/block.go create mode 100644 dex/networks/zec/block_test.go create mode 100644 dex/networks/zec/params.go create mode 100644 dex/networks/zec/test-data/block_1624455.dat create mode 100644 dex/networks/zec/test-data/shielded_sapling_tx.dat create mode 100644 dex/networks/zec/test-data/simnet_block.dat create mode 100644 dex/networks/zec/test-data/simnet_block_header.dat create mode 100644 dex/networks/zec/test-data/solution_1624455.dat create mode 100644 dex/networks/zec/test-data/unshielded_orchard_tx.dat create mode 100644 dex/networks/zec/test-data/unshielded_sapling_tx.dat create mode 100644 dex/networks/zec/test-data/v2_joinsplit_tx.dat create mode 100644 dex/networks/zec/test-data/v3_tx.dat create mode 100644 dex/networks/zec/tx.go create mode 100644 dex/networks/zec/tx_test.go create mode 100644 dex/testing/zec/alphawallet create mode 100644 dex/testing/zec/betawallet create mode 100755 dex/testing/zec/harness.sh create mode 100644 server/asset/zec/live_test.go create mode 100644 server/asset/zec/zec.go create mode 100644 server/asset/zec/zec_test.go diff --git a/client/asset/bch/bch.go b/client/asset/bch/bch.go index 147366a36b..463d2449ee 100644 --- a/client/asset/bch/bch.go +++ b/client/asset/bch/bch.go @@ -170,33 +170,21 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) // https://gist.github.com/markblundeberg/a3aba3c9d610e59c3c49199f697bc38b#making-unmalleable-smart-contracts // https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki NonSegwitSigner: rawTxInSigner, - // The old allowHighFees bool argument to sendrawtransaction. - ArglessChangeAddrRPC: true, + OmitAddressType: true, // Bitcoin Cash uses estimatefee instead of estimatesmartfee, and even // then, they modified it from the old Bitcoin Core estimatefee by // removing the confirmation target argument. FeeEstimator: estimateFee, } - xcWallet, err := btc.BTCCloneWallet(cloneCFG) - if err != nil { - return nil, err - } - - return &BCHWallet{ - ExchangeWalletFullNode: xcWallet, - }, nil -} - -// BCHWallet embeds btc.ExchangeWalletFullNode, but re-implements a couple of -// methods to perform on-the-fly address translation. -type BCHWallet struct { - *btc.ExchangeWalletFullNode + return btc.BTCCloneWallet(cloneCFG) } // 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) @@ -204,7 +192,7 @@ func rawTxInSigner(btcTx *wire.MsgTx, idx int, subScript []byte, hashType txscri 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. @@ -242,7 +230,7 @@ func translateTx(btcTx *wire.MsgTx) (*bchwire.MsgTx, error) { return nil, err } - bchTx := bchwire.NewMsgTx(bchwire.TxVersion) + bchTx := new(bchwire.MsgTx) err = bchTx.Deserialize(bytes.NewBuffer(txB)) if err != nil { return nil, err diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 436a06eb4c..14bfeead9f 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -206,8 +206,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, vals []int64, prevScripts [][]byte) ([]byte, error) // BTCCloneCFG holds clone specific parameters. type BTCCloneCFG struct { @@ -224,6 +227,9 @@ type BTCCloneCFG struct { // LegacyBalance is for clones that don't yet support the 'getbalances' RPC // call. LegacyBalance bool + // ZECStyleBalance is for clones that don't support getbalances or + // walletinfo, and don't take an account name argument. + ZECStyleBalance bool // If segwit is false, legacy addresses and contracts will be used. This // setting must match the configuration of the server's asset backend. Segwit bool @@ -258,6 +264,9 @@ type BTCCloneCFG struct { // BooleanGetBlockRPC causes the RPC client to use a boolean second argument // for the getblock endpoint, instead of Bitcoin's numeric. BooleanGetBlockRPC bool + // NumericGetRawRPC uses a numeric boolean indicator for the + // getrawtransaction RPC. + NumericGetRawRPC bool // LegacyValidateAddressRPC uses the validateaddress endpoint instead of // getwalletinfo in order to discover ownership of an address. LegacyValidateAddressRPC bool @@ -271,6 +280,20 @@ type BTCCloneCFG struct { // output value) that doesn't depend on the serialized size of the output. // If ConstantDustLimit is zero, dexbtc.IsDust is used. ConstantDustLimit uint64 + // TxDeserializer is an optional function used to deserialize a transaction. + TxDeserializer func([]byte) (*wire.MsgTx, error) + // TxSerializer is an optional function used to serialize a transaction. + TxSerializer func(*wire.MsgTx) ([]byte, error) + // TxHasher is a function that generates a tx hash from a MsgTx. + TxHasher func(*wire.MsgTx) *chainhash.Hash + // TxSizeCalculator is an optional function that will be used to calculate + // the size of a transaction. + TxSizeCalculator func(*wire.MsgTx) uint64 + // TxVersion is an optional function that returns a version to use for + // new transactions. + TxVersion func() int32 + // ManualMedianTime causes the median time to be calculated manually. + ManualMedianTime bool } // outPoint is the hash and output index of a transaction output. @@ -454,7 +477,9 @@ func readRPCWalletConfig(settings map[string]string, symbol string, net dex.Netw // parseRPCWalletConfig parses a *RPCWalletConfig from the settings map and // creates the unconnected *rpcclient.Client. -func parseRPCWalletConfig(settings map[string]string, symbol string, net dex.Network, ports dexbtc.NetPorts, singularWallet bool) (*RPCWalletConfig, *rpcclient.Client, error) { +func parseRPCWalletConfig(settings map[string]string, symbol string, net dex.Network, + ports dexbtc.NetPorts, singularWallet bool) (*RPCWalletConfig, *rpcclient.Client, error) { + cfg, err := readRPCWalletConfig(settings, symbol, net, ports) if err != nil { return nil, nil, err @@ -592,11 +617,17 @@ type baseWallet struct { redeemConfTarget uint64 useSplitTx bool useLegacyBalance bool + zecStyleBalance bool segwit bool signNonSegwit TxInSigner estimateFee func(RawRequester, uint64) (uint64, error) // TODO: resolve the awkwardness of an RPC-oriented func in a generic framework decodeAddr dexbtc.AddressDecoder + deserializeTx func([]byte) (*wire.MsgTx, error) + serializeTx func(*wire.MsgTx) ([]byte, error) + calcTxSize func(*wire.MsgTx) uint64 + hashTx func(*wire.MsgTx) *chainhash.Hash stringAddr dexbtc.AddressStringer + txVersion func() int32 tipMtx sync.RWMutex currentTip *block @@ -692,7 +723,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 @@ -808,17 +839,6 @@ func BTCCloneWallet(cfg *BTCCloneCFG) (*ExchangeWalletFullNode, error) { return btc, nil } -func newRPCWalletConnection(cfg *RPCWalletConfig) (*rpcclient.Client, error) { - endpoint := cfg.RPCBind + "/wallet/" + cfg.WalletName - return rpcclient.New(&rpcclient.ConnConfig{ - HTTPPostMode: true, - DisableTLS: true, - Host: endpoint, - User: cfg.RPCUser, - Pass: cfg.RPCPass, - }, nil) -} - // newRPCWallet creates the ExchangeWallet and starts the block monitor. func newRPCWallet(requester RawRequesterWithContext, cfg *BTCCloneCFG, walletConfig *WalletConfig) (*ExchangeWalletFullNode, error) { btc, err := newUnconnectedWallet(cfg, walletConfig) @@ -837,7 +857,6 @@ func newRPCWallet(requester RawRequesterWithContext, cfg *BTCCloneCFG, walletCon decodeAddr: btc.decodeAddr, stringAddr: btc.stringAddr, deserializeBlock: blockDeserializer, - arglessChangeAddrRPC: cfg.ArglessChangeAddrRPC, legacyRawSends: cfg.LegacyRawFeeLimit, minNetworkVersion: cfg.MinNetworkVersion, log: cfg.Logger.SubLogger("RPC"), @@ -845,8 +864,13 @@ func newRPCWallet(requester RawRequesterWithContext, cfg *BTCCloneCFG, walletCon omitAddressType: cfg.OmitAddressType, legacySignTx: cfg.LegacySignTxRPC, booleanGetBlock: cfg.BooleanGetBlockRPC, - legacyValidateAddressRPC: cfg.LegacyValidateAddressRPC, unlockSpends: cfg.UnlockSpends, + deserializeTx: btc.deserializeTx, + serializeTx: btc.serializeTx, + hashTx: btc.hashTx, + numericGetRawTxRPC: cfg.NumericGetRawRPC, + legacyValidateAddressRPC: cfg.LegacyValidateAddressRPC, + manualMedianTime: cfg.ManualMedianTime, }) return &ExchangeWalletFullNode{btc}, nil } @@ -887,18 +911,41 @@ func newUnconnectedWallet(cfg *BTCCloneCFG, walletCfg *WalletConfig) (*baseWalle addrDecoder = cfg.AddressDecoder } - addrStringer := cfg.AddressStringer - if cfg.AddressStringer == nil { - addrStringer = func(addr btcutil.Address, _ *chaincfg.Params) (string, error) { - return addr.String(), nil - } - } - nonSegwitSigner := rawTxInSig if cfg.NonSegwitSigner != nil { nonSegwitSigner = cfg.NonSegwitSigner } + txDeserializer := cfg.TxDeserializer + if txDeserializer == nil { + txDeserializer = msgTxFromBytes + } + + txSerializer := cfg.TxSerializer + if txSerializer == nil { + txSerializer = serializeMsgTx + } + + txSizeCalculator := cfg.TxSizeCalculator + if txSizeCalculator == nil { + txSizeCalculator = dexbtc.MsgTxVBytes + } + + txHasher := cfg.TxHasher + if txHasher == nil { + txHasher = hashTx + } + + addrStringer := cfg.AddressStringer + if addrStringer == nil { + addrStringer = stringifyAddress + } + + txVersion := cfg.TxVersion + if txVersion == nil { + txVersion = func() int32 { return wire.TxVersion } + } + w := &baseWallet{ symbol: cfg.Symbol, chainParams: cfg.ChainParams, @@ -914,12 +961,18 @@ func newUnconnectedWallet(cfg *BTCCloneCFG, walletCfg *WalletConfig) (*baseWalle redeemConfTarget: redeemConfTarget, useSplitTx: walletCfg.UseSplitTx, useLegacyBalance: cfg.LegacyBalance, + zecStyleBalance: cfg.ZECStyleBalance, segwit: cfg.Segwit, signNonSegwit: nonSegwitSigner, estimateFee: cfg.FeeEstimator, decodeAddr: addrDecoder, stringAddr: addrStringer, walletInfo: cfg.WalletInfo, + deserializeTx: txDeserializer, + serializeTx: txSerializer, + hashTx: txHasher, + calcTxSize: txSizeCalculator, + txVersion: txVersion, } if w.estimateFee == nil { @@ -1018,10 +1071,26 @@ func (btc *baseWallet) IsDust(txOut *wire.TxOut, minRelayTxFee uint64) bool { // getBlockchainInfoResult models the data returned from the getblockchaininfo // command. type getBlockchainInfoResult struct { - Blocks int64 `json:"blocks"` - Headers int64 `json:"headers"` - BestBlockHash string `json:"bestblockhash"` - InitialBlockDownload bool `json:"initialblockdownload"` + Blocks int64 `json:"blocks"` + Headers int64 `json:"headers"` + BestBlockHash string `json:"bestblockhash"` + // InitialBlockDownload will be true if the node is still in the initial + // block download mode. + InitialBlockDownload *bool `json:"initialblockdownload"` + // InitialBlockDownloadComplete will be true if this node has completed its + // initial block download and is expected to be synced to the network. + // ZCash uses this terminology instead of initialblockdownload. + InitialBlockDownloadComplete *bool `json:"initial_block_download_complete"` +} + +func (r *getBlockchainInfoResult) syncing() bool { + if r.InitialBlockDownloadComplete != nil && *r.InitialBlockDownloadComplete { + return false + } + if r.InitialBlockDownload != nil && *r.InitialBlockDownload { + return true + } + return r.Headers-r.Blocks > 1 } // SyncStatus is information about the blockchain sync status. @@ -1065,7 +1134,7 @@ func (btc *baseWallet) OwnsDepositAddress(address string) (bool, error) { // Balance returns the total available funds in the wallet. Part of the // asset.Wallet interface. func (btc *baseWallet) Balance() (*asset.Balance, error) { - if btc.useLegacyBalance { + if btc.useLegacyBalance || btc.zecStyleBalance { return btc.legacyBalance() } balances, err := btc.node.balances() @@ -1092,6 +1161,15 @@ func (btc *baseWallet) legacyBalance() (*asset.Balance, error) { return nil, fmt.Errorf("legacyBalance unimplemented for spv clients") } + if btc.zecStyleBalance { + var bal uint64 + // args: "(dummy)" minconf includeWatchonly inZat + if err := cl.call(methodGetBalance, anylist{"", 0, false, true}, &bal); err != nil { + return nil, err + } + return &asset.Balance{Available: bal}, nil + } + walletInfo, err := cl.GetWalletInfo() if err != nil { return nil, fmt.Errorf("(legacy) GetWalletInfo error: %w", err) @@ -1791,9 +1869,9 @@ func (btc *baseWallet) split(value uint64, lots uint64, outputs []*output, if err != nil { return nil, false, err } - txHash := msgTx.TxHash() - op := newOutput(&txHash, 0, reqFunds) + txHash := btc.hashTx(msgTx) + op := newOutput(txHash, 0, reqFunds) // Need to save one funding coin (in the deferred function). fundingCoins = map[outPoint]*utxo{op.pt: { @@ -1964,7 +2042,7 @@ func (btc *baseWallet) Locked() bool { // fundedTx creates and returns a new MsgTx with the provided coins as inputs. func (btc *baseWallet) fundedTx(coins asset.Coins) (*wire.MsgTx, uint64, []outPoint, error) { - baseTx := wire.NewMsgTx(wire.TxVersion) + baseTx := wire.NewMsgTx(btc.txVersion()) var totalIn uint64 // Add the funding utxos. pts := make([]outPoint, 0, len(coins)) @@ -2673,12 +2751,12 @@ func (btc *baseWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, ui if err != nil { return nil, nil, 0, err } + txHash := btc.hashTx(msgTx) // Prepare the receipts. receipts := make([]asset.Receipt, 0, swapCount) - txHash := msgTx.TxHash() for i, contract := range swaps.Contracts { - output := newOutput(&txHash, uint32(i), contract.Value) + output := newOutput(txHash, uint32(i), contract.Value) signedRefundTx, err := btc.refundTx(output.txHash(), output.vout(), contracts[i], contract.Value, refundAddrs[i], swaps.FeeRate) if err != nil { return nil, nil, 0, fmt.Errorf("error creating refund tx: %w", err) @@ -2746,11 +2824,12 @@ func (btc *baseWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, ui // Redeem sends the redemption transaction, completing the atomic swap. func (btc *baseWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin, uint64, error) { // Create a transaction that spends the referenced contract. - msgTx := wire.NewMsgTx(wire.TxVersion) + 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)) + values := make([]int64, 0, len(form.Redemptions)) for _, r := range form.Redemptions { if r.Spends == nil { return nil, nil, 0, fmt.Errorf("no audit info") @@ -2772,16 +2851,21 @@ 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 } // Calculate the size and the fees. - size := dexbtc.MsgTxVBytes(msgTx) + size := btc.calcTxSize(msgTx) if btc.segwit { // Add the marker and flag weight here. witnessVBytes := (dexbtc.RedeemSwapSigScriptSize*uint64(len(form.Redemptions)) + 2 + 3) / 4 @@ -2844,7 +2928,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 } @@ -2856,12 +2940,12 @@ func (btc *baseWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin, } // Send the transaction. - checkHash := msgTx.TxHash() + checkHash := btc.hashTx(msgTx) txHash, err := btc.node.sendRawTransaction(msgTx) if err != nil { return nil, nil, 0, err } - if *txHash != checkHash { + if *txHash != *checkHash { return nil, nil, 0, fmt.Errorf("redemption sent, but received unexpected transaction ID back from RPC server. "+ "expected %s, got %s", *txHash, checkHash) } @@ -2961,7 +3045,7 @@ func (btc *baseWallet) AuditContract(coinID, contract, txData dex.Bytes, rebroad return nil, fmt.Errorf("error finding unspent contract: %s:%d : %w", txHash, vout, err) } } else { - tx, err = msgTxFromBytes(txData) + tx, err = btc.deserializeTx(txData) if err != nil { return nil, fmt.Errorf("coin not found, and error encountered decoding tx data: %v", err) } @@ -3042,16 +3126,11 @@ func (btc *baseWallet) LocktimeExpired(contract dex.Bytes) (bool, time.Time, err return false, time.Time{}, fmt.Errorf("error extracting contract locktime: %w", err) } contractExpiry := time.Unix(int64(locktime), 0).UTC() - bestBlockHash, err := btc.node.getBestBlockHash() + medianTime, err := btc.node.medianTime() if err != nil { - return false, time.Time{}, fmt.Errorf("get best block hash error: %w", err) + return false, time.Time{}, fmt.Errorf("error getting median time: %w", err) } - bestBlockHeader, err := btc.node.getBlockHeader(bestBlockHash) - if err != nil { - return false, time.Time{}, fmt.Errorf("get best block header error: %w", err) - } - bestBlockMedianTime := time.Unix(bestBlockHeader.MedianTime, 0).UTC() - return bestBlockMedianTime.After(contractExpiry), contractExpiry, nil + return medianTime.After(contractExpiry), contractExpiry, nil } // FindRedemption watches for the input that spends the specified contract @@ -3077,7 +3156,7 @@ func (btc *baseWallet) FindRedemption(ctx context.Context, coinID, _ dex.Bytes) return nil, nil, fmt.Errorf("error finding wallet transaction: %v", err) } - txOut, err := txOutFromTxBytes(tx.Hex, vout) + txOut, err := btc.txOutFromTxBytes(tx.Hex, vout) if err != nil { return nil, nil, err } @@ -3282,7 +3361,7 @@ func (btc *baseWallet) tryRedemptionRequests(ctx context.Context, startBlock *ch } if err := searchCtx.Err(); err != nil { if errors.Is(err, context.DeadlineExceeded) { - btc.log.Error("mempool search exceeded %s time limit", searchDur) + btc.log.Errorf("mempool search exceeded %s time limit", searchDur) } else { btc.log.Error("mempool search was cancelled") } @@ -3327,12 +3406,12 @@ func (btc *baseWallet) Refund(coinID, contract dex.Bytes, feeSuggestion uint64) return nil, fmt.Errorf("error creating refund tx: %w", err) } - checkHash := msgTx.TxHash() + checkHash := btc.hashTx(msgTx) refundHash, err := btc.node.sendRawTransaction(msgTx) if err != nil { return nil, fmt.Errorf("sendRawTransaction: %w", err) } - if *refundHash != checkHash { + if *refundHash != *checkHash { return nil, fmt.Errorf("refund sent, but received unexpected transaction ID back from RPC server. "+ "expected %s, got %s", *refundHash, checkHash) } @@ -3349,7 +3428,7 @@ func (btc *baseWallet) refundTx(txHash *chainhash.Hash, vout uint32, contract de // Create the transaction that spends the contract. feeRate := btc.targetFeeRateWithFallback(2, feeSuggestion) // meh level urgency - msgTx := wire.NewMsgTx(wire.TxVersion) + msgTx := wire.NewMsgTx(btc.txVersion()) msgTx.LockTime = uint32(lockTime) prevOut := wire.NewOutPoint(txHash, vout) txIn := wire.NewTxIn(prevOut, []byte{}, nil) @@ -3360,7 +3439,7 @@ func (btc *baseWallet) refundTx(txHash *chainhash.Hash, vout uint32, contract de msgTx.AddTxIn(txIn) // Calculate fees and add the change output. - size := dexbtc.MsgTxVBytes(msgTx) + size := btc.calcTxSize(msgTx) if btc.segwit { // Add the marker and flag weight too. @@ -3392,18 +3471,20 @@ func (btc *baseWallet) refundTx(txHash *chainhash.Hash, vout uint32, contract de msgTx.AddTxOut(txOut) if btc.segwit { - // NewTxSigHashes uses the PrevOutFetcher only for detecting a taproot - // output, so we can provide a dummy that always returns a wire.TxOut - // with a nil pkScript that so IsPayToTaproot returns false. sigHashes := txscript.NewTxSigHashes(msgTx, new(txscript.CannedPrevOutputFetcher)) - 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) } @@ -3493,7 +3574,7 @@ func (btc *baseWallet) send(address string, val uint64, feeRate uint64, subtract return nil, 0, 0, fmt.Errorf("failed to fetch transaction after send: %w", err) } - tx, err := msgTxFromBytes(txRes.Hex) + tx, err := btc.deserializeTx(txRes.Hex) if err != nil { return nil, 0, 0, fmt.Errorf("error decoding transaction: %w", err) } @@ -3822,13 +3903,13 @@ func (btc *baseWallet) checkRedemptionBlockDetails(outPt outPoint, blockHash *ch } blk, err := btc.node.getBlock(*blockHash) if err != nil { - return 0, fmt.Errorf("error retrieving redemption block for %s: %w", blockHash, err) + return 0, fmt.Errorf("error retrieving redemption block %s: %w", blockHash, err) } var tx *wire.MsgTx out: for _, iTx := range blk.Transactions { - if iTx.TxHash() == outPt.txHash { + if *btc.hashTx(iTx) == outPt.txHash { tx = iTx break out } @@ -3897,7 +3978,7 @@ func (btc *baseWallet) signTxAndAddChange(baseTx *wire.MsgTx, addr btcutil.Addre if err != nil { return makeErr("signing error: %v, raw tx: %x", err, btc.wireBytes(baseTx)) } - vSize := dexbtc.MsgTxVBytes(msgTx) + vSize := btc.calcTxSize(msgTx) minFee := feeRate * vSize remaining := totalIn - totalOut if minFee > remaining { @@ -3924,7 +4005,7 @@ func (btc *baseWallet) signTxAndAddChange(baseTx *wire.MsgTx, addr btcutil.Addre changeAdded := !btc.IsDust(changeOutput, feeRate) if changeAdded { // Add the change output. - vSize0 := dexbtc.MsgTxVBytes(baseTx) + vSize0 := btc.calcTxSize(baseTx) baseTx.AddTxOut(changeOutput) changeSize := dexbtc.MsgTxVBytes(baseTx) - vSize0 // may be dexbtc.P2WPKHOutputSize addrStr, _ := btc.stringAddr(addr, btc.chainParams) // just for logging @@ -3942,7 +4023,7 @@ func (btc *baseWallet) signTxAndAddChange(baseTx *wire.MsgTx, addr btcutil.Addre if err != nil { return makeErr("signing error: %v, raw tx: %x", err, btc.wireBytes(baseTx)) } - vSize = dexbtc.MsgTxVBytes(msgTx) // recompute the size with new tx signature + vSize = btc.calcTxSize(msgTx) // recompute the size with new tx signature reqFee := feeRate * vSize if reqFee > remaining { // I can't imagine a scenario where this condition would be true, but @@ -3979,16 +4060,17 @@ func (btc *baseWallet) signTxAndAddChange(baseTx *wire.MsgTx, addr btcutil.Addre changeOutput.Value, msgTx.TxHash()) } + txHash := btc.hashTx(msgTx) + fee := totalIn - totalOut actualFeeRate := fee / vSize - txHash := msgTx.TxHash() btc.log.Debugf("%d signature cycles to converge on fees for tx %s: "+ "min rate = %d, actual fee rate = %d (%v for %v bytes), change = %v", sigCycles, txHash, feeRate, actualFeeRate, fee, vSize, changeAdded) var change *output if changeAdded { - change = newOutput(&txHash, uint32(changeIdx), uint64(changeOutput.Value)) + change = newOutput(txHash, uint32(changeIdx), uint64(changeOutput.Value)) } return msgTx, change, fee, nil @@ -3999,36 +4081,53 @@ func (btc *baseWallet) broadcastTx(signedTx *wire.MsgTx) error { if err != nil { return fmt.Errorf("sendrawtx error: %v, raw tx: %x", err, btc.wireBytes(signedTx)) } - checkHash := signedTx.TxHash() - if *txHash != checkHash { + checkHash := btc.hashTx(signedTx) + if *txHash != *checkHash { return fmt.Errorf("transaction sent, but received unexpected transaction ID back from RPC server. "+ "expected %s, got %s. raw tx: %x", checkHash, *txHash, btc.wireBytes(signedTx)) } return nil } +// txOutFromTxBytes parses the specified *wire.TxOut from the serialized +// transaction. +func (btc *baseWallet) txOutFromTxBytes(txB []byte, vout uint32) (*wire.TxOut, error) { + msgTx, err := btc.deserializeTx(txB) + if err != nil { + return nil, fmt.Errorf("error decoding transaction bytes: %v", err) + } + + if len(msgTx.TxOut) <= int(vout) { + return nil, fmt.Errorf("no vout %d in tx %s", vout, btc.hashTx(msgTx)) + } + return msgTx.TxOut[vout], nil +} + // 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 } + privKey, err := btc.node.privKeyForAddress(addrStr) if err != nil { 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 } + return sig, privKey.PubKey().SerializeCompressed(), nil } // 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 @@ -4037,7 +4136,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 { @@ -4158,7 +4257,7 @@ func (btc *baseWallet) lockedSats() (uint64, error) { if err != nil { return 0, err } - txOut, err := txOutFromTxBytes(tx.Hex, rpcOP.Vout) + txOut, err := btc.txOutFromTxBytes(tx.Hex, rpcOP.Vout) if err != nil { return 0, err } @@ -4169,15 +4268,20 @@ 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 +// asset.Wallet interface. +func (btc *baseWallet) GetBestBlockHeight() (int32, error) { + return btc.node.getBestBlockHeight() } // Convert the BTC value to satoshi. @@ -4192,7 +4296,6 @@ type blockHeader struct { Confirmations int64 `json:"confirmations"` Height int64 `json:"height"` Time int64 `json:"time"` - MedianTime int64 `json:"mediantime"` PreviousBlockHash string `json:"previousblockhash"` } @@ -4288,7 +4391,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) } @@ -4297,6 +4402,12 @@ func rawTxInSig(tx *wire.MsgTx, idx int, pkScript []byte, hashType txscript.SigH func findRedemptionsInTx(ctx context.Context, segwit bool, reqs map[outPoint]*findRedemptionReq, msgTx *wire.MsgTx, chainParams *chaincfg.Params) (discovered map[outPoint]*findRedemptionResult) { + return findRedemptionsInTxWithHasher(ctx, segwit, reqs, msgTx, chainParams, hashTx) +} + +func findRedemptionsInTxWithHasher(ctx context.Context, segwit bool, reqs map[outPoint]*findRedemptionReq, msgTx *wire.MsgTx, + chainParams *chaincfg.Params, hashTx func(*wire.MsgTx) *chainhash.Hash) (discovered map[outPoint]*findRedemptionResult) { + discovered = make(map[outPoint]*findRedemptionResult, len(reqs)) for vin, txIn := range msgTx.TxIn { @@ -4310,7 +4421,7 @@ func findRedemptionsInTx(ctx context.Context, segwit bool, reqs map[outPoint]*fi } if outPt.txHash == poHash && outPt.vout == poVout { // Match! - txHash := msgTx.TxHash() + txHash := hashTx(msgTx) secret, err := dexbtc.FindKeyPush(txIn.Witness, txIn.SignatureScript, req.contractHash[:], segwit, chainParams) if err != nil { req.fail("no secret extracted from redemption input %s:%d for swap output %s: %v", @@ -4318,7 +4429,7 @@ func findRedemptionsInTx(ctx context.Context, segwit bool, reqs map[outPoint]*fi continue } discovered[outPt] = &findRedemptionResult{ - redemptionCoinID: toCoinID(&txHash, uint32(vin)), + redemptionCoinID: toCoinID(txHash, uint32(vin)), secret: secret, } } @@ -4357,6 +4468,15 @@ func float64PtrStr(v *float64) string { return strconv.FormatFloat(*v, 'f', 8, 64) } +func hashTx(tx *wire.MsgTx) *chainhash.Hash { + h := tx.TxHash() + return &h +} + +func stringifyAddress(addr btcutil.Address, _ *chaincfg.Params) (string, error) { + return addr.String(), nil +} + func deserializeBlock(b []byte) (*wire.MsgBlock, error) { msgBlock := &wire.MsgBlock{} return msgBlock, msgBlock.Deserialize(bytes.NewReader(b)) diff --git a/client/asset/btc/btc_test.go b/client/asset/btc/btc_test.go index 5fbc499441..848d14ff30 100644 --- a/client/asset/btc/btc_test.go +++ b/client/asset/btc/btc_test.go @@ -554,6 +554,16 @@ func makeTxHex(pkScripts []dex.Bytes, inputs []*wire.TxIn) ([]byte, error) { return txBuf.Bytes(), nil } +// msgTxFromHex creates a wire.MsgTx by deserializing the hex-encoded +// transaction. +func msgTxFromHex(txHex string) (*wire.MsgTx, error) { + b, err := hex.DecodeString(txHex) + if err != nil { + return nil, err + } + return msgTxFromBytes(b) +} + func makeRPCVin(txHash *chainhash.Hash, vout uint32, sigScript []byte, witness [][]byte) *wire.TxIn { var rpcWitness []string for _, b := range witness { diff --git a/client/asset/btc/livetest/livetest.go b/client/asset/btc/livetest/livetest.go index 6aea90ce9d..7955ecd146 100644 --- a/client/asset/btc/livetest/livetest.go +++ b/client/asset/btc/livetest/livetest.go @@ -37,23 +37,29 @@ var tLogger dex.Logger type WalletConstructor func(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) -func tBackend(ctx context.Context, t *testing.T, cfg *Config, node, name string, blkFunc func(string, error)) *connectedWallet { +func tBackend(ctx context.Context, t *testing.T, cfg *Config, walletName *WalletName, blkFunc func(string, error)) *connectedWallet { t.Helper() user, err := user.Current() if err != nil { t.Fatalf("error getting current user: %v", err) } - cfgPath := filepath.Join(user.HomeDir, "dextest", cfg.Asset.Symbol, node, node+".conf") + + fileName := walletName.Filename + if fileName == "" { + fileName = walletName.Node + ".conf" + } + + cfgPath := filepath.Join(user.HomeDir, "dextest", cfg.Asset.Symbol, walletName.Node, fileName) settings, err := config.Parse(cfgPath) if err != nil { t.Fatalf("error reading config options: %v", err) } - settings["walletname"] = name + settings["walletname"] = walletName.Name if cfg.SplitTx { settings["txsplit"] = "1" } - reportName := fmt.Sprintf("%s:%s-%s", cfg.Asset.Symbol, node, name) + reportName := fmt.Sprintf("%s:%s-%s", cfg.Asset.Symbol, walletName.Node, walletName.Name) walletCfg := &asset.WalletConfig{ Settings: settings, @@ -65,7 +71,7 @@ func tBackend(ctx context.Context, t *testing.T, cfg *Config, node, name string, }, } - w, err := cfg.NewWallet(walletCfg, tLogger.SubLogger(node+"."+name), dex.Regtest) + w, err := cfg.NewWallet(walletCfg, tLogger.SubLogger(walletName.Node+"."+walletName.Name), dex.Regtest) if err != nil { t.Fatalf("error creating backend: %v", err) } @@ -109,7 +115,11 @@ func (rig *testRig) close() { } func (rig *testRig) mineAlpha() error { - return exec.Command("tmux", "send-keys", "-t", rig.symbol+"-harness:2", "./mine-alpha 1", "C-m").Run() + tmuxWindow := rig.symbol + "-harness:2" + if rig.symbol == "zec" { + tmuxWindow = rig.symbol + "-harness:4" + } + return exec.Command("tmux", "send-keys", "-t", tmuxWindow, "./mine-alpha 1", "C-m").Run() } func randBytes(l int) []byte { @@ -121,6 +131,9 @@ func randBytes(l int) []byte { type WalletName struct { Node string Name string + // Filename is optional. If specified, it will be used instead of + // [node].conf. + Filename string } type Config struct { @@ -131,6 +144,7 @@ type Config struct { SPV bool FirstWallet *WalletName SecondWallet *WalletName + Unencrypted bool } func Run(t *testing.T, cfg *Config) { @@ -179,24 +193,25 @@ func Run(t *testing.T, cfg *Config) { } t.Log("Setting up alpha/beta/gamma wallet backends...") - rig.firstWallet = tBackend(tCtx, t, cfg, cfg.FirstWallet.Node, cfg.FirstWallet.Name, blkFunc) - // rig.backends["beta"], rig.connectionMasters["beta"] = tBackend(tCtx, t, cfg, "beta", "", tLogger.SubLogger("beta"), blkFunc) - rig.secondWallet = tBackend(tCtx, t, cfg, cfg.SecondWallet.Node, cfg.SecondWallet.Name, blkFunc) + rig.firstWallet = tBackend(tCtx, t, cfg, cfg.FirstWallet, blkFunc) + rig.secondWallet = tBackend(tCtx, t, cfg, cfg.SecondWallet, blkFunc) defer rig.close() // Unlock the wallet for use. - err := rig.firstWallet.Unlock(walletPassword) - if err != nil { - t.Fatalf("error unlocking gamma wallet: %v", err) - } + if !cfg.Unencrypted { + err := rig.firstWallet.Unlock(walletPassword) + if err != nil { + t.Fatalf("error unlocking gamma wallet: %v", err) + } - if cfg.SPV { - // // The test expects beta and gamma to be unlocked. - // if err := rig.beta().Unlock(walletPassword); err != nil { - // t.Fatalf("beta Unlock error: %v", err) - // } - if err := rig.secondWallet.Unlock(walletPassword); err != nil { - t.Fatalf("gamma Unlock error: %v", err) + if cfg.SPV { + // // The test expects beta and gamma to be unlocked. + // if err := rig.beta().Unlock(walletPassword); err != nil { + // t.Fatalf("beta Unlock error: %v", err) + // } + if err := rig.secondWallet.Unlock(walletPassword); err != nil { + t.Fatalf("gamma Unlock error: %v", err) + } } } @@ -365,12 +380,15 @@ func Run(t *testing.T, cfg *Config) { if strings.Contains(err.Error(), "error finding unspent contract") { return wait.TryAgain } + c <- nil t.Fatalf("error auditing contract: %v", err) } c <- ai return wait.DontTryAgain }, - ExpireFunc: func() { t.Fatalf("makeRedemption -> AuditContract timed out") }, + ExpireFunc: func() { + t.Fatalf("makeRedemption -> AuditContract timed out") + }, }) // Alpha should be able to redeem. @@ -440,7 +458,7 @@ func Run(t *testing.T, cfg *Config) { latencyQ.Wait(&wait.Waiter{ Expiration: time.Now().Add(time.Second * 10), TryFunc: func() wait.TryDirective { - ctx, cancel := context.WithTimeout(tCtx, time.Millisecond*5) + ctx, cancel := context.WithTimeout(tCtx, time.Second) defer cancel() _, _, err = rig.secondWallet.FindRedemption(ctx, swapReceipt.Coin().ID(), nil) if err != nil { diff --git a/client/asset/btc/rpcclient.go b/client/asset/btc/rpcclient.go index fba3c7f13f..22df2eb638 100644 --- a/client/asset/btc/rpcclient.go +++ b/client/asset/btc/rpcclient.go @@ -25,6 +25,7 @@ import ( const ( methodGetBalances = "getbalances" + methodGetBalance = "getbalance" methodListUnspent = "listunspent" methodLockUnspent = "lockunspent" methodListLockUnspent = "listlockunspent" @@ -75,8 +76,6 @@ type rpcCore struct { segwit bool decodeAddr dexbtc.AddressDecoder stringAddr dexbtc.AddressStringer - deserializeBlock func([]byte) (*wire.MsgBlock, error) - arglessChangeAddrRPC bool legacyRawSends bool minNetworkVersion uint64 log dex.Logger @@ -84,8 +83,14 @@ type rpcCore struct { omitAddressType bool legacySignTx bool booleanGetBlock bool - legacyValidateAddressRPC bool unlockSpends bool + deserializeTx func([]byte) (*wire.MsgTx, error) + serializeTx func(*wire.MsgTx) ([]byte, error) + deserializeBlock func([]byte) (*wire.MsgBlock, error) + hashTx func(*wire.MsgTx) *chainhash.Hash + numericGetRawTxRPC bool + legacyValidateAddressRPC bool + manualMedianTime bool } // rpcClient is a bitcoind JSON RPC client that uses rpcclient.Client's @@ -112,6 +117,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) } @@ -141,18 +148,17 @@ func (wc *rpcClient) estimateSmartFee(confTarget int64, mode *btcjson.EstimateSm // SendRawTransactionLegacy broadcasts the transaction with an additional legacy // boolean `allowhighfees` argument set to false. func (wc *rpcClient) SendRawTransactionLegacy(tx *wire.MsgTx) (*chainhash.Hash, error) { - txBytes, err := serializeMsgTx(tx) + txBytes, err := wc.serializeTx(tx) if err != nil { return nil, err } - return wc.callHashGetter(methodSendRawTransaction, anylist{ hex.EncodeToString(txBytes), false}) } // SendRawTransaction broadcasts the transaction. func (wc *rpcClient) SendRawTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { - b, err := serializeMsgTx(tx) + b, err := wc.serializeTx(tx) if err != nil { return nil, err } @@ -282,6 +288,35 @@ func (wc *rpcClient) getBestBlockHeight() (int32, error) { return int32(header.Height), nil } +// getChainStamp satisfies chainStamper for manual median time calculations. +func (wc *rpcClient) getChainStamp(blockHash *chainhash.Hash) (stamp time.Time, prevHash *chainhash.Hash, err error) { + hdr, err := wc.getBlockHeader(blockHash) + if err != nil { + return + } + prevHash, err = chainhash.NewHashFromStr(hdr.PreviousBlockHash) + if err != nil { + return + } + return time.Unix(hdr.Time, 0).UTC(), prevHash, nil +} + +// medianTime is the median time for the current best block. +func (wc *rpcClient) medianTime() (stamp time.Time, err error) { + tipHash, err := wc.getBestBlockHash() + if err != nil { + return + } + if wc.manualMedianTime { + return calcMedianTime(wc, tipHash) + } + hdr, err := wc.getRPCBlockHeader(tipHash) + if err != nil { + return + } + return time.Unix(hdr.MedianTime, 0).UTC(), nil +} + // GetRawMempool returns the hashes of all transactions in the memory pool. func (wc *rpcClient) GetRawMempool() ([]*chainhash.Hash, error) { var mempool []string @@ -305,12 +340,16 @@ func (wc *rpcClient) GetRawMempool() ([]*chainhash.Hash, error) { // GetRawTransaction retrieves the MsgTx. func (wc *rpcClient) GetRawTransaction(txHash *chainhash.Hash) (*wire.MsgTx, error) { var txB dex.Bytes - err := wc.call(methodGetRawTransaction, anylist{txHash.String(), false}, &txB) + args := anylist{txHash.String(), false} + if wc.numericGetRawTxRPC { + args[1] = 0 + } + err := wc.call(methodGetRawTransaction, args, &txB) if err != nil { return nil, err } - tx := wire.NewMsgTx(wire.TxVersion) - return tx, tx.Deserialize(bytes.NewReader(txB)) + + return wc.deserializeTx(txB) } // balances retrieves a wallet's balance details. @@ -359,7 +398,7 @@ func (wc *rpcClient) changeAddress() (btcutil.Address, error) { var addrStr string var err error switch { - case wc.arglessChangeAddrRPC: + case wc.omitAddressType: err = wc.call(methodChangeAddress, nil, &addrStr) case wc.segwit: err = wc.call(methodChangeAddress, anylist{"bech32"}, &addrStr) @@ -401,7 +440,7 @@ func (wc *rpcClient) address(aType string) (btcutil.Address, error) { // signTx attempts to have the wallet sign the transaction inputs. func (wc *rpcClient) signTx(inTx *wire.MsgTx) (*wire.MsgTx, error) { - txBytes, err := serializeMsgTx(inTx) + txBytes, err := wc.serializeTx(inTx) if err != nil { return nil, fmt.Errorf("tx serialization error: %w", err) } @@ -410,6 +449,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) @@ -423,7 +463,7 @@ func (wc *rpcClient) signTx(inTx *wire.MsgTx) (*wire.MsgTx, error) { } return nil, fmt.Errorf("signing incomplete. %d signing errors encountered: %s", len(res.Errors), errMsg) } - outTx, err := msgTxFromBytes(res.Hex) + outTx, err := wc.deserializeTx(res.Hex) if err != nil { return nil, fmt.Errorf("error deserializing transaction response: %w", err) } @@ -495,7 +535,7 @@ func (wc *rpcClient) GetWalletInfo() (*GetWalletInfoResult, error) { } // GetAddressInfo gets information about the given address by calling -// getaddressinfo or validateaddress RPC command. +// getaddressinfo RPC command. func (wc *rpcClient) getAddressInfo(addr btcutil.Address, method string) (*GetAddressInfoResult, error) { ai := new(GetAddressInfoResult) addrStr, err := wc.stringAddr(addr, wc.chainParams) @@ -534,7 +574,7 @@ func (wc *rpcClient) syncStatus() (*syncStatus, error) { return &syncStatus{ Target: int32(chainInfo.Headers), Height: int32(chainInfo.Blocks), - Syncing: chainInfo.InitialBlockDownload || chainInfo.Headers-chainInfo.Blocks > 1, + Syncing: chainInfo.syncing(), }, nil } @@ -558,17 +598,33 @@ func (wc *rpcClient) swapConfirmations(txHash *chainhash.Hash, vout uint32, _ [] return uint32(tx.Confirmations), true, nil } -// getBlockHeader gets the block header for the specified block hash. -func (wc *rpcClient) getBlockHeader(blockHash *chainhash.Hash) (*blockHeader, error) { - blkHeader := new(blockHeader) +// rpcBlockHeader adds a MedianTime field to blockHeader. +type rpcBlockHeader struct { + blockHeader + MedianTime int64 `json:"mediantime"` +} + +// getBlockHeader gets the *rpcBlockHeader for the specified block hash. +func (wc *rpcClient) getRPCBlockHeader(blockHash *chainhash.Hash) (*rpcBlockHeader, error) { + blkHeader := new(rpcBlockHeader) err := wc.call(methodGetBlockHeader, anylist{blockHash.String(), true}, blkHeader) if err != nil { return nil, err } + return blkHeader, nil } +// getBlockHeader gets the *blockHeader for the specified block hash. +func (wc *rpcClient) getBlockHeader(blockHash *chainhash.Hash) (*blockHeader, error) { + hdr, err := wc.getRPCBlockHeader(blockHash) + if err != nil { + return nil, err + } + return &hdr.blockHeader, nil +} + // getBlockHeight gets the mainchain height for the specified block. func (wc *rpcClient) getBlockHeight(blockHash *chainhash.Hash) (int32, error) { hdr, err := wc.getBlockHeader(blockHash) @@ -651,7 +707,7 @@ func (wc *rpcClient) findRedemptionsInMempool(ctx context.Context, reqs map[outP logAbandon(fmt.Sprintf("getrawtransaction error for tx hash %v: %v", txHash, err)) return } - newlyDiscovered := findRedemptionsInTx(ctx, wc.segwit, reqs, tx, wc.chainParams) + newlyDiscovered := findRedemptionsInTxWithHasher(ctx, wc.segwit, reqs, tx, wc.chainParams, wc.hashTx) for outPt, res := range newlyDiscovered { discovered[outPt] = res } @@ -672,7 +728,7 @@ func (wc *rpcClient) searchBlockForRedemptions(ctx context.Context, reqs map[out discovered = make(map[outPoint]*findRedemptionResult, len(reqs)) for _, msgTx := range msgBlock.Transactions { - newlyDiscovered := findRedemptionsInTx(ctx, wc.segwit, reqs, msgTx, wc.chainParams) + newlyDiscovered := findRedemptionsInTxWithHasher(ctx, wc.segwit, reqs, msgTx, wc.chainParams, wc.hashTx) for outPt, res := range newlyDiscovered { discovered[outPt] = res } @@ -713,35 +769,11 @@ func serializeMsgTx(msgTx *wire.MsgTx) ([]byte, error) { return buf.Bytes(), nil } -// msgTxFromHex creates a wire.MsgTx by deserializing the hex-encoded -// transaction. -func msgTxFromHex(txHex string) (*wire.MsgTx, error) { - b, err := hex.DecodeString(txHex) - if err != nil { - return nil, err - } - return msgTxFromBytes(b) -} - // msgTxFromBytes creates a wire.MsgTx by deserializing the transaction. func msgTxFromBytes(txB []byte) (*wire.MsgTx, error) { - msgTx := wire.NewMsgTx(wire.TxVersion) + msgTx := new(wire.MsgTx) if err := msgTx.Deserialize(bytes.NewReader(txB)); err != nil { return nil, err } return msgTx, nil } - -// txOutFromTxBytes parses the specified *wire.TxOut from the serialized -// transaction. -func txOutFromTxBytes(txB []byte, vout uint32) (*wire.TxOut, error) { - msgTx, err := msgTxFromBytes(txB) - if err != nil { - return nil, fmt.Errorf("error decoding transaction bytes: %v", err) - } - - if len(msgTx.TxOut) <= int(vout) { - return nil, fmt.Errorf("no vout %d in tx %s", vout, msgTx.TxHash()) - } - return msgTx.TxOut[vout], nil -} diff --git a/client/asset/btc/spv.go b/client/asset/btc/spv.go index c3ce1c0c6e..6bb5953378 100644 --- a/client/asset/btc/spv.go +++ b/client/asset/btc/spv.go @@ -521,6 +521,21 @@ func (w *spvWallet) getBestBlockHeight() (int32, error) { return w.wallet.syncedTo().Height, nil } +// getChainStamp satisfies chainStamper for manual median time calculations. +func (w *spvWallet) getChainStamp(blockHash *chainhash.Hash) (stamp time.Time, prevHash *chainhash.Hash, err error) { + hdr, err := w.cl.GetBlockHeader(blockHash) + if err != nil { + return + } + return hdr.Timestamp, &hdr.PrevBlock, nil +} + +// medianTime is the median time for the current best block. +func (w *spvWallet) medianTime() (time.Time, error) { + blk := w.wallet.syncedTo() + return calcMedianTime(w, &blk.Hash) +} + // getChainHeight is only for confirmations since it does not reflect the wallet // manager's sync height, just the chain service. func (w *spvWallet) getChainHeight() (int32, error) { @@ -1015,11 +1030,6 @@ func (w *spvWallet) getBlockHeader(blockHash *chainhash.Hash) (*blockHeader, err return nil, err } - medianTime, err := w.calcMedianTime(blockHash) - if err != nil { - return nil, err - } - tip, err := w.cl.BestBlock() if err != nil { return nil, fmt.Errorf("BestBlock error: %v", err) @@ -1035,43 +1045,9 @@ func (w *spvWallet) getBlockHeader(blockHash *chainhash.Hash) (*blockHeader, err Confirmations: int64(confirms(blockHeight, tip.Height)), Height: int64(blockHeight), Time: hdr.Timestamp.Unix(), - MedianTime: medianTime.Unix(), }, nil } -const medianTimeBlocks = 11 - -// calcMedianTime calculates the median time of the previous 11 block headers. -// The median time is used for validating time-locked transactions. See notes in -// btcd/blockchain (*blockNode).CalcPastMedianTime() regarding incorrectly -// calculated median time for blocks 1, 3, 5, 7, and 9. -func (w *spvWallet) calcMedianTime(blockHash *chainhash.Hash) (time.Time, error) { - timestamps := make([]int64, 0, medianTimeBlocks) - - zeroHash := chainhash.Hash{} - - h := blockHash - for i := 0; i < medianTimeBlocks; i++ { - hdr, err := w.cl.GetBlockHeader(h) - if err != nil { - return time.Time{}, fmt.Errorf("BlockHeader error for hash %q: %v", h, err) - } - timestamps = append(timestamps, hdr.Timestamp.Unix()) - - if hdr.PrevBlock == zeroHash { - break - } - h = &hdr.PrevBlock - } - - sort.Slice(timestamps, func(i, j int) bool { - return timestamps[i] < timestamps[j] - }) - - medianTimestamp := timestamps[len(timestamps)/2] - return time.Unix(medianTimestamp, 0), nil -} - func (w *spvWallet) logFilePath() string { return filepath.Join(w.netDir, logDirName, logFileName) } diff --git a/client/asset/btc/wallet.go b/client/asset/btc/wallet.go index 1b604f76f8..98bd356345 100644 --- a/client/asset/btc/wallet.go +++ b/client/asset/btc/wallet.go @@ -2,6 +2,8 @@ package btc import ( "context" + "fmt" + "sort" "sync" "time" @@ -22,6 +24,7 @@ type Wallet interface { getBlockHeight(*chainhash.Hash) (int32, error) getBestBlockHash() (*chainhash.Hash, error) getBestBlockHeight() (int32, error) + medianTime() (time.Time, error) balances() (*GetBalancesResult, error) listUnspent() ([]*ListUnspentResult, error) lockUnspent(unlock bool, ops []*output) error @@ -49,3 +52,43 @@ type Wallet interface { type tipNotifier interface { tipFeed() <-chan *block } + +// chainStamper is a source of the timestamp and the previous block hash for a +// specified block. A chainStamper is used to manually calculate the median time +// for a block. +type chainStamper interface { + getChainStamp(*chainhash.Hash) (stamp time.Time, prevHash *chainhash.Hash, err error) +} + +const medianTimeBlocks = 11 + +// calcMedianTime calculates the median time of the previous 11 block headers. +// The median time is used for validating time-locked transactions. See notes in +// btcd/blockchain (*blockNode).CalcPastMedianTime() regarding incorrectly +// calculated median time for blocks 1, 3, 5, 7, and 9. +func calcMedianTime(stamper chainStamper, blockHash *chainhash.Hash) (time.Time, error) { + timestamps := make([]int64, 0, medianTimeBlocks) + + zeroHash := chainhash.Hash{} + + h := blockHash + for i := 0; i < medianTimeBlocks; i++ { + stamp, prevHash, err := stamper.getChainStamp(h) + if err != nil { + return time.Time{}, fmt.Errorf("BlockHeader error for hash %q: %v", h, err) + } + timestamps = append(timestamps, stamp.Unix()) + + if *prevHash == zeroHash { + break + } + h = prevHash + } + + sort.Slice(timestamps, func(i, j int) bool { + return timestamps[i] < timestamps[j] + }) + + medianTimestamp := timestamps[len(timestamps)/2] + return time.Unix(medianTimestamp, 0), nil +} diff --git a/client/asset/interface.go b/client/asset/interface.go index cb497dab27..6d46c2d41f 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -124,6 +124,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. diff --git a/client/asset/zec/regnet_test.go b/client/asset/zec/regnet_test.go new file mode 100644 index 0000000000..090f3bc4fc --- /dev/null +++ b/client/asset/zec/regnet_test.go @@ -0,0 +1,159 @@ +//go:build harness + +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 ( + tLotSize uint64 = 1e6 + tZEC = &dex.Asset{ + ID: BipID, + Symbol: "zec", + SwapSize: dexzec.InitTxSize, + SwapSizeBase: dexzec.InitTxSizeBase, + MaxFeeRate: 100, + SwapConf: 1, + } +) + +func TestWallet(t *testing.T) { + livetest.Run(t, &livetest.Config{ + NewWallet: NewWallet, + LotSize: tLotSize, + Asset: tZEC, + FirstWallet: &livetest.WalletName{ + Node: "alpha", + Filename: "alpha.conf", + }, + SecondWallet: &livetest.WalletName{ + Node: "beta", + 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) +} diff --git a/client/asset/zec/zec.go b/client/asset/zec/zec.go new file mode 100644 index 0000000000..7a4ca5c517 --- /dev/null +++ b/client/asset/zec/zec.go @@ -0,0 +1,260 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package zec + +import ( + "encoding/json" + "fmt" + "math" + "strconv" + + "decred.org/dcrdex/client/asset" + "decred.org/dcrdex/client/asset/btc" + "decred.org/dcrdex/dex" + dexbtc "decred.org/dcrdex/dex/networks/btc" + dexzec "decred.org/dcrdex/dex/networks/zec" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" +) + +const ( + version = 0 + BipID = 133 + // The default fee is passed to the user as part of the asset.WalletInfo + // structure. + defaultFee = 10 + defaultFeeRateLimit = 1000 + minNetworkVersion = 5000025 + walletTypeRPC = "zcashdRPC" + + mainnetNU5ActivationHeight = 1687104 + testnetNU5ActivationHeight = 1842420 + testnetSaplingActivationHeight = 280000 + testnetOverwinterActivationHeight = 207500 +) + +var ( + fallbackFeeKey = "fallbackfee" + configOpts = []*asset.ConfigOption{ + { + Key: "rpcuser", + DisplayName: "JSON-RPC Username", + Description: "ZCash's 'rpcuser' setting", + }, + { + Key: "rpcpassword", + DisplayName: "JSON-RPC Password", + Description: "ZCash's 'rpcpassword' setting", + NoEcho: true, + }, + { + Key: "rpcbind", + DisplayName: "JSON-RPC Address", + Description: " or : (default 'localhost')", + }, + { + Key: "rpcport", + DisplayName: "JSON-RPC Port", + Description: "Port for RPC connections (if not set in Address)", + }, + { + Key: fallbackFeeKey, + DisplayName: "Fallback fee rate", + Description: "ZCash's 'fallbackfee' rate. Units: ZEC/kB", + DefaultValue: defaultFee * 1000 / 1e8, + }, + { + Key: "feeratelimit", + DisplayName: "Highest acceptable fee rate", + Description: "This is the highest network fee rate you are willing to " + + "pay on swap transactions. If feeratelimit is lower than a market's " + + "maxfeerate, you will not be able to trade on that market with this " + + "wallet. Units: BTC/kB", + DefaultValue: defaultFeeRateLimit * 1000 / 1e8, + }, + { + Key: "txsplit", + DisplayName: "Pre-split funding inputs", + Description: "When placing an order, create a \"split\" transaction to fund the order without locking more of the wallet balance than " + + "necessary. Otherwise, excess funds may be reserved to fund the order until the first swap contract is broadcast " + + "during match settlement, or the order is canceled. This an extra transaction for which network mining fees are paid. " + + "Used only for standing-type orders, e.g. limit orders without immediate time-in-force.", + IsBoolean: true, + }, + } + // WalletInfo defines some general information about a ZCash wallet. + WalletInfo = &asset.WalletInfo{ + Name: "ZCash", + Version: version, + UnitInfo: dexzec.UnitInfo, + AvailableWallets: []*asset.WalletDefinition{{ + Type: walletTypeRPC, + Tab: "External", + Description: "Connect to zcashcoind", + DefaultConfigPath: dexbtc.SystemConfigPath("zcash"), + ConfigOpts: configOpts, + NoAuth: true, + }}, + } +) + +func init() { + asset.Register(BipID, &Driver{}) +} + +// Driver implements asset.Driver. +type Driver struct{} + +// Open creates the ZEC exchange wallet. Start the wallet with its Run method. +func (d *Driver) Open(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) { + return NewWallet(cfg, logger, network) +} + +// DecodeCoinID creates a human-readable representation of a coin ID for +// ZCash. +func (d *Driver) DecodeCoinID(coinID []byte) (string, error) { + // ZCash and Bitcoin have the same tx hash and output format. + return (&btc.Driver{}).DecodeCoinID(coinID) +} + +// Info returns basic information about the wallet and asset. +func (d *Driver) Info() *asset.WalletInfo { + return WalletInfo +} + +// NewWallet is the exported constructor by which the DEX will import the +// exchange wallet. The wallet will shut down when the provided context is +// canceled. The configPath can be an empty string, in which case the standard +// system location of the zcashd config file is assumed. +func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, net dex.Network) (asset.Wallet, error) { + var btcParams *chaincfg.Params + var addrParams *dexzec.AddressParams + switch net { + case dex.Mainnet: + btcParams = dexzec.MainNetParams + addrParams = dexzec.MainNetAddressParams + case dex.Testnet: + btcParams = dexzec.TestNet4Params + addrParams = dexzec.TestNet4AddressParams + case dex.Regtest: + btcParams = dexzec.RegressionNetParams + addrParams = dexzec.RegressionNetAddressParams + default: + return nil, fmt.Errorf("unknown network ID %v", net) + } + + // Designate the clone ports. These will be overwritten by any explicit + // settings in the configuration file. + ports := dexbtc.NetPorts{ + Mainnet: "8232", + Testnet: "18232", + Simnet: "18232", + } + var w *btc.ExchangeWalletFullNode + cloneCFG := &btc.BTCCloneCFG{ + WalletCFG: cfg, + MinNetworkVersion: minNetworkVersion, + WalletInfo: WalletInfo, + Symbol: "zec", + Logger: logger, + Network: net, + ChainParams: btcParams, + Ports: ports, + DefaultFallbackFee: defaultFee, + DefaultFeeRateLimit: defaultFeeRateLimit, + LegacyRawFeeLimit: true, + ZECStyleBalance: true, + Segwit: false, + OmitAddressType: true, + LegacySignTxRPC: true, + NumericGetRawRPC: true, + LegacyValidateAddressRPC: true, + SingularWallet: true, + FeeEstimator: estimateFee, + AddressDecoder: func(addr string, net *chaincfg.Params) (btcutil.Address, error) { + return dexzec.DecodeAddress(addr, addrParams, btcParams) + }, + AddressStringer: func(addr btcutil.Address, btcParams *chaincfg.Params) (string, error) { + return dexzec.EncodeAddress(addr, addrParams) + }, + TxSizeCalculator: dexzec.CalcTxSize, + NonSegwitSigner: signTx, + TxDeserializer: func(b []byte) (*wire.MsgTx, error) { + zecTx, err := dexzec.DeserializeTx(b) + if err != nil { + return nil, err + } + return zecTx.MsgTx, nil + }, + BlockDeserializer: func(b []byte) (*wire.MsgBlock, error) { + zecBlock, err := dexzec.DeserializeBlock(b) + if err != nil { + return nil, err + } + return &zecBlock.MsgBlock, nil + }, + TxSerializer: func(btcTx *wire.MsgTx) ([]byte, error) { + return zecTx(btcTx).Bytes() + }, + TxHasher: func(tx *wire.MsgTx) *chainhash.Hash { + h := zecTx(tx).TxHash() + return &h + }, + TxVersion: func() int32 { + return dexzec.VersionNU5 + }, + // https://github.com/zcash/zcash/pull/6005 + ManualMedianTime: true, + } + + var err error + w, err = btc.BTCCloneWallet(cloneCFG) + return w, err +} + +func zecTx(tx *wire.MsgTx) *dexzec.Tx { + return dexzec.NewTxFromMsgTx(tx, dexzec.MaxExpiryHeight) +} + +// estimateFee uses ZCash's estimatefee RPC, since estimatesmartfee +// is not implemented. +// ZCash's fee estimation is pretty crappy. Full nodes can take hours to +// get up to speed, and forget about simnet. +// See https://github.com/zcash/zcash/issues/2552 +func estimateFee(node btc.RawRequester, confTarget uint64) (uint64, error) { + const feeConfs = 10 + resp, err := node.RawRequest("estimatefee", []json.RawMessage{[]byte(strconv.Itoa(feeConfs))}) + if err != nil { + return 0, err + } + var feeRate float64 + err = json.Unmarshal(resp, &feeRate) + if err != nil { + return 0, err + } + if feeRate <= 0 { + return 0, fmt.Errorf("fee could not be estimated") + } + return uint64(math.Round(feeRate * 1e5)), nil +} + +// signTx signs the transaction input with ZCash's BLAKE-2B sighash digest. +// Won't work with shielded or blended transactions. +func signTx(btcTx *wire.MsgTx, idx int, pkScript []byte, hashType txscript.SigHashType, + key *btcec.PrivateKey, amts []int64, prevScripts [][]byte) ([]byte, error) { + + tx := zecTx(btcTx) + + sigHash, err := tx.SignatureDigest(idx, hashType, amts, prevScripts) + if err != nil { + return nil, fmt.Errorf("sighash calculation error: %v", err) + } + + return append(ecdsa.Sign(key, sigHash[:]).Serialize(), byte(hashType)), nil +} diff --git a/client/cmd/dexc/main.go b/client/cmd/dexc/main.go index c118208b10..7519747614 100644 --- a/client/cmd/dexc/main.go +++ b/client/cmd/dexc/main.go @@ -22,6 +22,7 @@ import ( _ "decred.org/dcrdex/client/asset/dcr" // register dcr asset _ "decred.org/dcrdex/client/asset/doge" // register doge asset _ "decred.org/dcrdex/client/asset/ltc" // register ltc asset + _ "decred.org/dcrdex/client/asset/zec" // register zec asset "decred.org/dcrdex/client/core" "decred.org/dcrdex/client/rpcserver" diff --git a/client/cmd/simnet-trade-tests/run b/client/cmd/simnet-trade-tests/run index 45f3ed35f3..581e958339 100755 --- a/client/cmd/simnet-trade-tests/run +++ b/client/cmd/simnet-trade-tests/run @@ -14,7 +14,7 @@ case $1 in ;; bchdcr) - ./simnet-trade-tests --base bch --quote dcr --quote1node trading1 --quote2node trading2 \ + ./simnet-trade-tests --base bch --quote dcr --quote1node trading1 --quote2node trading2 \ --regasset dcr ${@:2} ;; @@ -31,13 +31,17 @@ case $1 in ./simnet-trade-tests --base1node trading1 --base2node trading2 --quote eth ${@:2} ;; + zecbtc) + ./simnet-trade-tests --base zec --quote btc --regasset btc ${@:2} + ;; + help|--help|-h) ./simnet-trade-tests --help cat < 1 wallet, so gamma and delta // have their own nodes. default: @@ -1489,6 +1498,10 @@ func dogeWallet(node string) (*tWallet, error) { return btcCloneWallet(doge.BipID, false, node, "dogecoindRPC") } +func zecWallet(node string) (*tWallet, error) { + return btcCloneWallet(zec.BipID, false, node, "zcashdRPC") +} + func (s *simulationTest) newClient(name string, cl *SimClient) (*simulationClient, error) { wallets := make(map[uint32]*tWallet, 2) addWallet := func(assetID uint32, useSPV bool, node string) error { @@ -1509,6 +1522,8 @@ func (s *simulationTest) newClient(name string, cl *SimClient) (*simulationClien tw, err = bchWallet(useSPV, node) case doge.BipID: tw, err = dogeWallet(node) + case zec.BipID: + tw, err = zecWallet(node) default: return fmt.Errorf("no method to create wallet for asset %d", assetID) } @@ -1736,8 +1751,8 @@ func (s *simulationTest) placeOrder(client *simulationClient, qty, rate uint64, r := calc.ConventionalRateAlt(rate, s.base.conversionFactor, s.quote.conversionFactor) - client.log.Infof("placed order %sing %s %s at %s %s (%s)", sellString(client.isSeller), - s.base.valFmt(qty), s.base.symbol, r, s.quote.symbol, ord.ID[:8]) + client.log.Infof("placed order %sing %s %s at %f %s/%s (%s)", sellString(client.isSeller), + s.base.valFmt(qty), s.base.symbol, r, s.quote.symbol, s.base.symbol, ord.ID[:8]) return ord.ID.String(), nil } diff --git a/client/webserver/site/src/img/coins/zec.png b/client/webserver/site/src/img/coins/zec.png new file mode 100644 index 0000000000000000000000000000000000000000..34b150f5e735c75f7cb82de45d6b40c8a0985b1d GIT binary patch literal 4061 zcmV<34TS-JgRCwC$oq3oP#TmwbHM`J&3lakw4uKdkq6UaSjEE#CgC^hw z!6b^wsxdK&7dZ?bh$0Z9l7JdCXhe)x5O0Y%1PsIr69MB5qR4&7>B7#!PUVmKW@*OV zsp{F;ncmrbpQoSgnXT@r?zfKbs;{6Cp@OO>wTz%S4EQSWHQ-?25TG^C0w|V}dw?=v z8?Zr+<-iK1)Tdf&-AG7gXa{@;I05Jb^Z=UG@}4_^rNAQKW#C`HXN`TbX7mDXmY1e; za8v@X0oMWD8ejjuo^TEDevU{;cf1K)2^`cAiRc470#t;6h}2^jFbz1WArNsq5DOs* z)g1|7Ch(0yB;p9*$$COWb#u*hphF=L(Hs~Ll-E--{Krn<*Fcl{D)#mR?gtLf()|*^ z3OPOkHp#J5^bdfhKr5iF=qiWHacCBwbumy1yj5R%MKLfzx_WI|WEn6U_^puJq84>J z5I9Lh+9uW zU1?f!JVsOC`*P3k0d2C}i`N3r0keRAdQmFnO%$OS23!vu6Y!o}fwROMlXpTc0;U2* z0j~KF_>FY=RIE9R8q0QWgp6v~_v@^j$s z0QY!Z#_A7*h|a)7nOMVOb>0iy2VCz(*(Bff1|9^C3_!|o;OSfvG8}l+u|;d(4IH<|p8 zttUFwJfPHzvdTAK-sLFeZIyTReq9oBDDWTAf?X!e!6I@Jk5MWQ)1tO-Y1>Y?^NI9j zt~n1P*_n~H06+DjJSU$ANCP|2;o9XG%l5XqAfzdT&`=Y7PBQuBdJWvD0%yKKn_nr>3fFA@I zvQBm&V=0O|<@~x#$S5G31o{_pJ{DMQDL<4Nq9to066XP-3 zVqyWVbx<@%xe{^&#^0O{F%kDrri(Uhke@jyfn!Cg z>IaEwgr^}yQB+-_6ub<);*+Zzu@A+{1doGa7RIiZ4i-W* z1F%~#r%3NO7GvWo7-T8R>fCsYj%X?|?gpFo-YwmbHOz8@O^CCJ`ftJA50{<}M|H`{ zt(pKyiR%GOP+mF=tCkRn!n4f>4+DF|l6!WckkRXK7=0cT7iI01=Sx7}7L1WK9fs^T z=2eYW34Jv72=AFbIl4tQYTm6gygNO6FL!$x480cC1&Ae_d^xmjomGxIh%WacCiY-x znzDT`VNaitqT-)q`H>AOqF&Jn4JODTD`<6qIkEw*;ZJ`LM#j`Dp-AonA*p-x z4;vZf6>!6Suy(8aHGlmbw5$7o)&42(u14`!DI;_z2|3!Lo$pXeEtm6eg^3Ie+&>#0 zn&*Da?Z1S6J#tdLx7VGI1?R`>GlTEqB@pn2|Qz^?IFmHxjx z2qA~s=1lq9IV@yU?1Aw!;Da^p*G&5@bpA?CtJz$mJ2(<@h@TLf<1*=R=@J$~9-RmG zJnvrqrcyY$ckb({%(6RxjX?;p#i=Hfw6qEX8B3PKMYp+^AJhvjISV5BS@e)x;K+p>V{|JWkrCuRP$X-X5cjv33 z?|uU3PH->Z?`RnH!>lez$hc$sdw{K6#PLO!IF5sLg6(B6>P}_bxDc1iDFaXvnDWDb=>L z9UJ|G*bETe0GbVA9d0XzYCrJqhO4K-j&k>Frv3`rv<_9K)sL!DcYtjW{m4&IAg$a|!>*B_)4x{I7Is_UnizZkB$ z&%ONlr@)9)2qTj3>R5};SB_#u>ZHEba`R*~{KC3?f>j&gqFda{cWnn_O2KOqNC78ql>F})oL#inDxaHzQ(N(Lf?#M>XmoA5UXBQM13E>;5)#MkT40m5 zl8IEt(TM$w44AAI=t_Mm_C)?OQopE9dz@yui2}w*FL@qV(8xm-9`&LuQNJOi!N``}ZU{aUc6|9n?^FQNh_{4DsY+B5pWVeJExu?y(! zMR^Ysw7wX7oLgT^E*W=1R75GA(68fLOi}}$i4;f9OzchQc#P|EA$jE?31&K+??tP! z#l4JuxyI9&1ZX%9Spbqn^D5H&NdQhy>`ja`FG7t^Rdfm*cM2JZxN9RLjwLUQtqr}5 zNwaOktw7(V1GUEF@L1FU$atQ8i9v_rmVjrHD1#XjlANS$Q7InAy4l%_vI~=m^t}Qm zVc)~%6iPA$3hyuUI0#QHi#XVShcyBZJunn1^d$IK!kwF zPU@0azL!+At(fTt6v#w>H*mTar3|xh=_TxuB*}SrjO&jCjbS0(z_ z%vDNjeJv)>R;@FNY~2YHEdBMCi?f!DzZ|&2i}Dxw+#R!foXbX&>%_#gwyp>Plc44) z$KJI;B#>7CM0uP+h}J4v@HQ~Qi;|2i9s)ds+e)(CGCBluQDoAFo<)2jjuZVn;Ic?@ zM0rsbU`*0?5pIKIh3FN%y(mj1vD=N9{iR&Us1)%(u)nA_VV7eGmQn5*%r;3Iq__0K z7}&y=AO2YlziAdVOTk5wnvQ!03ysN_H7%1LuWcOc?}4iWl>ZE4uCH>;#A6J`_zsTC zGZAxvv0hYM4`v&^kSfLlV0&c;}qPsDx8I#CftNwb5`)z*x3o)3=DC_JOp>HS%2< z`)_bxf=Mdb0h3hcNX$k7*-8uYDR7}kW%Ukiq(vsO&|~s_>;WccFh_YCJT-oSYA=BbE!$92=- zhD%7z;&V4lWMD_pFWQK1(cEY6+JkWnZO5e4TP=FShnUdV?Tv#`BLd<7+??g{*}7TH P00000NkvXXu0mjfsjZoU literal 0 HcmV?d00001 diff --git a/client/webserver/site/src/js/doc.ts b/client/webserver/site/src/js/doc.ts index a934d91eeb..20f38207fe 100644 --- a/client/webserver/site/src/js/doc.ts +++ b/client/webserver/site/src/js/doc.ts @@ -18,7 +18,8 @@ const BipIDs = { 28: 'vtc', 3: 'doge', 145: 'bch', - 60: 'eth' + 60: 'eth', + 133: 'zec' } const BipSymbols = Object.values(BipIDs) diff --git a/client/webserver/site/src/js/forms.ts b/client/webserver/site/src/js/forms.ts index 1cbef680ed..dc3c023ced 100644 --- a/client/webserver/site/src/js/forms.ts +++ b/client/webserver/site/src/js/forms.ts @@ -166,7 +166,8 @@ export class NewWalletForm { page.newWalletPass.value = '' page.submitAdd.textContent = intl.prep(intl.ID_CREATE) } else { - Doc.show(page.auth, page.newWalletPassBox) + Doc.show(page.auth) + if (!walletDef.noauth) Doc.show(page.newWalletPassBox) page.submitAdd.textContent = intl.prep(intl.ID_ADD) } diff --git a/client/webserver/site/src/js/registry.ts b/client/webserver/site/src/js/registry.ts index f7dcf89b24..491ef39414 100644 --- a/client/webserver/site/src/js/registry.ts +++ b/client/webserver/site/src/js/registry.ts @@ -191,6 +191,7 @@ export interface WalletDefinition { description: string configpath: string configopts: ConfigOption[] + noauth: boolean } export interface ConfigOption { @@ -205,6 +206,7 @@ export interface ConfigOption { isdate: boolean disablewhenactive: boolean isBirthdayConfig: boolean + noauth: boolean } export interface Coin { diff --git a/dex/networks/zec/addr.go b/dex/networks/zec/addr.go new file mode 100644 index 0000000000..0d2f0ad9c3 --- /dev/null +++ b/dex/networks/zec/addr.go @@ -0,0 +1,97 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org + +package zec + +import ( + "crypto/sha256" + "fmt" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/decred/base58" +) + +type AddressParams struct { + PubKeyHashAddrID [2]byte + ScriptHashAddrID [2]byte +} + +// DecodeAddress decodes an address string into an internal btc address. +// ZCash uses a double SHA-256 checksum but with a 2-byte address ID, so +// a little customization is needed. +// TODO: There also appears to be a bech32 encoding and something called a +// "unified payment address", but for our use of this function client-side, +// we will never generate those addresses. +// Do we need to revisit before NU5? +func DecodeAddress(a string, addrParams *AddressParams, btcParams *chaincfg.Params) (btcutil.Address, error) { + b := base58.Decode(a) + if len(b) < 7 { + return nil, fmt.Errorf("invalid address") + } + + var addrID [2]byte + copy(addrID[:], b[:2]) + + var checkSum [4]byte + copy(checkSum[:], b[len(b)-4:]) + + data := b[2 : len(b)-4] + hashDigest := b[:len(b)-4] + + if checksum(hashDigest) != checkSum { + return nil, fmt.Errorf("invalid checksum") + } + + switch addrID { + case addrParams.PubKeyHashAddrID: + return btcutil.NewAddressPubKeyHash(data, btcParams) + case addrParams.ScriptHashAddrID: + return btcutil.NewAddressScriptHashFromHash(data, btcParams) + } + + return nil, fmt.Errorf("unknown address type %v", addrID) +} + +// RecodeAddress converts an internal btc address to a ZCash address string. +func RecodeAddress(addr string, addrParams *AddressParams, btcParams *chaincfg.Params) (string, error) { + btcAddr, err := btcutil.DecodeAddress(addr, btcParams) + if err != nil { + return "", err + } + + return EncodeAddress(btcAddr, addrParams) +} + +// EncodeAddress converts a btcutil.Address from the BTC backend into a ZCash +// address string. +func EncodeAddress(btcAddr btcutil.Address, addrParams *AddressParams) (string, error) { + switch btcAddr.(type) { + case *btcutil.AddressPubKeyHash: + return b58Encode(btcAddr.ScriptAddress(), addrParams.PubKeyHashAddrID), nil + case *btcutil.AddressScriptHash: + return b58Encode(btcAddr.ScriptAddress(), addrParams.ScriptHashAddrID), nil + } + + return "", fmt.Errorf("unsupported address type %T", btcAddr) +} + +// b58Encode base-58 encodes the address with the serialization +// addrID | input | 4-bytes of double-sha256 checksum +func b58Encode(input []byte, addrID [2]byte) string { + b := make([]byte, 0, 2+len(input)+4) + b = append(b, addrID[:]...) + b = append(b, input[:]...) + cksum := checksum(b) + b = append(b, cksum[:]...) + return base58.Encode(b) +} + +// checksum computes a checksum, which is the first 4 bytes of a double-sha256 +// hash of the input message. +func checksum(input []byte) (cksum [4]byte) { + h := sha256.Sum256(input) + h2 := sha256.Sum256(h[:]) + copy(cksum[:], h2[:4]) + return +} diff --git a/dex/networks/zec/addr_test.go b/dex/networks/zec/addr_test.go new file mode 100644 index 0000000000..39116237be --- /dev/null +++ b/dex/networks/zec/addr_test.go @@ -0,0 +1,30 @@ +package zec + +import ( + "bytes" + "encoding/hex" + "testing" +) + +func TestAddress(t *testing.T) { + pkHash, _ := hex.DecodeString("0ca584c97d2c84ea296524ac89f2febcf1094347") + addr := "t1K2UQ5VzGHGC1ZPqGJXXSocxtjo5s6peSJ" + + btcAddr, err := DecodeAddress(addr, MainNetAddressParams, MainNetParams) + if err != nil { + t.Fatalf("DecodeAddress error: %v", err) + } + + if !bytes.Equal(btcAddr.ScriptAddress(), pkHash) { + t.Fatalf("wrong script address") + } + + reAddr, err := RecodeAddress(btcAddr.String(), MainNetAddressParams, MainNetParams) + if err != nil { + t.Fatalf("RecodeAddress error: %v", err) + } + + if reAddr != addr { + t.Fatalf("wrong recoded address. expected %s, got %s", addr, reAddr) + } +} diff --git a/dex/networks/zec/block.go b/dex/networks/zec/block.go new file mode 100644 index 0000000000..d1edd08eba --- /dev/null +++ b/dex/networks/zec/block.go @@ -0,0 +1,126 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org + +package zec + +import ( + "bytes" + "fmt" + "io" + "time" + + "github.com/btcsuite/btcd/wire" +) + +// Block extends a wire.MsgBlock to specify ZCash specific fields, or in the +// case of the Nonce, a type-variant. +type Block struct { + wire.MsgBlock + // Transactions and MsgBlock.Transactions should both be populated. Each + // *Tx.MsgTx will be the same as the *MsgTx in MsgBlock.Transactions. + Transactions []*Tx + HashBlockCommitments [32]byte // Using NU5 name + Nonce [32]byte // Bitcoin uses uint32 + Solution []byte // length 1344 on main and testnet, 36 on regtest +} + +// DeserializeBlock deserializes the ZCash-encoded block. +func DeserializeBlock(b []byte) (*Block, error) { + zecBlock := &Block{} + + // https://zips.z.cash/protocol/protocol.pdf section 7.6 + r := bytes.NewReader(b) + + if err := zecBlock.decodeBlockHeader(r); err != nil { + return nil, err + } + + txCount, err := wire.ReadVarInt(r, pver) + if err != nil { + return nil, err + } + + // TODO: Limit txCount based on block size, header size, min tx size. + + zecBlock.MsgBlock.Transactions = make([]*wire.MsgTx, 0, txCount) + zecBlock.Transactions = make([]*Tx, 0, txCount) + for i := uint64(0); i < txCount; i++ { + tx := &Tx{MsgTx: new(wire.MsgTx)} + + if err := tx.ZecDecode(r); err != nil { + blockHash := zecBlock.BlockHash() + return nil, fmt.Errorf("error decoding tx at index %d in block %s: %w", i, blockHash, err) + } + zecBlock.MsgBlock.Transactions = append(zecBlock.MsgBlock.Transactions, tx.MsgTx) + zecBlock.Transactions = append(zecBlock.Transactions, tx) + } + + return zecBlock, nil +} + +// See github.com/zcash/zcash CBlockHeader -> SerializeOp +func (z *Block) decodeBlockHeader(r io.Reader) error { + hdr := &z.MsgBlock.Header + + nVersion, err := readUint32(r) + if err != nil { + return err + } + hdr.Version = int32(nVersion) + + if _, err = io.ReadFull(r, hdr.PrevBlock[:]); err != nil { + return err + } + + if _, err := io.ReadFull(r, hdr.MerkleRoot[:]); err != nil { + return err + } + + _, err = io.ReadFull(r, z.HashBlockCommitments[:]) + if err != nil { + return err + } + + nTime, err := readUint32(r) + if err != nil { + return err + } + hdr.Timestamp = time.Unix(int64(nTime), 0) + + hdr.Bits, err = readUint32(r) + if err != nil { + return err + } + + err = readInternalByteOrder(r, z.Nonce[:]) + if err != nil { + return err + } + + solSize, err := wire.ReadVarInt(r, pver) + if err != nil { + return err + } + if solSize != 1344 && solSize != 36 { + return fmt.Errorf("wrong solution size %d", solSize) + } + z.Solution = make([]byte, solSize) + + _, err = io.ReadFull(r, z.Solution) + if err != nil { + return err + } + + return nil +} + +func readInternalByteOrder(r io.Reader, b []byte) error { + if _, err := io.ReadFull(r, b); err != nil { + return err + } + // Reverse the bytes + for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 { + b[i], b[j] = b[j], b[i] + } + return nil +} diff --git a/dex/networks/zec/block_test.go b/dex/networks/zec/block_test.go new file mode 100644 index 0000000000..070e55e605 --- /dev/null +++ b/dex/networks/zec/block_test.go @@ -0,0 +1,101 @@ +package zec + +import ( + "bytes" + _ "embed" + "encoding/binary" + "encoding/hex" + "testing" + + "github.com/btcsuite/btcd/chaincfg/chainhash" +) + +var ( + //go:embed test-data/simnet_block_header.dat + simnetBlockHeader []byte + //go:embed test-data/block_1624455.dat + block1624455 []byte + //go:embed test-data/solution_1624455.dat + solution1624455 []byte +) + +func TestBlock(t *testing.T) { + // expHash := mustDecodeHex("00000000015c8a406ff880c5be4d2ae2744eab8be02a33d0179d68f47e51ea82") + const expVersion = 4 + expPrevBlock, _ := chainhash.NewHashFromStr("000000000136717e394d8de1f86257724bb463348993a1fbb651bd3fd3f1a279") + expMerkleRoot, _ := chainhash.NewHashFromStr("43eec2cfd1487ee70ab0277d7f761d99cf6c19d554d2f73959f4af45d516727f") + // expHashBlockCommitments := mustDecodeHex("30702d1b320e2ea8f603aa8fb54baea5581761d19fccf5061ac82e81d8fdeea4") + expNonce := mustDecodeHex("f5a409400000000000000000000300000000000000000000000000000000d0e2") + + expBits := binary.LittleEndian.Uint32(mustDecodeHex("d0aa011c")) + const expTime = 1649294915 + + zecBlock, err := DeserializeBlock(block1624455) + if err != nil { + t.Fatalf("decodeBlockHeader error: %v", err) + } + + hdr := &zecBlock.MsgBlock.Header + + if hdr.Version != expVersion { + t.Fatalf("wrong version. expected %d, got %d", expVersion, hdr.Version) + } + + if *expPrevBlock != hdr.PrevBlock { + t.Fatal("wrong previous block", expPrevBlock, hdr.PrevBlock[:]) + } + + if *expMerkleRoot != hdr.MerkleRoot { + t.Fatal("wrong merkle root", expMerkleRoot, hdr.MerkleRoot[:]) + } + + // TODO: Find out why this is not right. + // if !bytes.Equal(zecBlock.HashBlockCommitments[:], expHashBlockCommitments) { + // t.Fatal("wrong hashBlockCommitments", zecBlock.HashBlockCommitments[:], expHashBlockCommitments, h) + // } + + if hdr.Bits != expBits { + t.Fatalf("wrong bits") + } + + if hdr.Timestamp.Unix() != expTime { + t.Fatalf("wrong timestamp") + } + + if !bytes.Equal(zecBlock.Nonce[:], expNonce) { + t.Fatal("wrong nonce", zecBlock.Nonce[:], expNonce) + } + + if !bytes.Equal(zecBlock.Solution[:], solution1624455) { + t.Fatal("wrong solution") + } + + if len(zecBlock.Transactions) != 1 { + t.Fatalf("expected 1 transaction, got %d", len(zecBlock.Transactions)) + } + + tx := zecBlock.Transactions[0] + + if len(tx.TxIn) != 1 { + t.Fatalf("wrong number of tx inputs. expected 1, got %d", len(tx.TxIn)) + } + + if len(tx.TxOut) != 4 { + t.Fatalf("wrong number of tx outputs. expected 4, got %d", len(tx.TxOut)) + } +} + +func TestSimnetBlockHeader(t *testing.T) { + zecBlock := &Block{} + if err := zecBlock.decodeBlockHeader(bytes.NewReader(simnetBlockHeader)); err != nil { + t.Fatalf("decodeBlockHeader error: %v", err) + } +} + +func mustDecodeHex(hx string) []byte { + b, err := hex.DecodeString(hx) + if err != nil { + panic("mustDecodeHex: " + err.Error()) + } + return b +} diff --git a/dex/networks/zec/params.go b/dex/networks/zec/params.go new file mode 100644 index 0000000000..9f0876f1cd --- /dev/null +++ b/dex/networks/zec/params.go @@ -0,0 +1,85 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org + +package zec + +import ( + "decred.org/dcrdex/dex" + "decred.org/dcrdex/dex/networks/btc" + "github.com/btcsuite/btcd/chaincfg" +) + +const ( + // MinimumTxOverhead + // 4 header + 4 nVersionGroup + 1 varint input count + 1 varint output count + // + 4 lockTime + 4 nExpiryHeight + 8 valueBalanceSapling + 1 varint nSpendsSapling + // + 1 varint nOutputsSapling + 1 varint nJoinSplit + MinimumTxOverhead = 29 + + InitTxSizeBase = MinimumTxOverhead + btc.P2PKHOutputSize + btc.P2SHOutputSize // 29 + 34 + 32 = 95 + InitTxSize = InitTxSizeBase + btc.RedeemP2PKHInputSize // 95 + 149 = 244 +) + +var ( + UnitInfo = dex.UnitInfo{ + AtomicUnit: "Zats", + Conventional: dex.Denomination{ + Unit: "ZEC", + ConversionFactor: 1e8, + }, + } + + // MainNetParams are the clone parameters for mainnet. ZCash, + // like Decred, uses two bytes for their address IDs. We will convert + // between address types on the fly and use these spoof parameters + // internally. + MainNetParams = btc.ReadCloneParams(&btc.CloneParams{ + ScriptHashAddrID: 0xBD, + PubKeyHashAddrID: 0xB8, + CoinbaseMaturity: 100, + Net: 0x24e92764, + }) + // TestNet4Params are the clone parameters for testnet. + TestNet4Params = btc.ReadCloneParams(&btc.CloneParams{ + PubKeyHashAddrID: 0x25, + ScriptHashAddrID: 0xBA, + CoinbaseMaturity: 100, + Net: 0xfa1af9bf, + }) + // RegressionNetParams are the clone parameters for simnet. + RegressionNetParams = btc.ReadCloneParams(&btc.CloneParams{ + PubKeyHashAddrID: 0x25, + ScriptHashAddrID: 0xBA, + CoinbaseMaturity: 100, + Net: 0xaae83f5f, + }) + + // MainNetAddressParams are used for string address parsing. We use a + // spoofed address internally, since ZCash uses a two-byte address ID + // instead of a 1-byte ID. + MainNetAddressParams = &AddressParams{ + ScriptHashAddrID: [2]byte{0x1C, 0xBD}, + PubKeyHashAddrID: [2]byte{0x1C, 0xB8}, + } + + // TestNet4AddressParams are used for string address parsing. + TestNet4AddressParams = &AddressParams{ + ScriptHashAddrID: [2]byte{0x1C, 0xBA}, + PubKeyHashAddrID: [2]byte{0x1D, 0x25}, + } + + // RegressionNetAddressParams are used for string address parsing. + RegressionNetAddressParams = &AddressParams{ + ScriptHashAddrID: [2]byte{0x1C, 0xBA}, + PubKeyHashAddrID: [2]byte{0x1D, 0x25}, + } +) + +func init() { + for _, params := range []*chaincfg.Params{MainNetParams, TestNet4Params, RegressionNetParams} { + err := chaincfg.Register(params) + if err != nil { + panic("failed to register zec parameters: " + err.Error()) + } + } +} diff --git a/dex/networks/zec/test-data/block_1624455.dat b/dex/networks/zec/test-data/block_1624455.dat new file mode 100644 index 0000000000000000000000000000000000000000..698c4eeb5360891b4556ef869e5c220977c79156 GIT binary patch literal 1721 zcmZvceLNEg7{@o;%nY52^VXVGqKoEbZ6VvRB^-H~w^1(Rv%EBIocGr{FH;?%x^;yR zV;ZNt4n-$+UPH@E-fxuGFj2%+pU>Ur{{x}-hN51_hWJQOfwsx;+UDbFJ(`x-cpvq^;T!et|vg5^u(uJy_d zk(|;HhdVGl!Xe!;JS&XbzgQpn3TLfi#SU?^c0^M~691eQ6 zi`RshzRl^KoydwEO)PQ8e&72km*VnTBO6&gHV!qlJve3ELhct2z;+Vx;A60L2Gp^= z$j(r!(`4Q?Br2=pt{SJWf%K63_Qrr3;ag8PDho|;3Hxpek=pHx-I(JSjxRj?m2Ijk z5l8jUfDB$sL494-AjO#sT|2g_br@k_ zqI+pmg1Y}>;ObCByUd#!i=#uw^Ez|(0z=0a2DY67^<2((#81F%^$W!k|lnrK32H1nL2y6FQ7)V@R*FS zfNBCrT{W&_D4?Qi(fFjp3~536C-8d}TEB=dIydvAaX%7#UbAmr(Xw{LCeveu$ZS^R z@&7{V1%5Q}E~(qf_7C+%xGUG^3Xfl}lli!({tQWpnlP8I-F5<2-))1N3$q9i9i+O( z5Qqc`8DhXdd|)y+S7nt@t(*siP!7%V)71$Oju(h$&zVqnt_LC-7pXXD#sl*NLV9Nt zEdX}dm3(& z#tg<>4^-jlS&>;-G8{h6fV1s>fxql{xfQ%6ZKwy7ZsjNK41d9ei#ZjZX>pWf z5y`BOs8_0hI~wDFOgl5SrX_PM5?e-BvIu^JX%)8EK4s2{KR~CgfR7K|*IhQQP8Q3_ z4hEJ|MRjQyAb#)on{oEprmDV!o)Wen#6#`40$dQ>*JJbQXd0f0PP2XG80@D9*On4F z73a@K%MbG=DlU?CtyhvO%S#mV>aKp1RwCE(!U&q^izD{U8NF;kWVc30`=|mHfS(U zwy&UXwgyoj`a>DAwW?tqR<@VT{&2SnMYUi-4D-U?>BfX#%Sa(c#Qw9NgTQZp>jsy3qUQbM9J%fcna<#sJdV#N)9i>DT71I=Tgi5=l{O}S#|{2 literal 0 HcmV?d00001 diff --git a/dex/networks/zec/test-data/shielded_sapling_tx.dat b/dex/networks/zec/test-data/shielded_sapling_tx.dat new file mode 100644 index 0000000000000000000000000000000000000000..037f4a73b6a54caf00f4291b69422003891af426 GIT binary patch literal 2373 zcmV-L3A*+K004l6ATNmk000000Oq2!BN?X+2QF5yuKi~)ms4GZpR!&?Q4@bee8LAkkj`G_Pgjh)-_n^H++-2l$^ zA5VlCV8`h1f4~dEb}9v|!*!Ai0|AhXf!8#NzAn)fPn(`T8mWqx&id??MUPAbhip_K zowehe2HUiHO!w0~T-AAv?GX|%*9;#5tr3q@s=aUZ`2+-@QEkwjx%llN2eKWPUoG96 zK(MGUGClN1vPZd{G;wF={7MZyf@Vy(?Dn;6gNYpF2FOwd>OD6Vf;`MPig}vtAyyXG z%EcRjb&+3jsXahvT?=P8CV7}2PaYR5Qj0s=3t!IvGT`SdJ zR16I~(C78ISrWffvMpf#Xs}S%-s6I2Mjqb`J|>{S3Ew1DkXNQSS0>IP7k*HOW=_8{jX2m2M7!__xZ6X{)Ziu6 zO{p-dUnr%@AB%2~Jis$|&Xls->~dfvNbLAQj<0<0_ih!`(IScPY7O&sn6-zN1W2LJ zIP7_8xuh%WjodZz0Nq>a&MpeFgHVmW)LLFSPv}=1?p=b5%NF1maQ}A#RQ64AfRd8v z4uL(63}`*`+G(h?E5QqE%iGwu1+dk=M^5#RR7rcBls1&57x+=ex7NbiR-fXf*V44w znoF6sH7T7lz%_Oju-1JBrhSMNg&GBnhgBfc2@ZQv)ndAKNanhJA)CLI%(Yso<$On(!KBiRG}b&JiymX2YEXn0HnyI zBso&mK);t3bMPB2>8EFv2LwHH=$j{XoQaP*K)=ik%^g-XBQ(=uE0>6_SgOVx37OY#PD96mL%ugFv?>akH5z)LXc&};DClNmU70-)6L8YyEH{%j#<=!Z$kk+QWP z>{18i(Czq{4M{qAIt$M?JOlW)v^M{nNP{m(Gs1#!WDcTO0Os65mpfklq-P zae5CQN`RZrm{SBw~C7t`M2Gv)IS#55iXf8c^9<{FAcZc%!b` zSoU8of(koR)bKyVmweih72JL#vM>}-wq)j#HR}I`ezCP6ZzARKi%+5pn+FFLk|VU^ zf9LU@yrT8Cy6R$Gn9upA=IPSwqE&zj@Z@Jp(5|QM-t=R=bm3!plMW6tiy-L7vjuA* zzGvNZ4d=FnNoP##hH!SdOM+#y8bvEzlp{+#T+A|(<*e5K3(6qO2N{ibj;t!zrE=Q-t_^L2pI(dmc&ukR^j^CU4F`XXsH3ya$_;>U*GHv zu$~eV&0g~}Pa@Q1$ugVqc~34(9U_{swR=NdDYnhv4g1Y$t8VUr%+>pEdFJX-LZ z_~nfxKP@9-&L)X0%UE!<@7f6AI zk5gHGW6opn@NN^%l9O7G;K@_AE6r%7_|j~5mzv=KEsl$om`ttM@WE30E#?zDD6$u8 zt58%cCpJ``fm>5lv8}bCg`V>&=pAtF0a^hjboa;wNCKuY;U|+~45`xKM_PM2ysCE1 z62;n9o=XJz_pYfPBu=CDT)PQ?Ugvz79UoE12<##j4vMO_s@iPFF`C>y{An`-Hr-R% zHLhugy3jd@ASVw~O1n%^Nvy%R^O36_02N@@|9uXG`7gRL@^qn$%H>L|m%hjOoPiWD zACD@!r1;8-ud9Mv^hg6GoFUPOK<{?ZY4TMPrex^S2o_|^lpV(zfq1c#9EXIj$St?h>Xcd zgJs$L(> zmUauaz?d-gz^Hkc<8}Fs6Vko=$Jh}SaOjL|9z#O+jU5*M6a0wipENO%1e?q-w7EgJV_n=dA9{c|YSE~xi|NFp z4r$WG-NtJMUUjQ{h z=lj;MX~%L*hk7!P=GWaPZ%e8WEJciRFAxQie!VI|?f|-1aGsF+k4wIZg5-2F%Ep#e zEAiYjb_EHjRO5cvO8dTcWyn|H5zZ?2LfX-?q6I^}XwY~zE@8#6pA_QQnh&J@LyMI{ zs1h)jLTW;#bw}E zofN(smX>rZfC%*pk2E zc6B@GkSf&=Mj5tDgN*5`r`qbinQ*?pdQs5;2&HiDC|S1$6l5bvUHP^k;ST|IVSZp; rbg(7B%dL{^?XmY2$ZjEVK$e(2 literal 0 HcmV?d00001 diff --git a/dex/networks/zec/test-data/simnet_block.dat b/dex/networks/zec/test-data/simnet_block.dat new file mode 100644 index 0000000000000000000000000000000000000000..36bf2b6260966b026dbc37054807eed094fe4d94 GIT binary patch literal 177 zcmV;i08ak|000260q;!obE$*Z(Cp768CiMREyT1S{JqAZG80H;n6M0P1YtD%Y}jCQ zUbZd|6-tJ}^`W2_wOu1B zUmyj(Jx*c|4-X&&0FR{;y--?L#h`;7yaALu64y>I3Z8av<#u#7sQ>^Z0OQUW19GWx fHY7vY9`fHEzugrvOp-FAIkDb6q);8DyTft*n|MxE literal 0 HcmV?d00001 diff --git a/dex/networks/zec/test-data/simnet_block_header.dat b/dex/networks/zec/test-data/simnet_block_header.dat new file mode 100644 index 0000000000000000000000000000000000000000..36bf2b6260966b026dbc37054807eed094fe4d94 GIT binary patch literal 177 zcmV;i08ak|000260q;!obE$*Z(Cp768CiMREyT1S{JqAZG80H;n6M0P1YtD%Y}jCQ zUbZd|6-tJ}^`W2_wOu1B zUmyj(Jx*c|4-X&&0FR{;y--?L#h`;7yaALu64y>I3Z8av<#u#7sQ>^Z0OQUW19GWx fHY7vY9`fHEzugrvOp-FAIkDb6q);8DyTft*n|MxE literal 0 HcmV?d00001 diff --git a/dex/networks/zec/test-data/solution_1624455.dat b/dex/networks/zec/test-data/solution_1624455.dat new file mode 100644 index 0000000000000000000000000000000000000000..b90878bbe559e9eecbf6482702a0e0ffd0d5ff97 GIT binary patch literal 1344 zcmV-G1;6?LWJ9Z?6@5O_gdtfo5>~|OEjxu%8IyXa&T{eh;YTymR`MZxnj0TAJ_0CS z6(EPe_+xvDJ-zM6DwRTCQgA2hu4-CUG+7QbMOp?_!>X%Uo|KFPpqcnU1*>-ocyYV$ za7E4aviH^u6xo@m)Y{vbM4B>P$$lQ~x_Q?(Nf$?nb5}`G9h5)=w;I|lnP)84|FtQc z3)@Z&3=0v?T;SbjuM6$=f7@RIMlLS{j~h~oFj93SFkTe=HZ1U-32oF07)VH+FTd1m zOzf`<1H=D5AO$<=p3vvulYQNRsaQ5D(EXoORooktAGY4#2Rln6;z`0q)8*6&{((LO zE(!H+2T{nTO)@3SI_p+*dXvkX8JW_&M6Ialk<=MK5?fbVaxXtsb{66th|Xj-_v)^s z-|VfJi90Ox<>4mDR;c(3sLL~1V|l3OZc!${i#WvrJH^S((Z0*9YLKhKqq@1Z*i=(R z2lFoSB_kn z#{#rY#NGGP5u+3fN?fu^m++|CdkA@}SdBt0&%RTpKz**jqS{LzMf7EeVn;V!k|+=S zXd&H`J~WVaYPLTE8W?`d*=n%O?KwP9=0xljbp-YoW7F2I)?Vhny&fL~VIb1$5kt7! zN|IUeKykqltFGi9Eo=HQU#YqKlxcKi9at5*pUE|ixd{3Xx=%zDT7c@JC&xGmy3R^B z>UKhE)+1V0d_O=x^hE_RZXNk?arp8I6d}c#rKAU$V?+Im-~%_SYd(qUI$HjXYaPAq zS~mu6r9pr{kIcSfb02ca!Jh~R0C=!eD05vO`ny?Qa;P@H;l{#IxD?*|bAYmEh)q;J z8h8f)^YwgkPCgj&;|UG)^isB3vbHw=7cU(z@6c{VsOb@R*jKzX{#%^0MLGbx+OzM; z<;$_vI=)pH4SLC1A{pD4SS74TmtOR#e3{Qh^FK>T~fC4z3mWFhw-CyIaG;-zh_2?S+=aBcVT9M1VdM#S!^NfYygJ*|4)*@3&9=mA@->*jt!iw3? z1CK{#22%^ss0-zpuv?0LREE|>J)}S_stz|Rd{7^XPu}w-2rhXxtY;KLZ?iPT$-+y( zaq8vy2jfQpv?QS{?>V-H;Q--%tmwbCP7^oeB!Ss67B3%hU9 z&q~)Tiav2Kic8l~Z)GhDCj!<|r=sh74ce>Vux&*DN%4lVuc;BBxr-2mSlBtB0i9Bv z+XZxFPN(h+;b}zN5HsUrgaYeiaV^$HdXlc^4VU>aqfigio@eo&?tHGx^aOv}Xc}_1V4EBREzD2A(rx+f`dzOh=H^)~3KX65Q&hv(oFoJp? zWf9R|wHIq04S}az1JeG`yc=;m$nh_g#cW!wX$Ni!xEOZJN8Is40UFW(3ElmZkjNol z0h$J#I>|cDMc8l3ni+``*m4L3WMlXA!6&L)8bJTDgoX_h5t;(g8c_4K{&8p6ZacbJaQ CafZ48 literal 0 HcmV?d00001 diff --git a/dex/networks/zec/test-data/unshielded_orchard_tx.dat b/dex/networks/zec/test-data/unshielded_orchard_tx.dat new file mode 100644 index 0000000000000000000000000000000000000000..71e7be21f90c65b00a64c90d20bcb1bd0a20ea05 GIT binary patch literal 244 zcmZQ!U}$Jn(C=jYu;;|yCEG+D`6lvNRv$aL-}Q3R2ZqLJDtlDKmZ?X5yUNJGz>wu` z;KHOJ^{j9DR*}Cq{wPU{#$5G(V|t2-&+qfJ@{rCQ(;HJ3$1o{~-ne|Z^NPsKrTdoV zFI0cu^YP(|k^|4~{>;72uc`Ym!-G+gd8fjEI|)Hu$-4%b^X;aIX7_fBRa{xNsc$2P zJmcMebE5tO0h5ChKhRbN2FbFOB0k=;-)*;%{Cw`}RO^T>MV!Z literal 0 HcmV?d00001 diff --git a/dex/networks/zec/test-data/unshielded_sapling_tx.dat b/dex/networks/zec/test-data/unshielded_sapling_tx.dat new file mode 100644 index 0000000000000000000000000000000000000000..66b3424611cf265d91090c34f57f8f8ea786298b GIT binary patch literal 244 zcmZQ!U}$Jn(C=i-PYrn0Ji(~f_|GTH&U+pe!=@d)@Q zc)kBW5HKzHSIZ2tSF&uSNYc)kGCK^px=QvYN4^aC-Kf9(a&^a=b2rMFU@H8-Oq*D} Z&e&j9*LChUji%B2C(Y>WSObzp0|4lrUZDU0 literal 0 HcmV?d00001 diff --git a/dex/networks/zec/test-data/v2_joinsplit_tx.dat b/dex/networks/zec/test-data/v2_joinsplit_tx.dat new file mode 100644 index 0000000000000000000000000000000000000000..8dfe906db6e083dadce1dd0e94cfe9e514ec3750 GIT binary patch literal 1943 zcmV;I2Wa>L000000clQA0000002y|v6crPixMV^n7Kjai7g)NA^1)o`O^B=j00001 z0000000000d38|$00000Ja3{+Mae1mp~Es_uo=bIa^M!bk$BCj^J4=>Ur@?Nf-T^) zjZFitOP}NQk8My873Ur)mNQ)d9kBQw51<@rOX~$SVLpo^Nfy2j|NM*^6^B*{J zX)8E&?^B+vSl>l(G8&)kLzT+8FqoSikUcI#r>lFT?zwv=D#h=CQy;5AY;NQ2b=sP@x06=h=+PFAQF z4EKRyb3~&(P6Wtxp30zG;KbLFYS%|RY0AvCe*Ev+>AHnWCpXr*83LW9V1|5mRvHrM zw)|=<%?3+iB|ZOP4{%HSFlMFiuDe+ZsFxJD$W6`w_01Bg0tDsaKhMh% zl>a5x?x7YdsU7-qaLY9O8b*$F$jS>nR009_5*+;AkVIE3_VY!85$h!16=0i)mAnMn z{gX4ju)qokdoN|)epZvxddV3eDc(~V);lxiR5IOCcBGJ2*uJk#_4@#-yndL5k%XTm zjz|>p;qG1cYESv$ySg6Nj%#fKCHaU$tOB}BBaq;fP4oA}MHifT!c>fy5eW>$0$`s4 z0x(Q-(|Wy^satj+Nr)FyzvBA+F=LlSR#mSs|jqIBT7&lqUyvO0DTOZneO?s2O*SKgIQ?CKz zgLe<_oVo4S;wwudW%H2S& zjFC*I{b)sF=SBF$CBiY3xd(JaTG!u~W7^+<-d@AI0V=M;br|3~p|;qaSXp%}S%F|9 zZiYv41!ivD=9n~tNWxtMhF1M^_@tyjKQf`R`QfQCqN}h8=Qf7_6$|`gKRDXehUY!( z3e~z*-vL}`w0b?5{a&;4yGG9+!!X9H;`ZrKkuZ~C5uB5MC5s~_pppX5XpZZk@g6G@ z4WjJ1@ydmDkA)wxE>8VYZ_AmrxWq`~(xq+CZyOQ2)|hKxc5Ez*1!E?p9EK61xNN~~ z9>=e%=#IMXKcI)y_@b1!W@`*kGUA@PZT11kr^!dZzNCXy&TuPL!<^gj61s&8Shi-e(rbyBz0j>#z1pA+T8wC zuQA0}Q!(d%zrvKT0Uh`84f*GtlZ=YEx=&MY(DAge-GDy-axH^Yn%|9lpUKxT#6YACL6gL>{lsOahS=PI7NFbqQnD#Zs(8;ieD7&0~Uab zMxW1=(*s_SDjk4k-CAiPo~EkvEGj!+(0||CDC)fGafxySI~Xn!8V#Dpo9Yezim%Un zu~yKx_SiOQl}H%5ru;r?F{b?uxBaH+!rYivO6q)ne$=}ye&}zHjE|}I$m7zqxNrac zZzcbaW~fk(VU_Jmkm#iK26Px2Z2fY=+_EyRke`zQ+I%#c6L75$|6-n!wB~Z+-V~uS zoPMc$&T3AEo&g@BRU5o?dD~$%A zAATQ=Vkdn(2JnI*#myd<_00E=s($Jlh-C~lBdzWK8gt$x|8V6N3GC$1u#2u{8 dQLFotV(S2)wiDYsLW^RZNikujG}}6pvOAzKmhy)0ygITt}KiKASM&Tsae(_Es|v`ML17;UH{2_BGzu 0 + + prevoutsDigest, err := tx.hashPrevOutsSig(anyoneCanPay) + if err != nil { + return + } + buf.Write(prevoutsDigest[:]) + + amtsDigest, err := tx.hashAmountsSig(anyoneCanPay, vals) + if err != nil { + return + } + buf.Write(amtsDigest[:]) + + prevScriptsDigest, err := tx.hashPrevScriptsSig(anyoneCanPay, prevScripts) + if err != nil { + return + } + buf.Write(prevScriptsDigest[:]) + + seqsDigest, err := tx.hashSequenceSig(anyoneCanPay) + if err != nil { + return + } + buf.Write(seqsDigest[:]) + + outputsDigest, err := tx.hashOutputsSig(anyoneCanPay) + if err != nil { + return + } + buf.Write(outputsDigest[:]) + + txInsDigest, err := tx.hashTxInSig(vin, vals[vin], prevScripts[vin]) + if err != nil { + return + } + buf.Write(txInsDigest[:]) + + return blake2bHash(buf.Bytes(), []byte(pkTransparentDigest)) +} + +func (tx *Tx) calcHashPrevOuts() ([32]byte, error) { + var buf bytes.Buffer + for _, in := range tx.TxIn { + buf.Write(in.PreviousOutPoint.Hash[:]) + var b [4]byte + binary.LittleEndian.PutUint32(b[:], in.PreviousOutPoint.Index) + buf.Write(b[:]) + } + return blake2bHash(buf.Bytes(), []byte(pkPrevOuts)) +} + +func (tx *Tx) hashPrevOutsSig(anyoneCanPay bool) ([32]byte, error) { + if anyoneCanPay { + return blake2bHash([]byte{}, []byte(pkPrevOuts)) + } + return tx.calcHashPrevOuts() +} + +func (tx *Tx) hashAmountsSig(anyoneCanPay bool, vals []int64) ([32]byte, error) { + if anyoneCanPay { + return blake2bHash([]byte{}, []byte(pkAmounts)) + } + b := make([]byte, 0, 8*len(vals)) + for _, v := range vals { + b = append(b, int64Bytes(v)...) + } + return blake2bHash(b, []byte(pkAmounts)) +} + +func (tx *Tx) hashPrevScriptsSig(anyoneCanPay bool, prevScripts [][]byte) (_ [32]byte, err error) { + if anyoneCanPay { + return blake2bHash([]byte{}, []byte(pkPrevScripts)) + } + buf := new(bytes.Buffer) + for _, s := range prevScripts { + if err = wire.WriteVarBytes(buf, pver, s); err != nil { + return + } + } + + return blake2bHash(buf.Bytes(), []byte(pkPrevScripts)) +} + +func (tx *Tx) hashSequence() ([32]byte, error) { + var b bytes.Buffer + for _, in := range tx.TxIn { + var buf [4]byte + binary.LittleEndian.PutUint32(buf[:], in.Sequence) + b.Write(buf[:]) + } + return blake2bHash(b.Bytes(), []byte(pkSequence)) +} + +func (tx *Tx) hashSequenceSig(anyoneCanPay bool) (h [32]byte, err error) { + if anyoneCanPay { + return blake2bHash([]byte{}, []byte(pkSequence)) + } + return tx.hashSequence() +} + +func (tx *Tx) hashOutputs() (_ [32]byte, err error) { + var b bytes.Buffer + for _, out := range tx.TxOut { + if err = wire.WriteTxOut(&b, 0, 0, out); err != nil { + return chainhash.Hash{}, err + } + } + return blake2bHash(b.Bytes(), []byte(pkOutputs)) +} + +func (tx *Tx) hashOutputsSig(anyoneCanPay bool) (_ [32]byte, err error) { + if anyoneCanPay { + return blake2bHash([]byte{}, []byte(pkOutputs)) + } + return tx.hashOutputs() +} + +func (tx *Tx) hashTxInSig(idx int, prevVal int64, prevScript []byte) (h [32]byte, err error) { + if len(tx.TxIn) <= idx { + return h, fmt.Errorf("no input at index %d", idx) + } + txIn := tx.TxIn[idx] + + // prev_hash 32 + prev_index 4 + prev_value 8 + script_pub_key var_int+L (size) + nSequence 4 + b := bytes.NewBuffer(make([]byte, 0, 50+len(prevScript))) + + b.Write(txIn.PreviousOutPoint.Hash[:]) + b.Write(uint32Bytes(txIn.PreviousOutPoint.Index)) + b.Write(int64Bytes(prevVal)) + if err = wire.WriteVarBytes(b, pver, prevScript); err != nil { + return + } + b.Write(uint32Bytes(txIn.Sequence)) + + return blake2bHash(b.Bytes(), []byte(pkTxIn)) + +} + +// Bytes encodes the receiver to w using the bitcoin protocol encoding. +// This is part of the Message interface implementation. +// See Serialize for encoding transactions to be stored to disk, such as in a +// database, as opposed to encoding transactions for the wire. +// msg.Version must be 4 or 5. +func (tx *Tx) Bytes() (_ []byte, err error) { + w := new(bytes.Buffer) + header := uint32(tx.Version) + if tx.Version >= VersionOverwinter { + header |= 1 << 31 + } + + if err = putUint32(w, header); err != nil { + return nil, fmt.Errorf("error writing version: %w", err) + } + + if tx.Version >= VersionOverwinter { + var groupID uint32 = versionOverwinterGroupID + switch tx.Version { + case VersionSapling: + groupID = versionSaplingGroupID + case VersionNU5: + groupID = versionNU5GroupID + } + + // nVersionGroupId + if err = putUint32(w, groupID); err != nil { + return nil, fmt.Errorf("error writing nVersionGroupId: %w", err) + } + } + + if tx.Version == VersionNU5 { + // nConsensusBranchId + if _, err = w.Write(ConsensusBranchNU5[:]); err != nil { + return nil, fmt.Errorf("error writing nConsensusBranchId: %w", err) + } + + // lock_time + if err = putUint32(w, tx.LockTime); err != nil { + return nil, fmt.Errorf("error writing lock_time: %w", err) + } + + // nExpiryHeight + if err = putUint32(w, tx.ExpiryHeight); err != nil { + return nil, fmt.Errorf("error writing nExpiryHeight: %w", err) + } + } + + // tx_in_count + if err = wire.WriteVarInt(w, pver, uint64(len(tx.MsgTx.TxIn))); err != nil { + return nil, fmt.Errorf("error writing tx_in_count: %w", err) + } + + // tx_in + for vin, ti := range tx.TxIn { + if err = writeTxIn(w, ti); err != nil { + return nil, fmt.Errorf("error writing tx_in %d: %w", vin, err) + } + } + + // tx_out_count + if err = wire.WriteVarInt(w, pver, uint64(len(tx.TxOut))); err != nil { + return nil, fmt.Errorf("error writing tx_out_count: %w", err) + } + + // tx_out + for vout, to := range tx.TxOut { + if err = wire.WriteTxOut(w, pver, tx.Version, to); err != nil { + return nil, fmt.Errorf("error writing tx_out %d: %w", vout, err) + } + } + + if tx.Version <= VersionSapling { + // lock_time + if err = putUint32(w, tx.LockTime); err != nil { + return nil, fmt.Errorf("error writing lock_time: %w", err) + } + + if tx.Version >= VersionOverwinter { + // nExpiryHeight + if err = putUint32(w, tx.ExpiryHeight); err != nil { + return nil, fmt.Errorf("error writing nExpiryHeight: %w", err) + } + } + } + + if tx.Version == VersionSapling { + // valueBalanceSapling + if err = putUint64(w, 0); err != nil { + return nil, fmt.Errorf("error writing valueBalanceSapling: %w", err) + } + } + + if tx.Version >= VersionSapling { + // nSpendsSapling + if err = wire.WriteVarInt(w, pver, 0); err != nil { + return nil, fmt.Errorf("error writing nSpendsSapling: %w", err) + } + + // nOutputsSapling + if err = wire.WriteVarInt(w, pver, 0); err != nil { + return nil, fmt.Errorf("error writing nOutputsSapling: %w", err) + } + } + + if tx.Version >= VersionPreOverwinter && tx.Version <= VersionSapling { + // nJoinSplit + if err = wire.WriteVarInt(w, pver, 0); err != nil { + return nil, fmt.Errorf("error writing nJoinSplit: %w", err) + } + return w.Bytes(), nil + } + + // NU 5 + + // no anchorSapling, because nSpendsSapling = 0 + // no bindingSigSapling or valueBalanceSapling, because nSpendsSapling + nOutputsSapling = 0 + + if tx.Version == VersionNU5 { + // nActionsOrchard + if err = wire.WriteVarInt(w, pver, 0); err != nil { + return nil, fmt.Errorf("error writing nActionsOrchard: %w", err) + } + } + + // vActionsOrchard, flagsOrchard, valueBalanceOrchard, anchorOrchard, + // sizeProofsOrchard, proofsOrchard, vSpendAuthSigsOrchard, and + // bindingSigOrchard are all empty, because nActionsOrchard = 0. + + return w.Bytes(), nil +} + +// see https://zips.z.cash/protocol/protocol.pdf section 7.1 +func DeserializeTx(b []byte) (*Tx, error) { + tx := &Tx{MsgTx: new(wire.MsgTx)} + r := bytes.NewReader(b) + if err := tx.ZecDecode(r); err != nil { + return nil, err + } + + remains := r.Len() + if remains > 0 { + return nil, fmt.Errorf("incomplete deserialization. %d bytes remaining", remains) + } + + return tx, nil +} + +// ZecDecode reads the serialized transaction from the reader and populates the +// *Tx's fields. +func (tx *Tx) ZecDecode(r io.Reader) (err error) { + ver, err := readUint32(r) + if err != nil { + return fmt.Errorf("error reading version: %w", err) + } + + // overWintered := (ver & (1 << 31)) > 0 + ver &= overwinterMask // Clear the overwinter bit + tx.Version = int32(ver) + + if tx.Version > VersionNU5 { + return fmt.Errorf("unsupported tx version %d > 4", ver) + } + + if ver >= VersionOverwinter { + // nVersionGroupId uint32 + if err = discardBytes(r, 4); err != nil { + return fmt.Errorf("error reading nVersionGroupId: %w", err) + } + } + + if ver == VersionNU5 { + // nConsensusBranchId uint32 + if err = discardBytes(r, 4); err != nil { + return fmt.Errorf("error reading nConsensusBranchId: %w", err) + } + // lock_time + if tx.LockTime, err = readUint32(r); err != nil { + return fmt.Errorf("error reading lock_time: %w", err) + } + // nExpiryHeight + if tx.ExpiryHeight, err = readUint32(r); err != nil { + return fmt.Errorf("error reading nExpiryHeight: %w", err) + } + } + + txInCount, err := wire.ReadVarInt(r, pver) + if err != nil { + return err + } + + tx.TxIn = make([]*wire.TxIn, 0, txInCount) + for i := 0; i < int(txInCount); i++ { + ti := new(wire.TxIn) + if err = readTxIn(r, ti); err != nil { + return err + } + tx.TxIn = append(tx.TxIn, ti) + } + + txOutCount, err := wire.ReadVarInt(r, pver) + if err != nil { + return err + } + + tx.TxOut = make([]*wire.TxOut, 0, txOutCount) + for i := 0; i < int(txOutCount); i++ { + to := new(wire.TxOut) + if err = readTxOut(r, to); err != nil { + return err + } + tx.TxOut = append(tx.TxOut, to) + } + + if ver < VersionNU5 { + // lock_time + if tx.LockTime, err = readUint32(r); err != nil { + return fmt.Errorf("error reading lock_time: %w", err) + } + } + + if ver == VersionOverwinter || ver == VersionSapling { + // nExpiryHeight + if tx.ExpiryHeight, err = readUint32(r); err != nil { + return fmt.Errorf("error reading nExpiryHeight: %w", err) + } + } + + // That's it for pre-overwinter. + if ver < VersionPreOverwinter { + return nil + } + + var bindingSigRequired bool + if ver == VersionSapling { + // valueBalanceSapling uint64 + if tx.ValueBalanceSapling, err = readInt64(r); err != nil { + return fmt.Errorf("error reading valueBalanceSapling: %w", err) + } + + if tx.NSpendsSapling, err = wire.ReadVarInt(r, pver); err != nil { + return fmt.Errorf("error reading nSpendsSapling: %w", err) + } else if tx.NSpendsSapling > 0 { + // vSpendsSapling - discard + bindingSigRequired = true + if err = discardBytes(r, int64(tx.NSpendsSapling*384)); err != nil { + return fmt.Errorf("error reading vSpendsSapling: %w", err) + } + } + + if tx.NOutputsSapling, err = wire.ReadVarInt(r, pver); err != nil { + return fmt.Errorf("error reading nOutputsSapling: %w", err) + } else if tx.NOutputsSapling > 0 { + // vOutputsSapling - discard + bindingSigRequired = true + if err = discardBytes(r, int64(tx.NOutputsSapling*948)); err != nil { + return fmt.Errorf("error reading vOutputsSapling: %w", err) + } + } + } + + if ver <= VersionSapling && ver >= VersionPreOverwinter { + if tx.NJoinSplit, err = wire.ReadVarInt(r, pver); err != nil { + return fmt.Errorf("error reading nJoinSplit: %w", err) + } else if tx.NJoinSplit > 0 { + // vJoinSplit - discard + tx.VJoinSplit = make([]*JoinSplit, 0, tx.NJoinSplit) + for i := uint64(0); i < tx.NJoinSplit; i++ { + sz := overwinterJoinSplitSize + if ver == 4 { + sz = saplingJoinSplitSize + } + old, err := readUint64(r) + if err != nil { + return fmt.Errorf("error reading joinsplit old: %w", err) + } + new, err := readUint64(r) + if err != nil { + return fmt.Errorf("error reading joinsplit new: %w", err) + } + tx.VJoinSplit = append(tx.VJoinSplit, &JoinSplit{ + Old: old, + New: new, + }) + if err = discardBytes(r, int64(sz-16)); err != nil { + return fmt.Errorf("error reading vJoinSplit: %w", err) + } + } + // joinSplitPubKey + if err = discardBytes(r, 32); err != nil { + return fmt.Errorf("error reading joinSplitPubKey: %w", err) + } + + // joinSplitSig + if err = discardBytes(r, 64); err != nil { + return fmt.Errorf("error reading joinSplitSig: %w", err) + } + } + } else { // NU5 + // nSpendsSapling + tx.NSpendsSapling, err = wire.ReadVarInt(r, pver) + if err != nil { + return fmt.Errorf("error reading nSpendsSapling: %w", err) + } else if tx.NSpendsSapling > 0 { + // vSpendsSapling - discard + bindingSigRequired = true + if err = discardBytes(r, int64(tx.NSpendsSapling*96)); err != nil { + return fmt.Errorf("error reading vSpendsSapling: %w", err) + } + } + + // nOutputsSapling + tx.NOutputsSapling, err = wire.ReadVarInt(r, pver) + if err != nil { + return fmt.Errorf("error reading nSpendsSapling: %w", err) + } else if tx.NOutputsSapling > 0 { + // vOutputsSapling - discard + bindingSigRequired = true + if err = discardBytes(r, int64(tx.NOutputsSapling*756)); err != nil { + return fmt.Errorf("error reading vOutputsSapling: %w", err) + } + } + + if tx.NOutputsSapling+tx.NSpendsSapling > 0 { + // valueBalanceSpending uint64 + if err = discardBytes(r, 8); err != nil { + return fmt.Errorf("error reading valueBalanceSpending: %w", err) + } + } + + if tx.NSpendsSapling > 0 { + // anchorSapling + if err = discardBytes(r, 32); err != nil { + return fmt.Errorf("error reading anchorSapling: %w", err) + } + // vSpendProofsSapling + if err = discardBytes(r, int64(tx.NSpendsSapling*192)); err != nil { + return fmt.Errorf("error reading vSpendProofsSapling: %w", err) + } + // vSpendAuthSigsSapling + if err = discardBytes(r, int64(tx.NSpendsSapling*64)); err != nil { + return fmt.Errorf("error reading vSpendAuthSigsSapling: %w", err) + } + } + + if tx.NOutputsSapling > 0 { + // vOutputProofsSapling + if err = discardBytes(r, int64(tx.NOutputsSapling*192)); err != nil { + return fmt.Errorf("error reading vOutputProofsSapling: %w", err) + } + } + } + + if bindingSigRequired { + // bindingSigSapling + if err = discardBytes(r, 64); err != nil { + return fmt.Errorf("error reading bindingSigSapling: %w", err) + } + } + + // pre-NU5 is done now. + if ver < VersionNU5 { + return nil + } + + // NU5-only fields below. + + // nActionsOrchard + tx.NActionsOrchard, err = wire.ReadVarInt(r, pver) + if err != nil { + return fmt.Errorf("error reading bindingSigSapling: %w", err) + } + + if tx.NActionsOrchard == 0 { + return nil + } + + // vActionsOrchard + if err = discardBytes(r, int64(tx.NActionsOrchard*820)); err != nil { + return fmt.Errorf("error reading vActionsOrchard: %w", err) + } + + // flagsOrchard + if err = discardBytes(r, 1); err != nil { + return fmt.Errorf("error reading flagsOrchard: %w", err) + } + + // valueBalanceOrchard uint64 + if tx.ValueBalanceOrchard, err = readInt64(r); err != nil { + return fmt.Errorf("error reading valueBalanceOrchard: %w", err) + } + + // anchorOrchard + if err = discardBytes(r, 32); err != nil { + return fmt.Errorf("error reading anchorOrchard: %w", err) + } + + // sizeProofsOrchard + tx.SizeProofsOrchard, err = wire.ReadVarInt(r, pver) + if err != nil { + return fmt.Errorf("error reading sizeProofsOrchard: %w", err) + } + + // proofsOrchard + if err = discardBytes(r, int64(tx.SizeProofsOrchard)); err != nil { + return fmt.Errorf("error reading proofsOrchard: %w", err) + } + + // vSpendAuthSigsOrchard + if err = discardBytes(r, int64(tx.NActionsOrchard*64)); err != nil { + return fmt.Errorf("error reading vSpendAuthSigsOrchard: %w", err) + } + + // bindingSigOrchard + if err = discardBytes(r, 64); err != nil { + return fmt.Errorf("error reading bindingSigOrchard: %w", err) + } + + return nil +} + +// SerializeSize is the size of the transaction when serialized. +func (tx *Tx) SerializeSize() uint64 { + var sz uint64 = 4 // header + ver := tx.Version + sz += uint64(wire.VarIntSerializeSize(uint64(len(tx.TxIn)))) // tx_in_count + for _, txIn := range tx.TxIn { // tx_in + sz += 32 /* prev hash */ + 4 /* prev index */ + 4 /* sequence */ + sz += uint64(wire.VarIntSerializeSize(uint64(len(txIn.SignatureScript))) + len(txIn.SignatureScript)) + } + sz += uint64(wire.VarIntSerializeSize(uint64(len(tx.TxOut)))) // tx_out_count + for _, txOut := range tx.TxOut { // tx_out + sz += 8 /* value */ + sz += uint64(wire.VarIntSerializeSize(uint64(len(txOut.PkScript))) + len(txOut.PkScript)) + } + sz += 4 // lockTime + + // join-splits are only versions 2 to 4. + if ver >= VersionPreOverwinter && ver < VersionNU5 { + sz += uint64(wire.VarIntSerializeSize(tx.NJoinSplit)) + if tx.NJoinSplit > 0 { + if ver < VersionSapling { + sz += tx.NJoinSplit * overwinterJoinSplitSize + } else { + sz += tx.NJoinSplit * saplingJoinSplitSize + } + sz += 32 // joinSplitPubKey + sz += 64 // joinSplitSig + } + } + + if ver >= VersionOverwinter { + sz += 4 // nExpiryHeight + sz += 4 // nVersionGroupId + } + + if ver >= VersionSapling { + sz += 8 // valueBalanceSapling + sz += uint64(wire.VarIntSerializeSize(tx.NSpendsSapling)) // nSpendsSapling + sz += 384 * tx.NSpendsSapling // vSpendsSapling + sz += uint64(wire.VarIntSerializeSize(tx.NOutputsSapling)) // nOutputsSapling + sz += 948 * tx.NOutputsSapling // vOutputsSapling + if tx.NSpendsSapling+tx.NOutputsSapling > 0 { + sz += 64 // bindingSigSapling + } + } + + if ver == VersionNU5 { + // With nSpendsSapling = 0 and nOutputsSapling = 0 + sz += 4 // nConsensusBranchId + sz += uint64(wire.VarIntSerializeSize(tx.NActionsOrchard)) // nActionsOrchard + if tx.NActionsOrchard > 0 { + sz += tx.NActionsOrchard * 820 // vActionsOrchard + sz++ // flagsOrchard + sz += 8 // valueBalanceOrchard + sz += 32 // anchorOrchard + sz += uint64(wire.VarIntSerializeSize(tx.SizeProofsOrchard)) // sizeProofsOrchard + sz += tx.SizeProofsOrchard // proofsOrchard + sz += 64 * tx.NActionsOrchard // vSpendAuthSigsOrchard + sz += 64 // bindingSigOrchard + + } + } + return sz +} + +// writeTxIn encodes ti to the bitcoin protocol encoding for a transaction +// input (TxIn) to w. +func writeTxIn(w io.Writer, ti *wire.TxIn) error { + err := writeOutPoint(w, &ti.PreviousOutPoint) + if err != nil { + return err + } + + err = wire.WriteVarBytes(w, pver, ti.SignatureScript) + if err != nil { + return err + } + + return putUint32(w, ti.Sequence) +} + +// writeOutPoint encodes op to the bitcoin protocol encoding for an OutPoint +// to w. +func writeOutPoint(w io.Writer, op *wire.OutPoint) error { + _, err := w.Write(op.Hash[:]) + if err != nil { + return err + } + return putUint32(w, op.Index) +} + +func uint32Bytes(v uint32) []byte { + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, v) + return b +} + +// putUint32 writes a little-endian encoded uint32 to the Writer. +func putUint32(w io.Writer, v uint32) error { + _, err := w.Write(uint32Bytes(v)) + return err +} + +func uint64Bytes(v uint64) []byte { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, v) + return b +} + +func int64Bytes(v int64) []byte { + return uint64Bytes(uint64(v)) +} + +// putUint64 writes a little-endian encoded uint64 to the Writer. +func putUint64(w io.Writer, v uint64) error { + _, err := w.Write(uint64Bytes(v)) + return err +} + +// readUint32 reads a little-endian encoded uint32 from the Reader. +func readUint32(r io.Reader) (uint32, error) { + b := make([]byte, 4) + if _, err := io.ReadFull(r, b); err != nil { + return 0, err + } + return binary.LittleEndian.Uint32(b), nil +} + +// readUint64 reads a little-endian encoded uint64 from the Reader. +func readUint64(r io.Reader) (uint64, error) { + b := make([]byte, 8) + if _, err := io.ReadFull(r, b); err != nil { + return 0, err + } + return binary.LittleEndian.Uint64(b), nil +} + +func readInt64(r io.Reader) (int64, error) { + u, err := readUint64(r) + if err != nil { + return 0, err + } + return int64(u), nil +} + +// readTxIn reads the next sequence of bytes from r as a transaction input. +func readTxIn(r io.Reader, ti *wire.TxIn) error { + err := readOutPoint(r, &ti.PreviousOutPoint) + if err != nil { + return err + } + + ti.SignatureScript, err = readScript(r) + if err != nil { + return err + } + + ti.Sequence, err = readUint32(r) + return err +} + +// readTxOut reads the next sequence of bytes from r as a transaction output. +func readTxOut(r io.Reader, to *wire.TxOut) error { + v, err := readUint64(r) + if err != nil { + return err + } + to.Value = int64(v) + + to.PkScript, err = readScript(r) + return err +} + +// readOutPoint reads the next sequence of bytes from r as an OutPoint. +func readOutPoint(r io.Reader, op *wire.OutPoint) error { + _, err := io.ReadFull(r, op.Hash[:]) + if err != nil { + return err + } + + op.Index, err = readUint32(r) + return err +} + +// readScript reads a variable length byte array. Copy of unexported +// btcd/wire.readScript. +func readScript(r io.Reader) ([]byte, error) { + count, err := wire.ReadVarInt(r, pver) + if err != nil { + return nil, err + } + if count > uint64(wire.MaxMessagePayload) { + return nil, fmt.Errorf("larger than the max allowed size "+ + "[count %d, max %d]", count, wire.MaxMessagePayload) + } + b := make([]byte, count) + _, err = io.ReadFull(r, b) + if err != nil { + return nil, err + } + return b, nil +} + +func discardBytes(r io.Reader, n int64) error { + m, err := io.CopyN(io.Discard, r, n) + if err != nil { + return err + } + if m != n { + return fmt.Errorf("only discarded %d of %d bytes", m, n) + } + return nil +} + +// blake2bHash is a BLAKE-2B hash of the data with the specified personalization +// key. +func blake2bHash(data, personalizationKey []byte) (_ [32]byte, err error) { + bHash, err := blake2b.New(&blake2b.Config{Size: 32, Person: personalizationKey}) + if err != nil { + return + } + + if _, err = bHash.Write(data); err != nil { + return + } + + var h [32]byte + copy(h[:], bHash.Sum(nil)) + return h, err +} + +// CalcTxSize calculates the size of a ZCash transparent transaction. CalcTxSize +// won't return accurate results for shielded or blended transactions. +func CalcTxSize(tx *wire.MsgTx) uint64 { + return (&Tx{MsgTx: tx}).SerializeSize() +} diff --git a/dex/networks/zec/tx_test.go b/dex/networks/zec/tx_test.go new file mode 100644 index 0000000000..7ad3573b9b --- /dev/null +++ b/dex/networks/zec/tx_test.go @@ -0,0 +1,241 @@ +package zec + +import ( + "bytes" + _ "embed" + "encoding/hex" + "fmt" + "testing" + + "github.com/btcsuite/btcd/txscript" +) + +var ( + //go:embed test-data/shielded_sapling_tx.dat + shieldedSaplingTx []byte + //go:embed test-data/unshielded_sapling_tx.dat + unshieldedSaplingTx []byte // mainnet + //go:embed test-data/unshielded_orchard_tx.dat + unshieldedOrchardTx []byte // testnet + //go:embed test-data/v3_tx.dat + v3Tx []byte + //go:embed test-data/v2_joinsplit_tx.dat + v2JoinSplit []byte +) + +func TestTxDeserializeV2(t *testing.T) { + // testnet tx 66bd29f14043843327fae377bd47659c6f02efd3aa62992a6ffa15ddd5fcbaff + tx, err := DeserializeTx(v2JoinSplit) + if err != nil { + t.Fatalf("error decoding tx: %v", err) + } + if len(tx.VJoinSplit) != 1 { + t.Fatalf("expected 1 vJoinSplit. saw %d", len(tx.VJoinSplit)) + } + if tx.SerializeSize() != uint64(len(v2JoinSplit)) { + t.Fatalf("wrong serialized size. wanted %d, got %d", len(v2JoinSplit), tx.SerializeSize()) + } + js := tx.VJoinSplit[0] + const expNew = 5338489 + if js.New != expNew { + t.Fatalf("wrong joinsplit new. expected %d, got %d", expNew, js.New) + } +} + +func TestTxDeserializeV3(t *testing.T) { + tx, err := DeserializeTx(v3Tx) + if err != nil { + t.Fatalf("error decoding tx: %v", err) + } + + const expHash = "fc546132ad9c7bee2a2390ccafc54f29c23bf2f311233f89294665a6c4b7cfa7" + if tx.TxHash().String() != expHash { + t.Fatalf("wrong v3 hash") + } + if tx.SerializeSize() != uint64(len(v3Tx)) { + t.Fatalf("wrong serialized size. wanted %d, got %d", len(v3Tx), tx.SerializeSize()) + } +} + +func TestTxDeserializeV4(t *testing.T) { + tx, err := DeserializeTx(unshieldedSaplingTx) + if err != nil { + t.Fatalf("error decoding tx: %v", err) + } + + const expHash = "fb70397806afddcc07b9607e844ff29f2fb09e9972a051c3fe4d56fe18147e77" + if tx.TxHash().String() != expHash { + t.Fatalf("wrong v4 hash") + } + if tx.SerializeSize() != uint64(len(unshieldedSaplingTx)) { + t.Fatalf("wrong serialized size. wanted %d, got %d", len(unshieldedSaplingTx), tx.SerializeSize()) + } + + if len(tx.TxIn) != 1 { + t.Fatalf("expected 1 input, got %d", len(tx.TxIn)) + } + + txIn := tx.TxIn[0] + if txIn.PreviousOutPoint.Hash.String() != "df0547ac001d335441bd621779e8946a1224949a014871b5f2a349352a270d69" { + t.Fatal("wrong previous outpoint hash") + } + if txIn.PreviousOutPoint.Index != 0 { + t.Fatal("wrong previous outpoint index") + } + if hex.EncodeToString(txIn.SignatureScript) != "47304402201656a4834651f39ac52eb866042ca7ede052ac843a914da4790573122c8e2ab302200af617e856abf4f8fb8d8086825dc63766943b4866ad3d6b4c8f222017c9b402012102d547eb1c5672a4d212de3c797a87b2b8fe731c2b502db6d7ad044850fe11d78f" { + t.Fatal("wrong signature script") + } + if txIn.Sequence != 4294967295 { + t.Fatalf("wrong sequence") + } + + if len(tx.TxOut) != 2 { + t.Fatalf("expected 2 outputs, got %d", len(tx.TxOut)) + } + txOut := tx.TxOut[1] + if txOut.Value != 41408718 { + t.Fatal("wrong value") + } + if hex.EncodeToString(txOut.PkScript) != "76a9144ff496917bae33309a8ad70bec81355bbf92988988ac" { + t.Fatalf("wrong pk script") + } + + if sz := CalcTxSize(tx.MsgTx); sz != uint64(len(unshieldedSaplingTx)) { + t.Fatalf("wrong calculated tx size. wanted %d, got %d", len(unshieldedSaplingTx), sz) + } + + serializedTx, err := tx.Bytes() + if err != nil { + t.Fatalf("error re-serializing: %v", err) + } + if !bytes.Equal(serializedTx, unshieldedSaplingTx) { + t.Fatalf("re-encoding does not match original") + } +} + +func TestTxDeserializeV5(t *testing.T) { + tx, err := DeserializeTx(unshieldedOrchardTx) + if err != nil { + t.Fatalf("error decoding tx: %v", err) + } + + const expHash = "a2a3e99169bad144e490708bbd9d4daf8dc9017397ad11e7a1e75a62f9919324" // testnet + if tx.TxHash().String() != expHash { + t.Fatalf("wrong v5 hash") + } + if tx.SerializeSize() != uint64(len(unshieldedOrchardTx)) { + t.Fatalf("wrong serialized size. wanted %d, got %d", len(unshieldedOrchardTx), tx.SerializeSize()) + } + + serializedTx, err := tx.Bytes() + if err != nil { + t.Fatalf("error re-serializing: %v", err) + } + if !bytes.Equal(serializedTx, unshieldedOrchardTx) { + fmt.Println("original:", hex.EncodeToString(unshieldedOrchardTx)) + fmt.Println("re-encode:", hex.EncodeToString(serializedTx)) + t.Fatalf("re-encoding does not match original") + } +} + +func TestShieldedTx(t *testing.T) { + // Just make sure it doesn't error. + tx, err := DeserializeTx(shieldedSaplingTx) + if err != nil { + t.Fatalf("error decoding tx: %v", err) + } + if tx.SerializeSize() != uint64(len(shieldedSaplingTx)) { + t.Fatalf("wrong serialized size. wanted %d, got %d", len(shieldedSaplingTx), tx.SerializeSize()) + } +} + +func TestV5SigDigest(t *testing.T) { + // test vector generated with github.com/zcash-hackworks/zcash-testvectors + txB, _ := hex.DecodeString("050000800a27a726b4d0d6c27a8f739a2d6f2c0201e152a" + + "8049e294c4d6e66b164939daffa2ef6ee6921481cdd86b3cc4318d9614fc820905d045" + + "3516aaca3f2498800000000") + sigDigest, _ := hex.DecodeString("19e9b271cf0b6f9d60b2834eb16802ce1aa41cd8e96f50bd38dd8f4c4c82616f") + transparentDigest, _ := hex.DecodeString("f31a4521a8d04d8e1f01a614ad0627278c870b7c9a48306cc74587a349dd92b9") + + const vin = 0 + prevScript, _ := hex.DecodeString("650051") + const prevValue = 1800841178198868 + + tx, err := DeserializeTx(txB) + if err != nil { + t.Fatalf("error decoding tx: %v", err) + } + + prevs, _ := hex.DecodeString("223b625f83e3cd9c127ad107fdb5402bfcd5f10681867f2fde8bcfb2aa2447ae") + amts, _ := hex.DecodeString("765008be6611a6e2a6520aa72fb67dc2d944577c26544d924a00d50ea7c58855") + scripts, _ := hex.DecodeString("1e43079ba1fa40d65beee6dd9b77b48017283b16438a852def75a5096cfea9b7") + seqs, _ := hex.DecodeString("17de0287f8f0c573fe74c52d1bd0e6b429cc9201390ededf4a625f5769e41716") + outputs, _ := hex.DecodeString("25f311cc149ecccef0e8ca8c9facd897ef88806008bc15818069470db9f84a37") + txinDigest, _ := hex.DecodeString("bb4ab0249df12a12df3d0d160f9ca6fbb0658c30dd44fd62d237994e77f2c1c6") + + prevoutsDigest, err := tx.hashPrevOutsSig(false) + if err != nil { + return + } + if !bytes.Equal(prevs, prevoutsDigest[:]) { + t.Fatalf("wrong prevoutsDigest") + } + + amtsDigest, err := tx.hashAmountsSig(false, []int64{prevValue}) + if err != nil { + return + } + if !bytes.Equal(amts, amtsDigest[:]) { + t.Fatalf("wrong amtsDigest") + } + + prevScriptsDigest, err := tx.hashPrevScriptsSig(false, [][]byte{prevScript}) + if err != nil { + return + } + if !bytes.Equal(scripts, prevScriptsDigest[:]) { + t.Fatalf("wrong prevScriptsDigest") + } + + seqsDigest, err := tx.hashSequenceSig(false) + if err != nil { + return + } + if !bytes.Equal(seqs, seqsDigest[:]) { + t.Fatalf("wrong seqsDigest") + } + + outputsDigest, err := tx.hashOutputsSig(false) + if err != nil { + return + } + if !bytes.Equal(outputs, outputsDigest[:]) { + t.Fatalf("wrong outputsDigest") + } + + txInsDigest, err := tx.hashTxInSig(vin, prevValue, prevScript) + if err != nil { + return + } + if !bytes.Equal(txinDigest, txInsDigest[:]) { + t.Fatalf("wrong txInsDigest") + } + + td, err := tx.transparentSigDigest(0, txscript.SigHashAll, []int64{prevValue}, [][]byte{prevScript}) + if err != nil { + t.Fatalf("transparentSigDigest error: %v", err) + } + + if !bytes.Equal(td[:], transparentDigest) { + t.Fatalf("wrong digest") + } + + sd, err := tx.SignatureDigest(0, txscript.SigHashAll, []int64{prevValue}, [][]byte{prevScript}) + if err != nil { + t.Fatalf("transparentSigDigest error: %v", err) + } + + if !bytes.Equal(sd[:], sigDigest) { + t.Fatalf("wrong signatureDigest") + } +} diff --git a/dex/testing/dcrdex/harness.sh b/dex/testing/dcrdex/harness.sh index 672a52ed23..c11df2be95 100755 --- a/dex/testing/dcrdex/harness.sh +++ b/dex/testing/dcrdex/harness.sh @@ -54,6 +54,9 @@ LTC_ON=$? ~/dextest/doge/harness-ctl/alpha getblockchaininfo &> /dev/null DOGE_ON=$? +~/dextest/zec/harness-ctl/alpha getblockchaininfo &> /dev/null +ZEC_ON=$? + ~/dextest/eth/harness-ctl/alpha attach --exec 'eth.blockNumber' > /dev/null ETH_ON=$? @@ -130,6 +133,20 @@ EOF else echo "WARNING: Dogecoin is not running. Configuring dcrdex markets without DOGE." fi +if [ $ZEC_ON -eq 0 ]; then + cat << EOF >> "./markets.json" + }, + { + "base": "ZEC_simnet", + "quote": "BTC_simnet", + "lotSize": 100000000, + "rateStep": 1000, + "epochDuration": ${EPOCH_DURATION}, + "marketBuyBuffer": 1.2 +EOF +else echo "WARNING: ZCash is not running. Configuring dcrdex markets without ZEC." +fi + cat << EOF >> "./markets.json" } ], @@ -203,6 +220,18 @@ if [ $DOGE_ON -eq 0 ]; then EOF fi +if [ $ZEC_ON -eq 0 ]; then + cat << EOF >> "./markets.json" + }, + "ZEC_simnet": { + "bip44symbol": "zec", + "network": "simnet", + "maxFeeRate": 200, + "swapConf": 1, + "configPath": "${TEST_ROOT}/zec/alpha/alpha.conf" +EOF +fi + cat << EOF >> "./markets.json" } } diff --git a/dex/testing/zec/alphawallet b/dex/testing/zec/alphawallet new file mode 100644 index 0000000000..01c26e156f --- /dev/null +++ b/dex/testing/zec/alphawallet @@ -0,0 +1,127 @@ +# Wallet dump created by Zcash v4.6.0-1 (2022-01-05 22:18:46 +0000) +# * Created on 2022-04-03T22:03:33Z +# * Best block at time of backup was 403 (0094433b5b3c527ebb052e73e3476b5feb1299919b63e4da4f7355b7e21d6b48), +# mined on 2022-04-03T22:04:11Z +# HDSeed=7402572ee8dc902ac5d0465ac1715f01d778e4213a89dc3a4f8676439fdaa44d fingerprint=66b19768d4e148f847f2dc09c4aa5427116042c0288db17ada1895c6194054a1 + +cW8mjSsvizy5LCZwGd7vmNDghKFwwobp7YEn2fj9KcpKxKXZw555 2022-04-03T22:02:54Z reserve=1 # addr=tmAA8EAagJB9xrjpk5ZQ3zXNQRPWKpK4R2v +cRH3W2C9izkcJqTcTaXZw8R3yc2opZVPUccBuJUy1AEasqYKdFcf 2022-04-03T22:02:54Z reserve=1 # addr=tmAjjCK5ZQGuevUDWGQMw3j56nX5nC6qk8e +cRtM4XEEPtwp15U3QKoEdV2C6nPipwNDrpRgbRcqjUDXudsSLpLW 2022-04-03T22:02:54Z change=1 # addr=tmAvoPy4pt9g1NuCir3t7iCAnosKjmxKeC6 +cUwPr3tr1iVjPVzwA8nPCEjvKRAYy6YFrEYS8WJGnmBZEPZW3Cs6 2022-04-03T22:02:54Z reserve=1 # addr=tmAwtbswHiSjzde3Js9xEYE7VuCqWaaK1Wh +cN19rm5UPn82RsvHTB1iUyNywhQFKQebnd7V6xpaRtzsth7FdgWX 2022-04-03T22:02:54Z reserve=1 # addr=tmB3zQCR1Axyf89Sh3RCAWaPgzYTmXpC9P4 +cTF7jhSpCGGhspVnksY9hHc3vbMZhsWAF4cKEzo4DfF89vwCUfCZ 2022-04-03T22:02:54Z reserve=1 # addr=tmBNGdKy6pzd1FAbecBxGQZkn9b3D1vynKH +cURa3BryVFbVEZgMb1ueuLgEzyB6iuPE2FSjg4sAPE5BdWK75MiY 2022-04-03T22:02:54Z reserve=1 # addr=tmBTXauMf7enmiiBnMf8uwam8H6DDsm4MjM +cPFGThHQzwFm9PapahR5kW2QSnSJ5NpSEfu4tDNn6AEw94GzChKn 2022-04-03T22:02:54Z reserve=1 # addr=tmBXobUh9M917PxrVCbfj7ARJUsuC7NVLUK +cVDuXmhcWWNrjboNuuy5qw9XtP37qZHiBtGgrbk14MXAeo3mC9HW 2022-04-03T22:02:54Z change=1 # addr=tmBa17E5RMDQr9SXu2pqsJE8iFKHReG4ypG +cRw5eHUvvm7yi7ZwQMwTqRyEFx9q2rCVvUJnJLfqx21QELwMjEU4 2022-04-03T22:02:54Z change=1 # addr=tmBdMGEW3YfZeDpQ7ivwtbnCg2EJcCernLL +cQBeK9XgPsNTqqWkio5FNtF9wASZLJ7NkRehYtShM8qkJLTBDkbL 2022-04-03T22:02:54Z reserve=1 # addr=tmBfmMN2AddEYgmRHn5H9fqTfvi5bkg2oQ3 +cRMnoonVsy3Ps5HASwkXGwPC7pn9pHyGChrrmWPNVExtWpiZGE5D 2022-04-03T22:02:54Z reserve=1 # addr=tmBuQv4gi6BM9aAU7FEQYj4ZnSHeh2oCfub +cUwMi9pbacZ4bwW4fVx2vmpgrgycgWy15uJ5Rhym6UHiFrC1f1Jk 2022-04-03T22:02:54Z reserve=1 # addr=tmCFsgiKje5vyFr1WKk6KCMLzah4jkhsw4D +cNLFfMBTahe7vdqDs7jtDDmsUwEiCGE7i7Hr6i7xi1f6btgsKAU9 2022-04-03T22:02:54Z reserve=1 # addr=tmCqaqWme47WU2bQLyufsSBmLzzamRbWXAU +cPRxJbN2urXoqrxrSy7nw2qkbXJsbeovfniiNfdZ2tELgyEdqU9h 2022-04-03T22:02:54Z reserve=1 # addr=tmD49nMtCYwMyFMDVj3DkJhLxPMQ9GTCTv3 +cTWCTq3Yqa8tpN4A6r6UpMgAfpCdXy7F8EWFFneNwPkcVk2mXkG9 2022-04-03T22:02:54Z reserve=1 # addr=tmDFo6yHtafm6qKGGc4crPk5vnbBb7GhshS +cThonMskDcyVSazXbrZ9Qm8HvhL3Be8NG1mnyrjEYThyhgsrvVtW 2022-04-03T22:02:54Z change=1 # addr=tmDUxJ4WLgMZkvRkRPEXqQ3M6fVQhAzbsRW +cVyxQqES65PwkjerfHPdMezXLkBTLntngYdJhRo8ryB6mzqxov18 2022-04-03T22:02:54Z reserve=1 # addr=tmDY2ERgrZ6ZmaVcSyRAWhBhgsuBVyXWFur +cN9yYxGz2p9MzjV5drdp8Kq8yoqS5pV9jvZRxYE7RoV96xfUTuCB 2022-04-03T22:02:54Z reserve=1 # addr=tmDZfpMxVXypRNGxLFBpFkzeLDV7Qju3tnH +cUiCxbU3z8etb23aLZ5hh1DpPWVpYxiRCePgZVLe6QLcFMvMCd59 2022-04-03T22:02:54Z reserve=1 # addr=tmEGdarBdBmH2WLWjMDeYVt6gcCoS5Sbmnj +cQc5MDXhaQZHLm2NuD3kMveugCCsNpHdWvHxGRXvt8ByCtvCz3AD 2022-04-03T22:02:54Z reserve=1 # addr=tmEJ2QFb17brkqavPAGrCoUej5AhGmbVQ36 +cSJ6WNgj1WeaB7gucPkeTa6wmgthvbzk9LLLhm4WHncf1BcnYgZt 2022-04-03T22:02:54Z label= # addr=tmEgW8c44RQQfft9FHXnqGp8XEcQQSRcUXD +cTenFH3HMdRgMXv6HvfJrseEfjheuZjAAeqCMx1KVKxJ9z7wamFG 2022-04-03T22:02:54Z reserve=1 # addr=tmEnweqHrA6CVwsmDNve1FLFGU6XvBtyXPD +cP1yZjeYAt2BmAFFvSdhjbo3GivrG3onp1gmX9xYyqBqs1wp6ggG 2022-04-03T22:02:54Z reserve=1 # addr=tmF8MGYfae9trjG56hLXoMB6vXaJHX98ERa +cPTRWFUBT5GpReLVK4JyPXQRuMt38mB1uwmNhJsfXGpxuDdrpMzi 2022-04-03T22:02:54Z reserve=1 # addr=tmF8kkE9YRFJG1Zq2nLS6hsjZpNbpEx2aDy +cNXmGYfsAe4Eh8hkxTjQTtjaNMejLeUvgMudapL5RinPbtth7LPx 2022-04-03T22:02:54Z reserve=1 # addr=tmFHhn5JarW11rKxdANjS3WvViq1ga56LJY +cPaJBh25JsBanS2yFwJZBs2Lz74ooKQccA5sFvkMwCvwBrQ7ma8Q 2022-04-03T22:02:54Z reserve=1 # addr=tmGAN8qPhZdr6ETvE6odsWB542XHPDpJKJS +cVG7GThr557XWME4omQ2sQnaKsZwfXUUEJ99eMwL87Yft9wvy19U 2022-04-03T22:02:54Z reserve=1 # addr=tmGAU5CuAJgD2cDUhPJkXEi4FvgRYJk13va +cSSwR9g1hoLmfwKN361vJ8Xnr2pXNr5Pb5msg4KfuqRMFdGjrwBY 2022-04-03T22:02:54Z reserve=1 # addr=tmGUTXkxvqQzvXbXyvFFgxJFvid7hLt6STh +cPhCU4yATWDSuVRaFKPGqESDwzjh1MqqDRRBwLnK9HGHx7VcPw2K 2022-04-03T22:02:54Z reserve=1 # addr=tmGqU5Jc6kEnPa4kcYvNKkPNVix9UcBN8YS +cSwACDBNaJRqqxQ2vy11t3H1ydTHfuKtEsSwfj86MSfXBSowwoGV 2022-04-03T22:02:54Z reserve=1 # addr=tmH6pNCbPbTWp28XaiHHzVWE8kwFpWJg19U +cP83Tybq6adbJYA7Siv8kgmXFULAPip7LJvnYr71jZEM7eGQegYL 2022-04-03T22:02:54Z reserve=1 # addr=tmH8y9qYj9eAHUqvz77bmByfijDjpGW3kfy +cVDRYmDStXQAL5bV5TojYT9F5uAqVVteRnnqSUuZ5rr3C9EEF5Zn 2022-04-03T22:02:54Z reserve=1 # addr=tmHFoJ98K8AvAQJnFwrtKZUq7AAZhHpuLfh +cVFwGfAAt6dYP9KahWW9aNfj4gSX6bVdNYshusF9mFkCT5KNSa1o 2022-04-03T22:02:54Z reserve=1 # addr=tmHdYoaTbREiiXTxYTsbwR7fW6GTsu7RUKH +cP9Loksj3wZ8AA7KD5sya2sQrc2rYiP2joTZZCxtA6k41BRxWw2y 2022-04-03T22:02:54Z reserve=1 # addr=tmHrLeirRusUKM72T2m6ebtCcwquyuvG9wm +cVfBhZyYP1ANSXVMpdYPndfisvLGrPAVDpTbjVvvrJRd8UvCFwxt 2022-04-03T22:02:54Z reserve=1 # addr=tmHyxQZFcDsQQGKKg8iZybnfDbphXePcTMM +cNVARopBb4JdNtoa4xFPRv8Xd2j5xJBs6BjgoCNYtD4Jd4zjM5Xt 2022-04-03T22:02:54Z reserve=1 # addr=tmJ5JiMV5ncQUedbbxjKzvzmRaWEWyJYXzB +cTykEmDL4oPkFaumwPXkaZykRuHdsDCNFiwcS9HeQjrD554Bu2yM 2022-04-03T22:02:54Z reserve=1 # addr=tmJKnrz6RmXrYcxmbJLZmtVcktg4s2PJoec +cUWjKyLkroXKP9SAMmwZ7puuUgbjXR4fALB9coubVsyd9Mzrxrdw 2022-04-03T22:02:54Z change=1 # addr=tmJPesPqXAXEAHKNSeJgArGxT9b5adz5nwX +cR5PjChC28GqUcaKP4oKVDrpbLJnCkudZ6UiReCi8sJisrWS4Mo3 2022-04-03T22:02:54Z change=1 # addr=tmJVvX463nQZYEnwXitEVpzjEQWtA1eobzU +cQAprNjZ9RzeTny7tV9vCRHd7uvwKfbZByDVaV4aUUddtddfimqM 2022-04-03T22:02:54Z reserve=1 # addr=tmK3xa7sS84CKG9HhzsSr3NKDnEgtiyoWjA +cRvs6eT8pxdsF4Fnnbz47EM1GYQ276E5yJNEYdhZhidKca3t8PUa 2022-04-03T22:02:54Z reserve=1 # addr=tmKLG9XLugdWk6QnCRQ7j5EUkwCVmR1qHni +cSWwxvXEYg7kPsL3RpPzB5RGe5TM5uU6nVm7bM1HkTAmcLMe6wjn 2022-04-03T22:02:54Z change=1 # addr=tmKbPNG3dSa9zj96nHUNTjwuS4c5m4eMH2u +cRxk4wP4UbibV57c5pYvKUUm3LsopnmFonXJg2iGstCTbpAeGhSi 2022-04-03T22:02:54Z reserve=1 # addr=tmKbe3qYNT2cEsfChjEKZyEBxvJKeBsTi3S +cRcVCoQnoe6yAmQDSz7XzYTCvfU2AX4cfLrWbTzzS9E2hCQc6vpB 2022-04-03T22:02:54Z reserve=1 # addr=tmKbyEY1udWXG4qdjgiw5WAcsKwELFxfffD +cNQda7UoGxuDnQJ1PY6XndX1ZBXNfpDnLEuWiksjtAVdLjxvoLtS 2022-04-03T22:02:54Z reserve=1 # addr=tmKwkSPXM6xtfcksGcjNpyV5v7mVovK8EUQ +cQyapgkpcH1fEmRqbzHDjxQ5kTGxTaSQGNKmUKnUfDUo7kHcZqra 2022-04-03T22:02:54Z reserve=1 # addr=tmLp5oj651yYNnQGWHqApzPS4JUq6r5MeCg +cVzRVuz3kjfzKwMJu3s3HrfJJmX91pb21g1ZYBrdYHURZtZaqyaZ 2022-04-03T22:02:54Z reserve=1 # addr=tmLuuaa2cSkAkX6dPFgXA8SBDr2dEUhsboD +cQN819PrVEqrmztELFChRNXxETnc4rNwN4CtquhXyNVV4pRkwYcS 2022-04-03T22:02:54Z reserve=1 # addr=tmMJn55GKrbqmwumHX5iCKGNkNHQxzqAU3q +cTqZNCgC2HPw5Trb8MVLDxQJ7frfDJZtbxeJ3Vza2v2Gg5Ru9Hk3 2022-04-03T22:02:54Z reserve=1 # addr=tmMP7EJLqkTLC7LmCPJvwXrPZe9NhiQ2kpX +cUNc6gFEHJTPRSfYyVTKXxDRwQ8VJWpYK3qdbyNV6BQVEzNo4ZMt 2022-04-03T22:02:54Z change=1 # addr=tmMfEW2UsK3H9q9YHcWZAXawCeEMG4HQA4M +cQ1oxLWQy2zzo89bdVnPWyQjJVp16fYDQmJR7SWJoyuDfYY4yMyh 2022-04-03T22:02:54Z reserve=1 # addr=tmMkXqFEW6X6A88Qo1Vrh8r8ktw9z2MxGrd +cRN4gTh37LihJE71AC3TFZYU3M1nBqHoD2VcN4iojeiqqWMi8XjG 2022-04-03T22:02:54Z reserve=1 # addr=tmN5Nw4fTbYknEnSoF3ZG9oLZ9E8VxbancN +cPw4ZmD8JusZ9uno6pEao9Mu5bMkVBufHk9odFNRs2Vz5S4XSa21 2022-04-03T22:02:54Z reserve=1 # addr=tmN6EPLAK2wn4cDMViMiE4psZaSJk4Gw1Up +cT7jQH5HzkC2KFJRmhrxChTBrrDC7bTnuTzAmQoRX5PvEAEyMMc4 2022-04-03T22:02:54Z reserve=1 # addr=tmNAyDETvVq5fMTRxnJ9SGvvXzU3UecCFQU +cW8hj2jSjZZuYFMt4Q3TvEWZ4FToCg7H7H4Y7udYm2HFbnhMZKwJ 2022-04-03T22:02:54Z reserve=1 # addr=tmNMwJt9v3wKcNuwkeMP4vuCmCh8QHVRLQq +cPPDFqxQFqp9pNzfWfJYp2S6x3avVRoP8waVapXorUgoMUjp8TvW 2022-04-03T22:02:54Z reserve=1 # addr=tmNRWbqRRzEqP8gofn5axY7C9ekA5MobMC3 +cVmwnLreqb6xtUummcNo6sjLViceXtXUyci9SmKP7KBiiDNugjZA 2022-04-03T22:02:54Z reserve=1 # addr=tmNYcvxiZiPBDMm6xHBkyDiSVXLWX6HigTz +cR5pLd8Lh1b5qboadwvsBr9hdYiDt56zBygTbpUcDm2cUxZyxAKD 2022-04-03T22:02:54Z reserve=1 # addr=tmP8L2mSiqRxFPKYutjT74mqYcExAKrJoQu +cToaMRbvDn9AtRpQi5m5iCTogXJFAwe1knx2NhCAhKPLWJ2aRdnx 2022-04-03T22:02:54Z reserve=1 # addr=tmPNwbeiuNhueomYuSUFHNJPiBRxErawsTv +cNMGF4rdNW4afxGAxDnUtW9RsyiKF8xsmuJEMefpUkKKDJTvEwMs 2022-04-03T22:02:54Z reserve=1 # addr=tmPwEhfz5aafYnzfmVqTdwPtMUR5nNLrfvw +cQiAM8hneQJCcodzE3wrehz1WLRunEatFABeCLMh1UZxeozKyWv4 2022-04-03T22:02:54Z change=1 # addr=tmQ58YFBNYsyAvLH9ZaDkuPNk9B6ab9Q83Y +cNRbMxZCkKY17oC9HEu5zdrTE1Fd7jucvN4ZdPRf1t5jsyz5EnSr 2022-04-03T22:02:54Z reserve=1 # addr=tmQJhoPGxYqUQoD8uvf68Kv6WCG4FussCdm +cTg9dowAEB8VQ5iXwCGyDwDGs9EVgTSXEBj9qbgciWTrqkN9C8pR 2022-04-03T22:02:54Z reserve=1 # addr=tmQM54VJ6sejNJn7XJv7GPn8WekDgYC8dcm +cPsoEp63xyE8Hr4c9doYpiWzskifk3tZEnuJug4MfZJmgS4g5xJz 2022-04-03T22:02:54Z reserve=1 # addr=tmQmrjGvwbXy6La5w9J8xBhBTefs2vyD1Hc +cMceYDqyqKEYZ2yHcxytBy6oKN97BtWTzzZyq6EUVi4ESfd7zLet 2022-04-03T22:02:54Z reserve=1 # addr=tmR3PxCfppNfPKoC3fEVSrWs2vpfr28bA1D +cQHouYPVZ8pnbavq9SovZXMjtaS4xt2wsrYHtrZVx52bNti3Yrh1 2022-04-03T22:02:54Z reserve=1 # addr=tmRHTN4wtSWHnWZcHxo7QzaKr5xdqCTsZkj +cUyYV1MZZD79X7G8nhsEnrm4c71SrwLetDewZHL584u9w9qpwHmp 2022-04-03T22:02:54Z reserve=1 # addr=tmRHju4agpVLyTSdTmpwMBhL4b2vJjk4pfR +cUz7mR1CayKGPmRyWAftZFKTL9LGHbSZkxDURcVdGtDGkp9adT8d 2022-04-03T22:02:54Z reserve=1 # addr=tmRSi1tGWniD9PFMdkhYBWeg8kb9UH875Ke +cPArp4NPU8S2U3tpxddHpr7S89EpAPJStzfU67axXFZ1dKoqfTRE 2022-04-03T22:02:54Z reserve=1 # addr=tmRn6caFFiv5gHE7QwDNKqrnJDFmKM52kTD +cQ3Bf7eXhu4DuFywiYmDSJzR13uGuw9bbsjmktmMQ1MhxbGZMwLV 2022-04-03T22:02:54Z reserve=1 # addr=tmSHkZZKPWNGf69GWhfGD2kL55VFVvMVw9K +cW9v3DL6KxHtgCraUKp1SH9HoRxhbvCt2CAXMDRXWBYVWPiCevNF 2022-04-03T22:02:54Z reserve=1 # addr=tmSU1c8dSkb1y1KkTVNmn1mtUVuwHwA2cRe +cNTFSPt98GhfVZEisqSDJxfvnWX9U3V73FrSWecVjdzKRU2GEFmJ 2022-04-03T22:02:54Z reserve=1 # addr=tmSkq9kQN1SAxu8MSPsbX9Jmv8J4QCNGQk3 +cPJvNpFNvRFA73tb951nBkSj7LprQB3b5EmUhwLzRaEiS4C1v46m 2022-04-03T22:02:54Z reserve=1 # addr=tmT5jhwLkTaF1VCovcQeJNpxsvgSHjvs7Yw +cQ8QzTWWKmzvy7zw2MrX6vRiqRktW4C6rmxc9DGdrBdfdm5yt1y2 2022-04-03T22:02:54Z reserve=1 # addr=tmT6PPEAcLuuBmMMtaX992b6F4qARV7wech +cUak75nEh3BAos3m5Lkw6LJ4gVnwZrEnYz44rA7tUwgx4fmfTKwo 2022-04-03T22:02:54Z reserve=1 # addr=tmTAFdF6oA9FTG7dmvecSjsndjFautfqFjK +cPqmTsLqBMJ1uMA7U2KPQLrNJNdBFi1akQKbnHJVw7gAQctF9nUD 2022-04-03T22:02:54Z reserve=1 # addr=tmTpovCbDEpWbjcwNEPQiMXytifjDZ1F9mg +cP4zoxfKJdr4fTeXopb37DKXvGMin2RzeSrrTGFWiGr9snqendYL 2022-04-03T22:02:54Z reserve=1 # addr=tmTtkFRyT1ri3NW1p6dqqx55YAhaGLsVcv5 +cV1CqZuANrHsmbD8GaCZR396fAqZJYCxNxQi2cdGGEfaqWTPzC77 2022-04-03T22:02:54Z reserve=1 # addr=tmUf3j5E3YPdj4LjytywWvLkjaWB4apmAYP +cQinX5tdQ6HcQqXdGKBtNBi8T5vPpVL2cwPxnraTzu4tbptfu3rk 2022-04-03T22:02:54Z change=1 # addr=tmUk1Uy16VnyznoQssRcpegaJZfoch6Pjhy +cQANuv3St8Pbmhr3S2NJzBv3w8T2ydxZuk3qMhJHqJxoV9exu4Nj 2022-04-03T22:02:54Z reserve=1 # addr=tmUtYEAno2es2ZysQuy6SLKmwnUVPozyPK7 +cNCtnm1MAr4hfHs2irvMpajwE9HiRuNbEyBioemzEfGZVYzTZsrb 2022-04-03T22:02:54Z reserve=1 # addr=tmV31s5jkddcGVpaECoipbpqQ36zvC1QsUA +cQJYCUeUNC7NmdCcNCmxDhSSQ8TZmSqQ5GmnxxFfyBHpUX6k6zhT 2022-04-03T22:02:54Z reserve=1 # addr=tmVG3LTZEq8p2ajq87CUFMHzd2Gog2vnWaK +cV9T8GChtp17vsJfx3x5XNyJw5w4AXcsXWDA4WqG7iezHgz2s73n 2022-04-03T22:02:54Z reserve=1 # addr=tmVMLbQnH6UXC2H1rqTsVXkcM5QoGV46Xn7 +cVNTQpHJrxMB2MXZB8joTrHhBhgscxyXiwNApRPYde1pak1ufkbj 2022-04-03T22:02:54Z reserve=1 # addr=tmVQCR4x6hrrrbGTRu1KG8f3A33CiMcazua +cUUbvZ3Y6FNHrv5ZhzH941mgEadrRKTd3ASD9EAZX1PJ8wfzMxyE 2022-04-03T22:02:54Z reserve=1 # addr=tmVR1UgDBuPDk8q2p7zDeYzxWBJ3zdYcwFb +cMyX85X1Wt7rvHaeFZURXNHXf153ZQgkBphh2ULXbTvFjgvurqc6 2022-04-03T22:02:54Z reserve=1 # addr=tmVc1Juc5yRFUaKmcSLnz9vpK9dqZgFtz5o +cPKGpnfzBGzFJrKQYsB6BR7i3ppHsNYz1nc5oqYzsrJvsMNqghF4 2022-04-03T22:02:54Z reserve=1 # addr=tmVeM73HCT6zaR5TGxbEYiDXGXwPhVmT6BQ +cNa5gYmpaNaiE6sJneMDrKYvi67K2Uum9VUFdSg5jbdTWZY4eZHe 2022-04-03T22:02:54Z reserve=1 # addr=tmW9mwdM9brCdiuDCcBAB6Y6DFqnjq3p2nU +cQURPoWzFbyTCdZLchrBMSsFQqRBjeXmWiieHiPb2AxNbatmLqhZ 2022-04-03T22:02:54Z reserve=1 # addr=tmWWSApp4RqDrASSrkBvbwVjPmnx7uRiHWw +cShhpmJ179ufewWFKsoPB1LvMumc1wVkqL2bfEnLr1mqWmifBNDD 2022-04-03T22:02:54Z reserve=1 # addr=tmWgGY1raemBq7G5KYhzE9ZroYDzQPXpK8d +cNKyE2wPX86PSDdQvJDonfYvs56S8MzaqFuwFL7HxaYPSNX6JS7i 2022-04-03T22:02:54Z reserve=1 # addr=tmWpwpTDDeZ6j7MEJFMgpHMJjaadN37cewq +cPHPH8T7CxtkvYepgs6pdmv2VDozdqKh5XUZJZudC73TwQU6xrGK 2022-04-03T22:02:54Z change=1 # addr=tmWrVzgga41Rg9TBZ1TujhCu84a6T5N17bx +cUGYuTYq8KeVEs5eXKQcwQPiZVJcSczBHZPhhuf1QFtwUrDCYsRf 2022-04-03T22:02:54Z reserve=1 # addr=tmWsJQLQxoBWZCjNEwCrwAdUUJyZTg2Tai1 +cN7P6WqaJ1oB9pMSRrxkEN89rS6RSiHnviLpQJpyYmxi2ucB8CRF 2022-04-03T22:02:54Z reserve=1 # addr=tmXGa46SVQD6W1sCETkoMkiyZbRSrYbjYkW +cSXCijBWsb3TJRrp6Bw5cBCH38zF8oC8ATCfsF39QtZ3oeTKG9cU 2022-04-03T22:02:54Z reserve=1 # addr=tmXWmuJdkaULebBLpoAf7gCZ3KpJT8FamyJ +cNGiziWAqtimfbfezrisZf6uLfsuBm424wvmkJ1KHupj9bgu9Bzs 2022-04-03T22:02:54Z reserve=1 # addr=tmYXd2Vn4cFeE18aHqUYGhGE1E7zvAWNGpH +cQn7pLNDDnYec82LYpb8oiAe9dW1AuwNgB4iYhNA1UV1KUgnfw6N 2022-04-03T22:02:54Z reserve=1 # addr=tmYbgLjZDfhuVQuohx44faksJQBXcPGzEfW +cS7Qka8xXo6e397hpsq1N1GGJykDN4jZfCwANtAA9z2Ezk7w2kR6 2022-04-03T22:02:54Z reserve=1 # addr=tmYnPTa156K3LSkAANvSBQiekgpVsQk9ouv +cRwUCaax9wACbfWrXh7ERN5Pz7cKuzJrsgjDpLwzBM6JcRrmkCdw 2022-04-03T22:02:54Z reserve=1 # addr=tmYo6jx6RgLBHurk4MqVAYoXtwGqP1sNoHz +cPjEAYSKcdnoaF8mvqS32WyyUU4saznPN7zX3JrwFu3fiTov5QVp 2022-04-03T22:02:54Z reserve=1 # addr=tmYy9HWJckHVQMgxkj7ZaTpYimdx9ys11jM +cPTZcs779aReZc44QEgaJ2LcVnhMLuMyyC5tMnzfigpjvGxn3zSC 2022-04-03T22:03:03Z reserve=1 # addr=tmMwvwbFYDQ5t9Qo5JioENDBpWdZQ8q72om +cUPuFKauSkzYj4mqc73Zzyky3Gj4n8Wk9rGS1Waart6wp8BuVV7S 2022-04-03T22:03:04Z reserve=1 # addr=tmGRpJgUnqWWF5axLHMEJBnVmNYyrFR8QbK +cRfPVHfETtgePJ2iyKe84UzLMPsrMimPxSsS16wUVykpq3QnoSey 2022-04-03T22:03:09Z reserve=1 # addr=tmBwA8bzTdg3duxFySbed4NUGJHadsp2Gcx +cULrhoisWPyyg9MjnCq85bHP3nZHfmZQcy6wWagqDZ9kNo9SkRPe 2022-04-03T22:03:09Z reserve=1 # addr=tmCWvnnKj3RhQuzvKb5VyDuRubAMynPs4f8 +cUHAVVjhtSqJWPuRYkvvxwgQFFrZrn1QG86u1ohg6uTnXrM37J4G 2022-04-03T22:03:09Z reserve=1 # addr=tmFKkcB2zPaFFtaVnqwzZqtuBGsGQzh88Hg +cRL5zrap6J9tFr6Kx83GZTxuJsNvoGY7wKhHtzegsP2TNX3bsaVS 2022-04-03T22:03:09Z reserve=1 # addr=tmHwSjJ86E1jJejATvTfRoiAZswvb7JTzEB +cN3HTS2i1ySUVNp3ghn25MsDy2wFyRc7J9Y3oDfCkXcTcGXb5ArD 2022-04-03T22:03:09Z reserve=1 # addr=tmNSgJExYz5YWE7cunhcizpXCj37hrZysvk +cVyqiEQsoFudgkFNoNC1aFzNcq1ZcUErPFWunisc2QohuLutuBjc 2022-04-03T22:03:09Z reserve=1 # addr=tmPWSQ11UmYMBn6fxU3fE7dWSM5DucgHwnY +cTZfj8oUeWDMR8iRAjuVjrzTXA8AtHLFCofCLqVxv8WZyXYMhuiN 2022-04-03T22:03:09Z reserve=1 # addr=tmPyx7WjcomsVLdU4wjUkRAE4XdHAMJjas7 +cS9u1Erc6FnUBRgEGjkRxXBcfiLgg5ba43a5HtTBkPmN3akHJ1QY 2022-04-03T22:03:09Z reserve=1 # addr=tmXNbm4SMKdmRxpVrtU8gVVR54euw3F26LD +cPkuiiGhRoCa9ow7pDRqt7sWGm7tJQnF8WymzUn43BYTnPHGG6uW 2022-04-03T22:03:09Z reserve=1 # addr=tmY5obWQEPcmCByufJsHjtLZ3rzzBATRJRw + + +# Zkeys + + +# Sapling keys + + +# End of dump diff --git a/dex/testing/zec/betawallet b/dex/testing/zec/betawallet new file mode 100644 index 0000000000..7c05a10c7d --- /dev/null +++ b/dex/testing/zec/betawallet @@ -0,0 +1,116 @@ +# Wallet dump created by Zcash v4.6.0-1 (2022-01-05 22:18:46 +0000) +# * Created on 2022-04-03T22:03:49Z +# * Best block at time of backup was 0 (029f11d80ef9765602235e1bc9727e3eb6ba20839319f761fee920d63401e327), +# mined on 2011-02-02T23:16:42Z +# HDSeed=f86ff289bab2f517c91b64f6aa293049e00d46aca8b3d3fa40f3e88080d71c76 fingerprint=b68f17e81209624e6f893f864230f462029e2e1279e296425fddb3a68a1b22c4 + +cRZJ5tNTmrh7pBmocdnHXS11wyaa7vLaqsunq9cNBpNAkUvmiBhz 2022-04-03T22:02:57Z reserve=1 # addr=tmAPrZAY8yNA17GhEcEPMUEi5e1ovn2iymA +cPhm1QdjC1AWXM4nZFzpJgXXabmKRGR4r58Ek6Sw34H6xBMwZ67w 2022-04-03T22:02:57Z reserve=1 # addr=tmAU3FAJYwNxTaCmxCEaT9Ju3EcumbRKAoX +cSRiiXVoduhjzqSGva24XdvivZS48mRVcQzUUpTY4BhdC52Ubi4C 2022-04-03T22:02:57Z reserve=1 # addr=tmAg1oeQxfCtp8mbSQpBknm7Wm95AZdyDzb +cVoSn4kZaF4NeG1112tYd4cuGZPL4ZLGwoT9hpBuNz8B3XtXAKJx 2022-04-03T22:02:57Z reserve=1 # addr=tmApSq8VWAEAN6ZT6CpPEyx5ZzBhcZiRgGb +cMwJYCPV5ooVp5RDVYbqxVSKrg6GbhkAcgtJRK8PKCULU1YMmu25 2022-04-03T22:02:57Z reserve=1 # addr=tmB4EdVvH3EV9DkhzpPJxKe6874ZMvuWrHd +cUuDYpxSpJPDK6X6UUJWGVcVSsdCeyY6YQAyKUiyFjLFsVzrmzGs 2022-04-03T22:02:57Z reserve=1 # addr=tmC4a4UjsfJF3Vufo8W4QTxuaUcqgqU6vk7 +cMshriZVNtVRcvxrNXjyDLfqsEDUTjB5CAttofJXLbFrMZNcmguZ 2022-04-03T22:02:57Z reserve=1 # addr=tmCFjaKUNdim8c5azTZNRaaoZ3HaX8ad7CT +cTqJHPVdb7PVCDziLE9souEZYRrbSCTqzoxs6qozc7m5Ze87DD6j 2022-04-03T22:02:57Z reserve=1 # addr=tmCQijuQa2gNnztyHHS5kD4uFxSutzRYbuY +cW1AQCKa9RaSpvgpPdPUdddBVHj6kdjhWM93QpjFuCqz9wH7fJKj 2022-04-03T22:02:57Z reserve=1 # addr=tmCUL9VKoUuJwn9iLbrTAhVHkA6YJgJYXmW +cTNEiRFjMsBC9BFTxMeeJ611G2WZHdiykc7mjbVDdxg1j21qoe7i 2022-04-03T22:02:57Z reserve=1 # addr=tmCbPCqWFxkZq8sTYWjxZeZgf7yRFG5kf1Q +cUJzatcStMo4eqneDxxgTWoAv3v23HZdrQvVXir3riM8mGHD9qKY 2022-04-03T22:02:57Z reserve=1 # addr=tmCcRjtCv6AJA6PHPb3oCkwbSAZr3JaQhn4 +cTmY6B5t5mKgBGK3daxwMRqbqnGPp81fjsKNHsNn5pTUhGKBYzZh 2022-04-03T22:02:57Z reserve=1 # addr=tmCffBCXRdZqGjRx4bKoJsZ9UXZB51W73e4 +cRngQgdXuwUGD1XPs8dkGhwj1PnnWU1uK4wxVZjzGk9GH6UmmobN 2022-04-03T22:02:57Z reserve=1 # addr=tmCmbUzXDS8U5xoAvybE6uUgnZ3o39UwTsN +cMqVi9taQUuEvsBdd89ag2fw6QCRJNVH9gcsvCvFKFEYP71J74fk 2022-04-03T22:02:57Z reserve=1 # addr=tmD3J8c2AsNVpPFR78X1wMaGbbrptPsuS5P +cSkb7qvHJAn76kjv4ypztwNa7uLxHbtte4sbpePj2XccDraP3Mi4 2022-04-03T22:02:57Z reserve=1 # addr=tmDWMj4xZRA1ytiu68TNCAEP7t1Bm4YYzpN +cUACMSTrRaKwmP8ehZKnnPt1yyJkAcZhcXEQZrDWRxseBSUWfBq1 2022-04-03T22:02:57Z reserve=1 # addr=tmDkaaFvxCXLQiRQAz7fmLBZZnxo6ddFXb9 +cPRHSfszCWaJpp7uSpTj8howUhdQVwKykgXAtgGrE1G4PKPvzpLe 2022-04-03T22:02:57Z reserve=1 # addr=tmDztU3VeygScwzPyTyf8e9GVJKEpkskxNE +cQAR5eJZbcQLuM8NfK9FaFypiXg55aF6qR7ij7TNMwm1MfYYtyyy 2022-04-03T22:02:57Z reserve=1 # addr=tmEBgSVrAB2XiCaUENZA1yn6r9meLXYtJUK +cNUoumj2144mquh1Y1ZYTJjuJb4UqW1Bgs7w1sAvfFXCPK65vNWX 2022-04-03T22:02:57Z reserve=1 # addr=tmEJWfmP2BDCbGST8YzVcCMuU9mHTAwDRnw +cVaJN4zeA9Pfxp3QLRQ6pkR2ddzH41qycz6NxbAnCWhfzc6QPrjJ 2022-04-03T22:02:57Z reserve=1 # addr=tmEMFMnJ91f5oapSmcDSPbxnB76RGKE2EFw +cPPgw5apkFjhnvFtvTHq3C12eEQXoDwRh3wS8egQzqdmCKi7PUiw 2022-04-03T22:02:57Z reserve=1 # addr=tmEMhmiW9be74v49wbN5hWvwCqp8bu7DXu2 +cPjKLVWUjouAnHMiEomX7ZP93fbU2u3e1eg8ByAmmvaZdFXkjmuL 2022-04-03T22:02:57Z reserve=1 # addr=tmEPABF6mGrB3YkS97nQ3X4hC5DjdanuRhA +cQEgPV7qv7ofdFFLR78nk71rZKiL27hSxZWofqh3rWCoyiShZ6ZK 2022-04-03T22:02:57Z reserve=1 # addr=tmEgQUztvhoa7ffmD4EoSBMVmLwTUEeadiP +cRd2DW5BL4exYpLtGVNkdTPSjoFg32B78ps57TiBDArEUiaazcNt 2022-04-03T22:02:57Z reserve=1 # addr=tmEpF167DGMbNriSn5PvNyMo3AWTH49DxcU +cUtugmP3StBL2XpZ67yG6jo16FjX3zaGwixk3tNDBzjaAMgLQSUu 2022-04-03T22:02:57Z reserve=1 # addr=tmEurBtdUAwQp5TEwApbTqSyJGLR2WtEYPv +cST6ptgJe6erLS8NW6t3qC7PsZQmvKronve9jpqthutREWrC45co 2022-04-03T22:02:57Z reserve=1 # addr=tmEvJi3FhvUSTXW5B77dbFyg7Z9NVyMge6E +cTSFyETdpHnRoqyvn229sCnvWTCgAuCo1JGZ8VVYu1RS2fHNVynG 2022-04-03T22:02:57Z reserve=1 # addr=tmF89Z4qFiqNK65vG5LzYdn9NVXrj3eXHF8 +cRK6W4D1keVh3U9xx5wonZ8aL1RJUr3qjzk7QSYPuca2p1znrnwf 2022-04-03T22:02:57Z reserve=1 # addr=tmF8FqVDyvSSVAimws2i4a2Rti9swPoSjzE +cUBEFN8WdcENwF3tSWxAHmq1WEjGba8R2d5XaWVYEGkRccwCjYRH 2022-04-03T22:02:57Z reserve=1 # addr=tmFBSmYfZoiD5rPUJ82mYPu7UKy5h5cm8tt +cR7u39dtKpXADdhqYRm8McEaCEecQYf4SziwnjEQtMY8JSsPNaHV 2022-04-03T22:02:57Z reserve=1 # addr=tmFMHVrmsXoDptH3qqxsdL79RUeL95WxA3i +cS6Y29PuXF4HGeaf6LCbxxi7fQgsj22JQYaus1Hj69VjcanhN6Qw 2022-04-03T22:02:57Z reserve=1 # addr=tmFRUx5DveF52fPmdS6zwKAAmPeUxvT2wgw +cVr2jm87tsocSs6YebhubLZKFQqSQTT3VXgzCWMSX7C5xyc1HH8K 2022-04-03T22:02:57Z reserve=1 # addr=tmFescFArSoZnd2b9n3d3P8ACHwu2yYVXS2 +cTy4Bdq4XM3mAEVrEYbDozQF6RyhUVRUKTAnGybTSLABZCzaSRZe 2022-04-03T22:02:57Z reserve=1 # addr=tmFfDoCdrZPX1o11BhNt2ceFjLueY9CX4TX +cTxDtMupF5RhkLWw7eA7MeGCXusp7QBhKbgpYaRLjC5ceTD1GR5M 2022-04-03T22:02:57Z reserve=1 # addr=tmGfDffbdaXNA2zePJwE1oXErdFnxywbMiL +cTuqRyN8TJpjz6y9Mvcj1ypoXd6ouERgewxtXvVXna9pY1PuXSTy 2022-04-03T22:02:57Z reserve=1 # addr=tmGrLmkBqjZy3d6grpu24JtswuLZDTL8r9p +cQm5zaH9R8DgatEoZoXmpcBifUR45prPLPBCZbHMbanaQTCixEnp 2022-04-03T22:02:57Z reserve=1 # addr=tmGrvTUs12KpcGokZq5p8NGn4GhawxEYdw2 +cPaEyryuzet4iLRTWXCYTbFjmvEvYThi5DQex3o3SM2xwU1HZ8Mk 2022-04-03T22:02:57Z reserve=1 # addr=tmH4bSUJYs5UtgVBFGfek18y3h45RuE1ZXo +cMdGYHcVTFE9G5z57aPmZcGhbQz8E6G2nEy6ChDajph4mPXXKfFY 2022-04-03T22:02:57Z reserve=1 # addr=tmH4e72psTuD6KskVpVzRrw3ZDL7P6m6ch1 +cSderWsaNNcwJQ8EFcoW7sezK4AzNJy74oqqaMrppssYA1zh3ayA 2022-04-03T22:02:57Z reserve=1 # addr=tmHRe7usLgZU77wzzisV9WzhuU6JLZm9kYy +cP4UGYNo8F7t6iMe5RtyLqLeWPgEzS6L1G9YwnZ1VYm9ABB2o3Y5 2022-04-03T22:02:57Z reserve=1 # addr=tmJ4fsf5qKy6kKw7kpT26sd157ujoGRKGa3 +cTJwAcgpR9nXPa4veRRkn8FyJiuQuDVsgVNE6fPyu7RjcLGm97dm 2022-04-03T22:02:57Z reserve=1 # addr=tmJNxoqmHHVKgkcyZqqikqYgESodWJH849N +cSCpuauhK9VbFjFWo3iQB89qwVnBBKDb1RwprBdGD3SMviWv1BJM 2022-04-03T22:02:57Z reserve=1 # addr=tmJSrAWQy4J5cmb585MRqwHCDcmrSwsDYmj +cUUsvvnLeuyVPP59fYyjbrmxGC4ov4sDGxjGYVJ6WjCJ1n8aGRiy 2022-04-03T22:02:57Z reserve=1 # addr=tmKMFq6sUjcNFyX2hL2BBQ6cQCp5X9svCEN +cUWVYvu3gRcvAyt8jqbmGzeNVLoePGAMvXitroy6mHQ97C2sRiDx 2022-04-03T22:02:57Z reserve=1 # addr=tmKpVsqCoW4XfCvSwz4Tfwg5qoEyvPf5deK +cNj5iGbie8Kgfrwc63xefncEGKxwRsYq5uNqkkNhNxkTxFRedevU 2022-04-03T22:02:57Z reserve=1 # addr=tmKqDF8SdfHorbGgoLXsSZSdK4YVaZQKpyc +cSmiv7Q9aTKcwtCatQnrnjtn1uGSDCbwgSESduUqPyhRUx533vNU 2022-04-03T22:02:57Z reserve=1 # addr=tmKrfvJjw5tTbUwMWdELg5oJ4TMe3QK5f9L +cSCTKVTsyRxWLpLYzizAJW4ogHpquiohWPuAujopBtFxV7G3sm1j 2022-04-03T22:02:57Z reserve=1 # addr=tmL23jCoidKwZysCWEs1ube6Dv6JG6xT1WE +cV4F8D4DG3NBhNW8F8Uz4a9bcXBrc7MWQoVdeqkhwhYWa5aEhWPh 2022-04-03T22:02:57Z reserve=1 # addr=tmLfUyPQN7it8WtvN7qobv8HD8ij9g1dxjv +cP42Gd6BfkBXhXFiNHUQTdNv9Cm5GdtgzXVqT8SFWw2jJRwdPBd7 2022-04-03T22:02:57Z reserve=1 # addr=tmLhnuhBSB36DPhupssEM6k5fAbk3VkNyFq +cTghSxkSqXt262Px67cKXyWyY4bwDYeY1NdzXR9Zx6ZxxuQM8VJC 2022-04-03T22:02:57Z reserve=1 # addr=tmLpfk9i91Sq1QQnGRDZyV3DRUEE7e4J8rM +cUoV9V2W66Nf1iHVAYRYgWrkwzxnPWveMgZ39P4pYxTS2MzYM6oN 2022-04-03T22:02:57Z reserve=1 # addr=tmLq4wL1jAeYfvkUz6qxQMxaiGJwKqfcBop +cVCCTexykRqDStPHhyWhw1whvTSL6dehFDYHCyC67jZUgUQMBc1q 2022-04-03T22:02:57Z reserve=1 # addr=tmMdmxgxDrSsRsbJmB7YwivpFobRufEHzLr +cTFicLKBudiKTJwHVBBXpUsRZ7Arban34LjKPdB8qoFdXianaUT7 2022-04-03T22:02:57Z reserve=1 # addr=tmMpB8Rjt21tX9wuxqxFASH1wVVdTxAKv6H +cUm6mtizhDNSjM4xx79Y9P7PNvtJWWGRJerQpT6dbvdDyY4NAfyy 2022-04-03T22:02:57Z reserve=1 # addr=tmNEvuGmayDnu5LCkUomMRi9aSEFxDKnnLp +cQX17a45cMDJRRC5o9CZGxShJjTwWcT9TUk3REnPkA6mxnVcMUWH 2022-04-03T22:02:57Z reserve=1 # addr=tmNagoUJ4zdiY4V2yox6hXwR5CAeQDS8jrB +cRuPpshQ2KrNX6Jv3m94iZMmNawDvggnfCqWNDgDn1sRYAop4WJN 2022-04-03T22:02:57Z reserve=1 # addr=tmPS3H2qTpEhD1W5ezPnsPsaQZ1DSPVmzJP +cSAdjyEcAiEqKJy5ubnAvP8F6fqZCTJCF8rdUSAXbXAiSLTSZphe 2022-04-03T22:02:57Z reserve=1 # addr=tmPTZR915oVSqj6xVULHne9TyNmhmNH9faB +cMuexCTXB8qdYgX6KQiGoPJDvdKg1Sqq6j4AFkakLDNDdyc6haLT 2022-04-03T22:02:57Z reserve=1 # addr=tmPqBX9ThS9vnX8LcvyPsDPi3kyesD3aimH +cS7Nj3jNvc2cmNP7qUirAqkX2k6fpPYsnPuPyyqGC9Jxs7dZxuvw 2022-04-03T22:02:57Z reserve=1 # addr=tmPqF7sZYpaBZgSK9EAT4FrdkvEGepcGf7H +cNPPvHMMWPic8Tw2ywP8Jr9PCYWFFMPXDsZepWqd8AnuFaRT7Ym8 2022-04-03T22:02:57Z reserve=1 # addr=tmPrVvWTtmv8VPz3ysG7JZVQH6sRCkhjpfJ +cPZch5z2Br19QvaVnezjqjMtaEJXZnJ6Cu9M1XapAfu5MMgP1aNy 2022-04-03T22:02:57Z reserve=1 # addr=tmQ63JGxzzqTvD6Hp1BEL2p8FtbmaM4Xmmh +cU48FgCMNAUei9dtJezpX8btk3HGUUNDrVPzboiCMzgNaP1rv4Hu 2022-04-03T22:02:57Z reserve=1 # addr=tmQCVay5Li72X1SQ3kdHDLXDSfDsf9Ggei4 +cRjGaPV4duL3dvvk8AAhtbA6fwKZGAkujkZpRzPJfsHB3pPrHt6P 2022-04-03T22:02:57Z reserve=1 # addr=tmQKXwhbPK8MbT4P3gVtZXvR6vYq56mKuzB +cRfg7ev9Ci76NroSnTepVAHAERbJnNiEHF5PLkKsa6DftT7AaoxA 2022-04-03T22:02:57Z reserve=1 # addr=tmQUe5D685yknFvvdQXKVczwLd1U8v9JD4v +cR9pDzD843TfcqzxV3eUaDDuVad7Bgk8vw6c8FELuQLWzqXt837z 2022-04-03T22:02:57Z reserve=1 # addr=tmRSw7PhfAXgcKexu7VpwRgjoESEijw26gx +cW7CWUuFR5UNhNmXQebsVpwiQ2E73eC8KXYyHMeog8SXk5dGdBNm 2022-04-03T22:02:57Z reserve=1 # addr=tmReBF7qKFgveMxZk32LR23cVBSbJ9aGuFJ +cMjcJgkqBDaWtgc2FJWXab5gpbM5aAVWFCB4KUKaEw7acxJWCSvJ 2022-04-03T22:02:57Z reserve=1 # addr=tmRp9mpe6NihxW358G7ahSz6BAy1wWAMGQU +cNLwZCqrQCz2tnDYSwaD6oGcgeB6f2SZXK7pf9B9E6FcED5yo8vG 2022-04-03T22:02:57Z reserve=1 # addr=tmRrTu67TW4dLqQcGFAjfyYTKqMpMM1uECd +cNEjFQYW225o7zqfnYLE9nhHMoZx5FUZSDCo5tkzpWbgHZxDtUia 2022-04-03T22:02:57Z reserve=1 # addr=tmSLyKVrWiWramrPk8RukTPQPST8R2Tug3V +cSRHfnYzJZebZSrgUXkRJqP9PAL1uCtkB2NkpdDPAjfqMUmAeFy9 2022-04-03T22:02:57Z reserve=1 # addr=tmSSA5GL7twFNc2dSp3eNVz7aXvahMahPiE +cUFWyd5ZL8obydrZC7vj8WB1hij9VfJ7pMTPHh1nVCHYMZFkGET4 2022-04-03T22:02:57Z reserve=1 # addr=tmSVAQkrUMeYpz74ag6pTiY842QX8Lcxgr1 +cUh3EF7hqnAvFGasfvA6dP5yYq4s1fZnwp3bxEA7h1geD5FstPG9 2022-04-03T22:02:57Z reserve=1 # addr=tmSeC4xLaMkhDTQznL1AAXFLm7zxBugQray +cPH8DQfinWjDSoQCFZcZVqzywyPUJ2URsmhQfs9TJaf2hKMk2Cio 2022-04-03T22:02:57Z reserve=1 # addr=tmSmp8k6oeMzLQWnJfsxUDDKSHUPmb7AS8o +cQ88otpvKN9zbF1RvEbSeyzabtNVD312fFvT73PQE2mrMNAXmVmW 2022-04-03T22:02:57Z label= # addr=tmSog4freWuq1aC13yf1996fy4qXPmv3GTB +cQrKmb7VPpucB6bP7jWd3aVngwBPG9pDivbjtn8thnYpCqnf5634 2022-04-03T22:02:57Z reserve=1 # addr=tmSxGbpzUEqZcHapKJ9GSEEaf1jatE47hwb +cT7Vh7zJ2NsXBFBD78JnuzKqrohjAAwf1yhnEJ8kGHbduPjXke3W 2022-04-03T22:02:57Z reserve=1 # addr=tmSyT5Dxx6pZYeJNRvdVPZtQ5tgrwvNqUZG +cTPeLG4ioSJbB5RzibBJDCTaCKexeiRUcZZXpydLuYLSAXfD1i8e 2022-04-03T22:02:57Z reserve=1 # addr=tmT2rfhsHHPJoUAwMYExwthCYivGfC5XrKS +cVVUGsz9SQdRTM13KM2NXZmhiL1FY28cGHmwma4FKahuoeEMvgRj 2022-04-03T22:02:57Z reserve=1 # addr=tmTUKn7XNyeoHi6mT7jffSJYhN29hcwZDmt +cULKHaBmvzj7MDDMLve75vftewRMZ5ZXY2hARLkXiCcgfTaD6kHa 2022-04-03T22:02:57Z reserve=1 # addr=tmTVS1cRozATfHfyced4WNYxtS31L259CyT +cR5bhndrNd8jCdpRmbtwidz3Ngq6UP7A1WbVbpsday5VSCt2PLuJ 2022-04-03T22:02:57Z reserve=1 # addr=tmTZj7Prt2tNk5CGrLAbHLPdv5JoLhMXg3u +cTjWS8xEeUEDMMKPtKJxC5MAUBX22sCVRb49zWyP87m7SAWssSEW 2022-04-03T22:02:57Z reserve=1 # addr=tmTnJaQL5GiStQy7JS9ZWLmz4diSJmSvCoZ +cTqiLMRHuQYsnATQfJc1mjeyZKbF22JTzbyEodoW2ypcArKP1FHf 2022-04-03T22:02:57Z reserve=1 # addr=tmTyAUi312BjSKPC1qSG5eRBPJZmzBbLpJP +cPhJUfMQ3E4M2b6MznE6hrcGmm82nxmjugHLuKygtDqmrsUJnySC 2022-04-03T22:02:57Z reserve=1 # addr=tmU479XGjx2EvH3LT1rkqGuHWFzfeDkxxhJ +cTK4yNhrih1zPoLDDMBBJhbx9CESo8AgfaqSxEhiTjEpdMd6PebX 2022-04-03T22:02:57Z reserve=1 # addr=tmUUzpFnsvRBpnMjRggCXUzmcCxMZtVFVFF +cVt2jxgZ49M3zzLceKsDouQk1mXzvXNNbHrT1HdsN8jwLmGBjBp2 2022-04-03T22:02:57Z reserve=1 # addr=tmUexJCpj3UNYZVKzPq8XQ5QMVFXRagnXGE +cSsHBKs1v5kmi8et1cJG1zdfsdY7ordx8AK65PgPcXoE6Lnx3xWf 2022-04-03T22:02:57Z reserve=1 # addr=tmUqCVYxCvUVqGAxKjXG9QXn3cUSBPbTHQH +cTYjum9xPgAjyCWg3xkTGSj3HyTPupCbqgzNmcacWv2FSGjrxAmb 2022-04-03T22:02:57Z reserve=1 # addr=tmV3dJd3gfeYK2bCMxjfEQYkvhXg4ah1PQG +cUsCFXHMCgf6gpCnpT3m76S79TruJ2HKKc7vrRLNJ6dBhkeiKMti 2022-04-03T22:02:57Z reserve=1 # addr=tmVKCAnexWnytY3K9RZyLtDTTzbyFAdRHGs +cQQzRidiEog2UYXd9Pyyg5yuDpbKh4LxzQsqyET9arcAGphHDJLg 2022-04-03T22:02:57Z reserve=1 # addr=tmVSrJjZvzLAFzF1WzWJSDdXSVjmwkheLwW +cQW1eWaqiyxPdi7a2zps3CPsnjjaApA7Hf1L5hfAbg1dX1pLzAVG 2022-04-03T22:02:57Z reserve=1 # addr=tmVx6AfdYsdArACBydJDzAMrTt6qZTwrKvv +cS5X93aEP6tenqbc3YABKPZgyqC1QVev92tjznv5F4pLf96Wo1or 2022-04-03T22:02:57Z reserve=1 # addr=tmW4gJYJpHkE27s67QSMAZYHXKWX7vLDWk2 +cNV9avCNKd3Khz5znyh96NWaEbn2jnJ2wCccw2tRVivoSzTRESQn 2022-04-03T22:02:57Z reserve=1 # addr=tmWjrTaR3ot7oxHi92bZNLKPS42ccrULQR4 +cPU58Lbcq6Yu9D7yZ7KK69UFEtJ4T3Wu2hX62iZeoVmMSDKQmFCy 2022-04-03T22:02:57Z reserve=1 # addr=tmWp4ak2vb8JBcT9xp7asSh74abTusDe3ka +cTM51sFbtaLhcHG49BrehZrHv5YdssooQQCrMD5SENXVwtFXuPSL 2022-04-03T22:02:57Z reserve=1 # addr=tmXGG44EaNHZgzBedKP7xug5WDsyTrcMqK7 +cRRRxpwbUevexZMn89MinJGCgYGdmijQzP98qKwxtHi74N3wngLk 2022-04-03T22:02:57Z reserve=1 # addr=tmXJVxV4f96S5kQjas7Km1WHGuSFaG8shKr +cSjLAaNA51nBHuKZL4ExYSrn624rrwDAMbasqcYt9bdwzkTbFfnD 2022-04-03T22:02:57Z reserve=1 # addr=tmXnXQsQeh9NzzTYx1x6AwCsUjX8S3ZnBZZ +cVS1saNAryV7LSfg4zicSYAzpYgz8CbadfPD8djKSgL7m6ii5k8o 2022-04-03T22:02:57Z reserve=1 # addr=tmYNcuTFSLE5sNdim74xjaUcuVtje1tpw3e +cVnLG2S3RFv6n5GqCXyZ8xgd72yfUZ5ictQ2C81Vjf1bR2ydWF9H 2022-04-03T22:02:57Z reserve=1 # addr=tmYj99meWN9iGpRL63gH8ksMT9ASxWjG92R +cQH6qCMioMSKBSJe5MP8v9b1kd1ty88ddUbK3FZuoDz8pwSjiAh9 2022-04-03T22:02:57Z reserve=1 # addr=tmYoaf11GqJc3wvfYgf4dAbMVaDter2BngF +cMfp8GQcs4xyXqpcEaddBDUN1HyFmjdJ7PGREXEHkJxKZJyEFAxa 2022-04-03T22:02:57Z reserve=1 # addr=tmYwWmc7M9L9wmQ7jcU5ZRvHpDTuMDoQTBG +cS7mPWVW92YyboFQq1rJMYPjskdNtY8y5Cee73uWEZiiMU9qZ5yn 2022-04-03T22:02:57Z reserve=1 # addr=tmYyEXrBWJTaFGysM7gaZ2Eb2dYxzCaHZHF + + +# Zkeys + + +# Sapling keys + + +# End of dump diff --git a/dex/testing/zec/harness.sh b/dex/testing/zec/harness.sh new file mode 100755 index 0000000000..2cf6778630 --- /dev/null +++ b/dex/testing/zec/harness.sh @@ -0,0 +1,322 @@ +#!/usr/bin/env bash + +# IMPORTANT NOTE: It can take the beta node a little bit to get caught up with +# alpha after the harness initializes. + +SYMBOL="zec" +DAEMON="zcashd" +CLI="zcash-cli" +RPC_USER="user" +RPC_PASS="pass" +ALPHA_LISTEN_PORT="33764" +BETA_LISTEN_PORT="33765" +DELTA_LISTEN_PORT="33766" +GAMMA_LISTEN_PORT="33767" +ALPHA_RPC_PORT="33768" +BETA_RPC_PORT="33769" +DELTA_RPC_PORT="33770" +GAMMA_RPC_PORT="33771" + +set -ex +NODES_ROOT=~/dextest/${SYMBOL} +rm -rf "${NODES_ROOT}" +SOURCE_DIR=$(pwd) + +ALPHA_DIR="${NODES_ROOT}/alpha" +BETA_DIR="${NODES_ROOT}/beta" +DELTA_DIR="${NODES_ROOT}/delta" +GAMMA_DIR="${NODES_ROOT}/gamma" +HARNESS_DIR="${NODES_ROOT}/harness-ctl" + +echo "Writing node config files" +mkdir -p "${HARNESS_DIR}" + +WALLET_PASSWORD="abc" + +ALPHA_CLI_CFG="-rpcport=${ALPHA_RPC_PORT} -regtest=1 -rpcuser=user -rpcpassword=pass" +BETA_CLI_CFG="-rpcport=${BETA_RPC_PORT} -regtest=1 -rpcuser=user -rpcpassword=pass" +DELTA_CLI_CFG="-rpcport=${DELTA_RPC_PORT} -regtest=1 -rpcuser=user -rpcpassword=pass" +GAMMA_CLI_CFG="-rpcport=${GAMMA_RPC_PORT} -regtest=1 -rpcuser=user -rpcpassword=pass" + +# DONE can be used in a send-keys call along with a `wait-for btc` command to +# wait for process termination. +DONE="; tmux wait-for -S ${SYMBOL}" +WAIT="wait-for ${SYMBOL}" + +SESSION="${SYMBOL}-harness" + +SHELL=$(which bash) + +################################################################################ +# Load prepared wallet if the files exist. +################################################################################ + +mkdir -p "${ALPHA_DIR}" +mkdir -p "${BETA_DIR}" +mkdir -p "${DELTA_DIR}" +mkdir -p "${GAMMA_DIR}" + +# mkdir -p ${ALPHA_DIR}/regtest +# cp ${SOURCE_DIR}/alpha_wallet.dat ${ALPHA_DIR}/regtest/wallet.dat +# mkdir -p ${BETA_DIR}/regtest +# cp ${SOURCE_DIR}/beta_wallet.dat ${BETA_DIR}/regtest/wallet.dat + +cd ${NODES_ROOT} && tmux new-session -d -s $SESSION $SHELL + +################################################################################ +# Write config files. +################################################################################ + +# These config files aren't actually used here, but can be used by other +# programs. I would use them here, but bitcoind seems to have some issues +# reading from the file when using regtest. + +cat > "${ALPHA_DIR}/alpha.conf" < "${BETA_DIR}/beta.conf" < "${DELTA_DIR}/delta.conf" < "${GAMMA_DIR}/gamma.conf" < "./alpha" < "./beta" < "./delta" < "./gamma" < "./mine-alpha" < "./mine-beta" < "./reorg" < "./new-wallet" < "${HARNESS_DIR}/quit" < 0 { return satsPerB, nil - } else if err != nil { - btc.log.Tracef("Estimatefee error for %s: %v", btc.name, err) } btc.log.Debugf("Fee estimate unavailable for %s. Using median fee.", btc.name) @@ -1166,7 +1226,7 @@ func (btc *Backend) estimateFee(ctx context.Context) (satsPerB uint64, err error // If the current block hasn't changed, no need to recalc. if btc.feeCache.hash == tip { - btc.log.Tracef("Using cached %s median fee rate", btc.name) + // btc.log.Tracef("Using cached %s median fee rate", btc.name) return btc.feeCache.fee, nil } @@ -1178,7 +1238,7 @@ func (btc *Backend) estimateFee(ctx context.Context) (satsPerB uint64, err error } if err != nil { if errors.Is(err, errNoCompetition) { - btc.log.Debugf("Blocks are too empty to calculate %d median fees. Using no-competition rate.", btc.name) + btc.log.Debugf("Blocks are too empty to calculate %s median fees. Using no-competition rate.", btc.name) btc.feeCache.fee = btc.noCompetitionRate btc.feeCache.hash = tip return btc.noCompetitionRate, nil @@ -1250,3 +1310,12 @@ func serializeMsgTx(msgTx *wire.MsgTx) ([]byte, error) { } return buf.Bytes(), nil } + +// msgTxFromBytes creates a wire.MsgTx by deserializing the transaction. +func msgTxFromBytes(txB []byte) (*wire.MsgTx, error) { + msgTx := new(wire.MsgTx) + if err := msgTx.Deserialize(bytes.NewReader(txB)); err != nil { + return nil, err + } + return msgTx, nil +} diff --git a/server/asset/btc/cache.go b/server/asset/btc/cache.go index d9ec1ff63e..1fbdd030e9 100644 --- a/server/asset/btc/cache.go +++ b/server/asset/btc/cache.go @@ -7,7 +7,6 @@ import ( "fmt" "sync" - "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg/chainhash" ) @@ -57,7 +56,7 @@ func (cache *blockCache) atHeight(height uint32) (*cachedBlock, bool) { // Add a block to the blockCache. This method will translate the RPC result // to a cachedBlock, returning the cachedBlock. If the block is not orphaned, it // will be added to the mainchain. -func (cache *blockCache) add(block *btcjson.GetBlockVerboseResult) (*cachedBlock, error) { +func (cache *blockCache) add(block *GetBlockVerboseResult) (*cachedBlock, error) { cache.mtx.Lock() defer cache.mtx.Unlock() hash, err := chainhash.NewHashFromStr(block.Hash) diff --git a/server/asset/btc/live_test.go b/server/asset/btc/live_test.go index a071985573..1f524c9da5 100644 --- a/server/asset/btc/live_test.go +++ b/server/asset/btc/live_test.go @@ -43,7 +43,9 @@ package btc import ( + "bytes" "context" + "encoding/hex" "fmt" "os" "testing" @@ -60,16 +62,32 @@ var ( ) func TestGetRawTransaction(t *testing.T) { - hash, err := chainhash.NewHashFromStr("79275472daabf4e79ef4dea9402841e61743e0080d4dd26a74819cfd64acadf9") + expTxB, _ := hex.DecodeString("010000000001017d2cc6700c20cb64879eab415a0f3ba0b" + + "d6c51bb2d95e3cc09a8f24c52d0335d0100000000ffffffff02ac2c15000000000017a" + + "91431ab6a773ae3e4c857dea6db96ca513a96369f27879364f60100000000220020f52" + + "e2cb266ee5919630b79207a3c946f655267eff4a1d3d829a17b5804320649050048304" + + "502210088128c387df6642fe56275722dc2a700535eeab88ef1a5ca077815ff538b83e" + + "4022010dc3cb58bac19613b69345fed923ff204b36d84d3c325df3ba63f5bab42198b0" + + "147304402201bb828e28c5e91fa9af8c80451ee390006f10eded73156d4e7b71937d32" + + "76ef102207e28c40aba395b5962d8fb1bfbcf4008888ac44b4c1b85827965ef0c69759" + + "e4701483045022100f2c4a38556127f879fe205e7d325a445033e413dbac5fec99e858" + + "133b1f37e2602201cd0cde48637beb036b507ee21b387103ff9d93751b6aac51ed4d02" + + "77fff78ef018b53210288f5e79b55ab76d7c263628fad475bd4cc4cee466acced9b41f" + + "d3a5cb71233a22102b976a06ed75f1931a303f587ccb2b9f18fe07c9d6aaceadb353ef" + + "945579e93d52102d5c87f884d6b828e9786d49b4cee87c72e589b14e1668f4dcd40dd2" + + "25be16a8621035eca61acb63c8738d5bccd57fb6544ca77dbc13593ce3e6754f8db563" + + "05bf16b54ae00000000") + + txHash, err := chainhash.NewHashFromStr("79275472daabf4e79ef4dea9402841e61743e0080d4dd26a74819cfd64acadf9") if err != nil { t.Fatal(err) } - tx, err := btc.node.GetRawTransaction(hash) + txB, err := btc.node.GetRawTransaction(txHash) if err != nil { t.Fatal(err) } - if got := tx.MsgTx().TxHash(); got != *hash { - t.Errorf("Got tx id %v, expected %v", got, hash) + if !bytes.Equal(txB, expTxB) { + t.Errorf("wrong tx bytes") } } diff --git a/server/asset/btc/rpcclient.go b/server/asset/btc/rpcclient.go index 77c48ff98e..8d9d569b6f 100644 --- a/server/asset/btc/rpcclient.go +++ b/server/asset/btc/rpcclient.go @@ -6,17 +6,14 @@ package btc import ( "bytes" "context" - "encoding/hex" "encoding/json" "fmt" "math" "math/rand" "sort" - "strings" "decred.org/dcrdex/dex" "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" ) @@ -45,15 +42,22 @@ type RawRequester interface { WaitForShutdown() } +// BlockFeeTransactions is a function that fetches a set of FeeTx, used to +// calculate median-fees manually. +type BlockFeeTransactions func(rc *RPCClient, blockHash *chainhash.Hash) (feeTxs []FeeTx, prevBlock chainhash.Hash, err error) + // RPCClient is a bitcoind wallet RPC client that uses rpcclient.Client's // RawRequest for wallet-related calls. type RPCClient struct { - ctx context.Context - requester RawRequester - booleanGetBlockRPC bool - maxFeeBlocks int - arglessFeeEstimates bool - blockDeserializer func([]byte) (*wire.MsgBlock, error) + ctx context.Context + requester RawRequester + booleanGetBlockRPC bool + maxFeeBlocks int + arglessFeeEstimates bool + numericGetRawRPC bool + blockDeserializer func([]byte) (*wire.MsgBlock, error) + deserializeTx func([]byte) (*wire.MsgTx, error) + blockFeeTransactions BlockFeeTransactions } func (rc *RPCClient) callHashGetter(method string, args anylist) (*chainhash.Hash, error) { @@ -149,23 +153,39 @@ func (rc *RPCClient) GetTxOut(txHash *chainhash.Hash, index uint32, mempool bool } // GetRawTransaction retrieves tx's information. -func (rc *RPCClient) GetRawTransaction(txHash *chainhash.Hash) (*btcutil.Tx, error) { - var txHex string - err := rc.call(methodGetRawTransaction, anylist{txHash.String()}, &txHex) +func (rc *RPCClient) GetRawTransaction(txHash *chainhash.Hash) ([]byte, error) { + var txB dex.Bytes + args := anylist{txHash.String(), false} + if rc.numericGetRawRPC { + args[1] = 0 + } + err := rc.call(methodGetRawTransaction, args, &txB) if err != nil { return nil, err } - return btcutil.NewTxFromReader(hex.NewDecoder(strings.NewReader(txHex))) + return txB, nil } // GetRawTransactionVerbose retrieves the verbose tx information. -func (rc *RPCClient) GetRawTransactionVerbose(txHash *chainhash.Hash) (*btcjson.TxRawResult, error) { - res := new(btcjson.TxRawResult) - return res, rc.call(methodGetRawTransaction, anylist{txHash.String(), - true}, res) +func (rc *RPCClient) GetRawTransactionVerbose(txHash *chainhash.Hash) (*VerboseTxExtended, error) { + args := anylist{txHash.String(), true} + if rc.numericGetRawRPC { + args[1] = 1 + } + res := new(VerboseTxExtended) + return res, rc.call(methodGetRawTransaction, args, res) } -func (rc *RPCClient) GetBlock(blockHash *chainhash.Hash) (*wire.MsgBlock, error) { +// GetBlockVerboseResult is a subset of *btcjson.GetBlockVerboseResult. +type GetBlockVerboseResult struct { + Hash string `json:"hash"` + Confirmations int64 `json:"confirmations"` + Height int64 `json:"height"` + Tx []string `json:"tx,omitempty"` + PreviousHash string `json:"previousblockhash"` +} + +func (rc *RPCClient) GetRawBlock(blockHash *chainhash.Hash) ([]byte, error) { arg := interface{}(0) if rc.booleanGetBlockRPC { arg = false @@ -175,6 +195,14 @@ func (rc *RPCClient) GetBlock(blockHash *chainhash.Hash) (*wire.MsgBlock, error) if err != nil { return nil, err } + return blockB, nil +} + +func (rc *RPCClient) GetMsgBlock(blockHash *chainhash.Hash) (*wire.MsgBlock, error) { + blockB, err := rc.GetRawBlock(blockHash) + if err != nil { + return nil, err + } var msgBlock *wire.MsgBlock if rc.blockDeserializer == nil { @@ -196,7 +224,7 @@ func (rc *RPCClient) GetBlock(blockHash *chainhash.Hash) (*wire.MsgBlock, error) // separate because it contains other useful info like the height and median // time that the wire type does not contain. func (rc *RPCClient) getBlockWithVerboseHeader(blockHash *chainhash.Hash) (*wire.MsgBlock, *btcjson.GetBlockHeaderVerboseResult, error) { - msgBlock, err := rc.GetBlock(blockHash) + msgBlock, err := rc.GetMsgBlock(blockHash) if err != nil { return nil, nil, err } @@ -211,12 +239,12 @@ func (rc *RPCClient) getBlockWithVerboseHeader(blockHash *chainhash.Hash) (*wire } // GetBlockVerbose fetches verbose block data for the block with the given hash. -func (rc *RPCClient) GetBlockVerbose(blockHash *chainhash.Hash) (*btcjson.GetBlockVerboseResult, error) { +func (rc *RPCClient) GetBlockVerbose(blockHash *chainhash.Hash) (*GetBlockVerboseResult, error) { arg := interface{}(1) if rc.booleanGetBlockRPC { arg = true } - res := new(btcjson.GetBlockVerboseResult) + res := new(GetBlockVerboseResult) return res, rc.call(methodGetBlock, anylist{blockHash.String(), arg}, res) } @@ -293,6 +321,76 @@ func (rc *RPCClient) medianFeeRate() (uint64, error) { return uint64(math.Round(float64(weight) / float64(txCount))), nil } +// FeeTx is a representation of a transaction that 1) has zero or more previous +// outpoints to fetch, and 2) given the requested outpoints, can report its tx +// fee rate, in Sats/byte. +type FeeTx interface { + PrevOuts() []wire.OutPoint + FeeRate(map[chainhash.Hash]map[int]int64) (uint64, error) +} + +// btcFeeTx is the FeeTx for a standard Bitcoin MsgTx. +type btcFeeTx struct { + *wire.MsgTx +} + +var _ FeeTx = (*btcFeeTx)(nil) + +// PrevOuts returns a list of previous outpoints for this tx. +func (tx *btcFeeTx) PrevOuts() []wire.OutPoint { + ops := make([]wire.OutPoint, len(tx.TxIn)) + for i, txIn := range tx.TxIn { + ops[i] = txIn.PreviousOutPoint + } + return ops +} + +// FeeRate calculates this tx's fee rate. +func (tx *btcFeeTx) FeeRate(prevOuts map[chainhash.Hash]map[int]int64) (uint64, error) { + var in, out int64 + for i, vin := range tx.TxIn { + prevOut := vin.PreviousOutPoint + outs, found := prevOuts[prevOut.Hash] + if !found { + return 0, fmt.Errorf("no prevout tx %s for %s:%d", prevOut.Hash, tx.TxHash(), i) + } + v, found := outs[int(prevOut.Index)] + if !found { + return 0, fmt.Errorf("no prevout vout %s:%d for %s:%d", prevOut.Hash, prevOut.Index, tx.TxHash(), i) + } + in += v + } + for _, vout := range tx.TxOut { + out += vout.Value + } + fees := in - out + if fees < 0 { + return 0, fmt.Errorf("fees < 0 for tx %s", tx.TxHash()) + } + sz := tx.SerializeSize() + if sz == 0 { + return 0, fmt.Errorf("size 0 tx %s", tx.TxHash()) + } + return uint64(math.Round(float64(fees) / float64(sz))), nil +} + +func btcBlockFeeTransactions(rc *RPCClient, blockHash *chainhash.Hash) (feeTxs []FeeTx, prevBlock chainhash.Hash, err error) { + blk, err := rc.GetMsgBlock(blockHash) + if err != nil { + return nil, chainhash.Hash{}, err + } + + if len(blk.Transactions) == 0 { + return nil, chainhash.Hash{}, fmt.Errorf("no transactions?") + } + + feeTxs = make([]FeeTx, len(blk.Transactions)-1) + for i, msgTx := range blk.Transactions[1:] { // skip coinbase + feeTxs[i] = &btcFeeTx{msgTx} + } + return feeTxs, blk.Header.PrevBlock, nil +} + // medianFeesTheHardWay calculates the median fees from the previous block(s). // medianFeesTheHardWay is used for assets that don't have a getblockstats RPC, // and is only useful for non-segwit assets. @@ -304,7 +402,7 @@ func (rc *RPCClient) medianFeesTheHardWay(ctx context.Context) (uint64, error) { return 0, err } - txs := make([]*wire.MsgTx, 0, numTxs) + txs := make([]FeeTx, 0, numTxs) // prev_out_tx_hash -> prev_out_index -> value prevOuts := make(map[chainhash.Hash]map[int]int64, numTxs) @@ -322,21 +420,15 @@ out: return 0, errNoCompetition } - blk, err := rc.GetBlock(iHash) + feeTxs, prevBlock, err := rc.blockFeeTransactions(rc, iHash) if err != nil { return 0, err } - if len(blk.Transactions) == 0 { - return 0, fmt.Errorf("no transactions?") - } - - rawTxs := blk.Transactions[1:] // skip coinbase - rand.Shuffle(len(rawTxs), func(i, j int) { rawTxs[i], rawTxs[j] = rawTxs[j], rawTxs[i] }) + rand.Shuffle(len(feeTxs), func(i, j int) { feeTxs[i], feeTxs[j] = feeTxs[j], feeTxs[i] }) - for _, tx := range rawTxs { - for _, vin := range tx.TxIn { - prevOut := vin.PreviousOutPoint + for _, tx := range feeTxs { + for _, prevOut := range tx.PrevOuts() { prevs := prevOuts[prevOut.Hash] if len(prevs) == 0 { prevs = make(map[int]int64, 1) @@ -353,7 +445,7 @@ out: } } - iHash = &blk.Header.PrevBlock + iHash = &prevBlock } // Fetch all the previous outpoints and log the values. @@ -362,11 +454,16 @@ out: return 0, context.Canceled } - utilTx, err := rc.GetRawTransaction(&txHash) + txB, err := rc.GetRawTransaction(&txHash) if err != nil { return 0, fmt.Errorf("GetRawTransaction error: %v", err) } - tx := utilTx.MsgTx() + + tx, err := rc.deserializeTx(txB) + if err != nil { + return 0, fmt.Errorf("error deserializing tx: %v", err) + } + for vout := range prevs { if len(tx.TxOut) < vout+1 { return 0, fmt.Errorf("too few outputs") @@ -378,24 +475,13 @@ out: // Do math. rates := make([]uint64, numTxs) for i, tx := range txs { - var in, out int64 - for _, vin := range tx.TxIn { - prevOut := vin.PreviousOutPoint - in += prevOuts[prevOut.Hash][int(prevOut.Index)] - } - for _, vout := range tx.TxOut { - out += vout.Value - } - fees := in - out - if fees < 0 { - return 0, fmt.Errorf("fees < 0 for tx %s", tx.TxHash()) - } - sz := tx.SerializeSize() - if sz == 0 { - return 0, fmt.Errorf("size 0 tx %s", tx.TxHash()) + r, err := tx.FeeRate(prevOuts) + if err != nil { + return 0, err } - rates[i] = uint64(fees) / uint64(sz) + rates[i] = r } + sort.Slice(rates, func(i, j int) bool { return rates[i] < rates[j] }) return rates[len(rates)/2], nil } @@ -432,6 +518,7 @@ func (rc *RPCClient) call(method string, args anylist, thing interface{}) error if err != nil { return fmt.Errorf("rawrequest error: %w", err) } + if thing != nil { return json.Unmarshal(b, thing) } diff --git a/server/asset/btc/tx.go b/server/asset/btc/tx.go index e85b122bd5..23a2a3cd2f 100644 --- a/server/asset/btc/tx.go +++ b/server/asset/btc/tx.go @@ -3,7 +3,10 @@ package btc -import "github.com/btcsuite/btcd/chaincfg/chainhash" +import ( + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg/chainhash" +) // Tx is information about a transaction. It must satisfy the asset.DEXTx // interface to be DEX-compatible. @@ -64,3 +67,64 @@ func newTransaction(btc *Backend, txHash, blockHash, lastLookup *chainhash.Hash, raw: rawTx, } } + +// JoinSplit represents a ZCash JoinSplit. +// https://zips.z.cash/protocol/canopy.pdf section 4.11 +type JoinSplit struct { + // Old = input + Old uint64 `json:"vpub_oldZat"` + // New = output + New uint64 `json:"vpub_newZat"` +} + +// VerboseTxExtended is a subset of *btcjson.TxRawResult, with the addition of +// some asset-specific fields. +type VerboseTxExtended struct { + Hex string `json:"hex"` + Txid string `json:"txid"` + Size int32 `json:"size,omitempty"` + Vsize int32 `json:"vsize,omitempty"` + Vin []*btcjson.Vin `json:"vin"` + Vout []*btcjson.Vout `json:"vout"` + BlockHash string `json:"blockhash,omitempty"` + Confirmations uint64 `json:"confirmations,omitempty"` + + // ZCash-specific fields. + + VJoinSplit []*JoinSplit `json:"vjoinsplit"` + ValueBalanceSapling int64 `json:"valueBalanceZat"` // Sapling pool + // ValueBalanceOrchard is disabled until zcashd encodes valueBalanceOrchard. + ValueBalanceOrchard int64 `json:"valueBalanceOrchardZat"` // Orchard pool + + // Other fields that could be used but aren't right now. + + // Hash string `json:"hash,omitempty"` + // Weight int32 `json:"weight,omitempty"` + // Version uint32 `json:"version"` + // LockTime uint32 `json:"locktime"` + // Time int64 `json:"time,omitempty"` + // Blocktime int64 `json:"blocktime,omitempty"` +} + +// Currently disabled because the verbose getrawtransaction results for ZCash +// do not includee the valueBalanceOrchard yet. +// https://github.com/zcash/zcash/pull/5969 +// // ShieldedIO sums the ZCash shielded pool inputs and outputs. Will return +// // zeros for non-ZCash-protocol transactions. +// func (tx *VerboseTxExtended) ShieldedIO() (in, out uint64) { +// for _, js := range tx.VJoinSplit { +// in += js.New +// out += js.Old +// } +// if tx.ValueBalanceSapling > 0 { +// in += uint64(tx.ValueBalanceSapling) +// } else if tx.ValueBalanceSapling < 0 { +// out += uint64(-1 * tx.ValueBalanceSapling) +// } +// if tx.ValueBalanceOrchard > 0 { +// in += uint64(tx.ValueBalanceOrchard) +// } else if tx.ValueBalanceOrchard < 0 { +// out += uint64(-1 * tx.ValueBalanceOrchard) +// } +// return +// } diff --git a/server/asset/dcr/live_test.go b/server/asset/dcr/live_test.go index 7839e57cb5..950b4bec6e 100644 --- a/server/asset/dcr/live_test.go +++ b/server/asset/dcr/live_test.go @@ -62,7 +62,7 @@ func TestMain(m *testing.M) { }() var err error - dcr, err = NewBackend("", logger, dex.Testnet) + dcr, err = NewBackend("", logger, dex.Mainnet) if err != nil { fmt.Printf("NewBackend error: %v\n", err) return 1 diff --git a/server/asset/zec/live_test.go b/server/asset/zec/live_test.go new file mode 100644 index 0000000000..1f782c0891 --- /dev/null +++ b/server/asset/zec/live_test.go @@ -0,0 +1,81 @@ +//go:build zeclive + +// go test -v -tags zeclive -run UTXOStats +// ----------------------------------- +// Grab the most recent block and iterate it's outputs, taking account of +// how many UTXOs are found, how many are of an unknown type, etc. +// +// go test -v -tags zeclive -run P2SHStats +// ----------------------------------------- +// For each output in the last block, check it's previous outpoint to see if +// it's a P2SH or P2WSH. If so, takes statistics on the script types, including +// for the redeem script. +// +// go test -v -tags zeclive -run LiveFees +// ------------------------------------------ +// Test that fees rates are parsed without error and that a few historical fee +// rates are correct + +package zec + +import ( + "context" + "fmt" + "os" + "testing" + + "decred.org/dcrdex/dex" + "decred.org/dcrdex/server/asset/btc" +) + +var ( + zec *ZECBackend + ctx context.Context +) + +func TestMain(m *testing.M) { + // Wrap everything for defers. + doIt := func() int { + logger := dex.StdOutLogger("ZECTEST", dex.LevelTrace) + be, err := NewBackend("", logger, dex.Mainnet) + if err != nil { + fmt.Printf("NewBackend error: %v\n", err) + return 1 + } + + var ok bool + zec, ok = be.(*ZECBackend) + if !ok { + fmt.Printf("Could not cast asset.Backend to *Backend") + return 1 + } + + ctx, cancel := context.WithCancel(context.Background()) + wg, err := zec.Connect(ctx) + if err != nil { + fmt.Printf("Connect failed: %v", err) + return 1 + } + defer wg.Wait() + defer cancel() + + return m.Run() + } + + os.Exit(doIt()) +} + +func TestUTXOStats(t *testing.T) { + btc.LiveUTXOStats(zec.Backend, t) +} + +func TestP2SHStats(t *testing.T) { + btc.LiveP2SHStats(zec.Backend, t, 50) +} + +func TestLiveFees(t *testing.T) { + btc.LiveFeeRates(zec.Backend, t, map[string]uint64{ + "920456117a0a9c55867e55cb02487d20a39feb4e4f6c9a69ec6f55fb243123e7": 5, + "90c2fbb3636e5bd8d35fc17202bfe86935fad3e8244e736f9b267c8d0ad14f90": 4631, + }) +} diff --git a/server/asset/zec/zec.go b/server/asset/zec/zec.go new file mode 100644 index 0000000000..6cef759efa --- /dev/null +++ b/server/asset/zec/zec.go @@ -0,0 +1,261 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package zec + +import ( + "encoding/hex" + "fmt" + "math" + + "decred.org/dcrdex/dex" + dexbtc "decred.org/dcrdex/dex/networks/btc" + dexzec "decred.org/dcrdex/dex/networks/zec" + "decred.org/dcrdex/server/asset" + "decred.org/dcrdex/server/asset/btc" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" +) + +// Driver implements asset.Driver. +type Driver struct{} + +// Setup creates the ZCash backend. Start the backend with its Run method. +func (d *Driver) Setup(configPath string, logger dex.Logger, network dex.Network) (asset.Backend, error) { + return NewBackend(configPath, logger, network) +} + +// DecodeCoinID creates a human-readable representation of a coin ID for +// ZCash. +func (d *Driver) DecodeCoinID(coinID []byte) (string, error) { + // ZCash and Bitcoin have the same tx hash and output format. + return (&btc.Driver{}).DecodeCoinID(coinID) +} + +// Version returns the Backend implementation's version number. +func (d *Driver) Version() uint32 { + return version +} + +// UnitInfo returns the dex.UnitInfo for the asset. +func (d *Driver) UnitInfo() dex.UnitInfo { + return dexzec.UnitInfo +} + +func init() { + asset.Register(BipID, &Driver{}) +} + +const ( + version = 0 + BipID = 133 + assetName = "zec" + feeConfs = 10 // Block time is 75 seconds +) + +// NewBackend generates the network parameters and creates a zec backend as a +// btc clone using an asset/btc helper function. +func NewBackend(configPath string, logger dex.Logger, network dex.Network) (asset.Backend, error) { + var btcParams *chaincfg.Params + var addrParams *dexzec.AddressParams + switch network { + case dex.Mainnet: + btcParams = dexzec.MainNetParams + addrParams = dexzec.MainNetAddressParams + case dex.Testnet: + btcParams = dexzec.TestNet4Params + addrParams = dexzec.TestNet4AddressParams + case dex.Regtest: + btcParams = dexzec.RegressionNetParams + addrParams = dexzec.RegressionNetAddressParams + default: + return nil, fmt.Errorf("unknown network ID %v", network) + } + + // Designate the clone ports. These will be overwritten by any explicit + // settings in the configuration file. + ports := dexbtc.NetPorts{ + Mainnet: "8232", + Testnet: "18232", + Simnet: "18232", + } + + if configPath == "" { + configPath = dexbtc.SystemConfigPath("zcash") + } + + be, err := btc.NewBTCClone(&btc.BackendCloneConfig{ + Name: assetName, + Segwit: false, + ConfigPath: configPath, + Logger: logger, + Net: network, + ChainParams: btcParams, + Ports: ports, + AddressDecoder: func(addr string, net *chaincfg.Params) (btcutil.Address, error) { + return dexzec.DecodeAddress(addr, addrParams, btcParams) + }, + InitTxSize: dexzec.InitTxSize, + InitTxSizeBase: dexzec.InitTxSizeBase, + TxDeserializer: func(b []byte) (*wire.MsgTx, error) { + zecTx, err := dexzec.DeserializeTx(b) + if err != nil { + return nil, err + } + return zecTx.MsgTx, nil + }, + DumbFeeEstimates: true, + ManualMedianFee: true, + BlockFeeTransactions: blockFeeTransactions, + NumericGetRawRPC: true, + ShieldedIO: shieldedIO, + }) + if err != nil { + return nil, err + } + + return &ZECBackend{ + Backend: be, + addrParams: addrParams, + btcParams: btcParams, + }, nil +} + +// ZECBackend embeds *btc.Backend and re-implements the Contract method to deal +// with ZCash address translation. +type ZECBackend struct { + *btc.Backend + btcParams *chaincfg.Params + addrParams *dexzec.AddressParams +} + +// Contract returns the output from embedded Backend's Contract method, but +// with the SwapAddress field converted to ZCash encoding. +// TODO: Drop this in favor of an AddressEncoder field in the +// BackendCloneConfig. +func (be *ZECBackend) Contract(coinID []byte, redeemScript []byte) (*asset.Contract, error) { // Contract.SwapAddress + contract, err := be.Backend.Contract(coinID, redeemScript) + if err != nil { + return nil, err + } + contract.SwapAddress, err = dexzec.RecodeAddress(contract.SwapAddress, be.addrParams, be.btcParams) + if err != nil { + return nil, err + } + return contract, nil +} + +func blockFeeTransactions(rc *btc.RPCClient, blockHash *chainhash.Hash) (feeTxs []btc.FeeTx, prevBlock chainhash.Hash, err error) { + blockB, err := rc.GetRawBlock(blockHash) + if err != nil { + return nil, chainhash.Hash{}, err + } + + blk, err := dexzec.DeserializeBlock(blockB) + if err != nil { + return nil, chainhash.Hash{}, err + } + + if len(blk.Transactions) == 0 { + return nil, chainhash.Hash{}, fmt.Errorf("block %s has no transactions", blockHash) + } + + feeTxs = make([]btc.FeeTx, 0, len(blk.Transactions)-1) + for _, tx := range blk.Transactions[1:] { // skip coinbase + feeTx, err := newFeeTx(tx) + if err != nil { + return nil, chainhash.Hash{}, fmt.Errorf("error parsing fee tx: %w", err) + } + feeTxs = append(feeTxs, feeTx) + } + + return feeTxs, blk.Header.PrevBlock, nil +} + +// feeTx implements FeeTx for manual median-fee calculations. +type feeTx struct { + size uint64 + prevOuts []wire.OutPoint + shieldedIn uint64 + transparentOut uint64 + shieldedOut uint64 +} + +var _ btc.FeeTx = (*feeTx)(nil) + +func newFeeTx(zecTx *dexzec.Tx) (*feeTx, error) { + var transparentOut uint64 + for _, out := range zecTx.TxOut { + transparentOut += uint64(out.Value) + } + prevOuts := make([]wire.OutPoint, len(zecTx.TxOut)) + for _, in := range zecTx.TxIn { + prevOuts = append(prevOuts, in.PreviousOutPoint) + } + var shieldedIn, shieldedOut uint64 + for _, js := range zecTx.VJoinSplit { + shieldedIn += js.New + shieldedOut += js.Old + } + if zecTx.ValueBalanceSapling > 0 { + shieldedIn += uint64(zecTx.ValueBalanceSapling) + } else if zecTx.ValueBalanceSapling < 0 { + shieldedOut += uint64(-1 * zecTx.ValueBalanceSapling) + } + if zecTx.ValueBalanceOrchard > 0 { + shieldedIn += uint64(zecTx.ValueBalanceOrchard) + } else if zecTx.ValueBalanceOrchard < 0 { + shieldedOut += uint64(-1 * zecTx.ValueBalanceOrchard) + } + + return &feeTx{ + size: zecTx.SerializeSize(), + transparentOut: transparentOut, + shieldedOut: shieldedOut, + shieldedIn: shieldedIn, + prevOuts: prevOuts, + }, nil +} + +func (tx *feeTx) PrevOuts() []wire.OutPoint { + return tx.prevOuts +} + +func (tx *feeTx) FeeRate(prevOuts map[chainhash.Hash]map[int]int64) (uint64, error) { + var transparentIn uint64 + for _, op := range tx.prevOuts { + outs, found := prevOuts[op.Hash] + if !found { + return 0, fmt.Errorf("previous outpoint tx not found for %+v", op) + } + prevOutValue, found := outs[int(op.Index)] + if !found { + return 0, fmt.Errorf("previous outpoint vout not found for %+v", op) + } + transparentIn += uint64(prevOutValue) + } + in := tx.shieldedIn + transparentIn + out := tx.shieldedOut + tx.transparentOut + if out > in { + return 0, fmt.Errorf("out > in. %d > %d", out, in) + } + return uint64(math.Round(float64(in-out) / float64(tx.size))), nil +} + +func shieldedIO(tx *btc.VerboseTxExtended) (in, out uint64, err error) { + txB, err := hex.DecodeString(tx.Hex) + if err != nil { + return 0, 0, fmt.Errorf("hex.DecodeString error: %w", err) + } + zecTx, err := dexzec.DeserializeTx(txB) + if err != nil { + return 0, 0, fmt.Errorf("DeserializeTx error: %w", err) + } + feeTx, err := newFeeTx(zecTx) + if err != nil { + return 0, 0, err + } + return feeTx.shieldedIn, feeTx.shieldedOut, nil +} diff --git a/server/asset/zec/zec_test.go b/server/asset/zec/zec_test.go new file mode 100644 index 0000000000..501195555f --- /dev/null +++ b/server/asset/zec/zec_test.go @@ -0,0 +1,41 @@ +//go:build !zeclive + +package zec + +import ( + "encoding/hex" + "testing" + + dexzec "decred.org/dcrdex/dex/networks/zec" + "decred.org/dcrdex/server/asset/btc" +) + +func TestCompatibility(t *testing.T) { + fromHex := func(str string) []byte { + b, err := hex.DecodeString(str) + if err != nil { + t.Fatalf("error decoding %s: %v", str, err) + } + return b + } + + pkhAddr := "t1SqYLhzHyGoWwatRNGrTt4ueqivKdJpFY4" + btcPkhAddr, err := dexzec.DecodeAddress(pkhAddr, dexzec.MainNetAddressParams, dexzec.MainNetParams) + if err != nil { + t.Fatalf("error decoding p2pkh address: %v", err) + } + + shAddr := "t3ZJCdehVh9MTm6BaKWZmWy5Hsw7PhJxmTc" + btcShAddr, err := dexzec.DecodeAddress(shAddr, dexzec.MainNetAddressParams, dexzec.MainNetParams) + if err != nil { + t.Fatalf("error decoding p2sh address: %v", err) + } + + items := &btc.CompatibilityItems{ + P2PKHScript: fromHex("76a91462553d6a85afe7753cbe8dc57c7f34f6a8efd79f88ac"), + PKHAddr: btcPkhAddr.String(), + P2SHScript: fromHex("a914a19f5d7d23bbbff0695363f932c8d67c0169963f87"), + SHAddr: btcShAddr.String(), + } + btc.CompatibilityCheck(items, dexzec.MainNetParams, t) +} diff --git a/server/cmd/dcrdex/main.go b/server/cmd/dcrdex/main.go index 2591ebf991..c81d6cd712 100644 --- a/server/cmd/dcrdex/main.go +++ b/server/cmd/dcrdex/main.go @@ -23,6 +23,7 @@ import ( _ "decred.org/dcrdex/server/asset/dcr" // register dcr asset _ "decred.org/dcrdex/server/asset/doge" // register doge asset _ "decred.org/dcrdex/server/asset/ltc" // register ltc asset + _ "decred.org/dcrdex/server/asset/zec" // register zec asset dexsrv "decred.org/dcrdex/server/dex" "github.com/decred/dcrd/dcrec/secp256k1/v4" ) diff --git a/server/cmd/dexcoin/main.go b/server/cmd/dexcoin/main.go index 9df496a24f..5f29b0f163 100644 --- a/server/cmd/dexcoin/main.go +++ b/server/cmd/dexcoin/main.go @@ -16,6 +16,7 @@ import ( _ "decred.org/dcrdex/server/asset/dcr" _ "decred.org/dcrdex/server/asset/doge" _ "decred.org/dcrdex/server/asset/ltc" + _ "decred.org/dcrdex/server/asset/zec" ) type coinDecoder func([]byte) (string, error)