diff --git a/client/asset/bch/bch.go b/client/asset/bch/bch.go index 09a6ede4b5..f5d877a520 100644 --- a/client/asset/bch/bch.go +++ b/client/asset/bch/bch.go @@ -18,6 +18,7 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" "github.com/gcash/bchd/bchec" bchscript "github.com/gcash/bchd/txscript" bchwire "github.com/gcash/bchd/wire" @@ -162,59 +163,24 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) // Bitcoin Cash uses the Cash Address encoding, which is Bech32, but // not indicative of segwit. We provide a custom encoder. AddressDecoder: dexbch.DecodeCashAddress, + AddressStringer: func(addr btcutil.Address) string { + a, _ := dexbch.RecodeCashAddress(addr.String(), params) + return a + }, // Bitcoin Cash has a custom signature hash algorithm. Since they don't // have segwit, Bitcoin Cash implemented a variation of the withdrawn // BIP0062 that utilizes Shnorr signatures. // 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 -} - -// Address converts the Bitcoin base58-encoded address returned by the embedded -// ExchangeWallet into a Cash Address. -func (bch *BCHWallet) Address() (string, error) { - btcAddrStr, err := bch.ExchangeWalletFullNode.Address() - if err != nil { - return "", err - } - return dexbch.RecodeCashAddress(btcAddrStr, bch.Net()) -} - -// AuditContract modifies the *asset.Contract returned by the ExchangeWallet -// AuditContract method by converting the Recipient to the Cash Address -// encoding. -func (bch *BCHWallet) AuditContract(coinID, contract, txData dex.Bytes, rebroadcast bool) (*asset.AuditInfo, error) { // AuditInfo has address - ai, err := bch.ExchangeWalletFullNode.AuditContract(coinID, contract, txData, rebroadcast) - if err != nil { - return nil, err - } - ai.Recipient, err = dexbch.RecodeCashAddress(ai.Recipient, bch.Net()) - if err != nil { - return nil, err - } - return ai, nil + return btc.BTCCloneWallet(cloneCFG) } // rawTxSigner signs the transaction using Bitcoin Cash's custom signature diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 27c7f1de9e..831d0c24f0 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -219,6 +219,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 @@ -229,11 +232,6 @@ type BTCCloneCFG struct { // into btcutil.Address. If AddressDecoder is not supplied, // btcutil.DecodeAddress will be used. AddressDecoder dexbtc.AddressDecoder - // BlockDeserializer can be used in place of (*wire.MsgBlock).Deserialize. - BlockDeserializer func([]byte) (*wire.MsgBlock, error) - // ArglessChangeAddrRPC can be true if the getrawchangeaddress takes no - // address-type argument. - ArglessChangeAddrRPC bool // NonSegwitSigner can be true if the transaction signature hash data is not // the standard for non-segwit Bitcoin. If nil, txscript. NonSegwitSigner TxInSigner @@ -249,6 +247,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 @@ -262,6 +263,19 @@ 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) + // BlockDeserializer is an optional function to deserialize a block. + BlockDeserializer func([]byte) (*wire.MsgBlock, error) + // TxHasher is a function that generates a tx hash from a MsgTx. + TxHasher func(*wire.MsgTx) *chainhash.Hash + // AddressStringer is a function to convert an address to a string. + AddressStringer func(btcutil.Address) string + // TxSizeCalculator is an optional function that will be used to calculate + // the size of a transaction. + TxSizeCalculator func(*wire.MsgTx) uint64 } // outPoint is the hash and output index of a transaction output. @@ -454,7 +468,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 @@ -585,10 +601,16 @@ type baseWallet struct { redeemConfTarget uint64 useSplitTx bool useLegacyBalance bool + zecStyleBalance bool segwit bool signNonSegwit TxInSigner estimateFee func(RawRequester, uint64) (uint64, error) 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 + stringifyAddress func(btcutil.Address) string tipMtx sync.RWMutex currentTip *block @@ -746,17 +768,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) @@ -774,7 +785,6 @@ func newRPCWallet(requester RawRequesterWithContext, cfg *BTCCloneCFG, walletCon segwit: cfg.Segwit, decodeAddr: btc.decodeAddr, deserializeBlock: blockDeserializer, - arglessChangeAddrRPC: cfg.ArglessChangeAddrRPC, legacyRawSends: cfg.LegacyRawFeeLimit, minNetworkVersion: cfg.MinNetworkVersion, log: cfg.Logger.SubLogger("RPC"), @@ -782,8 +792,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, + stringifyAddress: btc.stringifyAddress, }) return &ExchangeWalletFullNode{btc}, nil } @@ -829,6 +844,31 @@ func newUnconnectedWallet(cfg *BTCCloneCFG, walletCfg *WalletConfig) (*baseWalle 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 + } + + addressStringer := cfg.AddressStringer + if addressStringer == nil { + addressStringer = stringifyAddress + } + w := &baseWallet{ symbol: cfg.Symbol, chainParams: cfg.ChainParams, @@ -844,11 +884,17 @@ 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, walletInfo: cfg.WalletInfo, + deserializeTx: txDeserializer, + serializeTx: txSerializer, + hashTx: txHasher, + stringifyAddress: addressStringer, + calcTxSize: txSizeCalculator, } if w.estimateFee == nil { @@ -958,10 +1004,21 @@ 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 *bool `json:"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. @@ -1004,7 +1061,7 @@ func (btc *baseWallet) OwnsAddress(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() @@ -1031,6 +1088,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) @@ -1726,15 +1792,15 @@ 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: { txHash: op.txHash(), vout: op.vout(), - address: addr.String(), + address: btc.stringifyAddress(addr), amount: reqFunds, }} @@ -2008,12 +2074,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) @@ -2059,7 +2125,7 @@ func (btc *baseWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, ui btc.fundingCoins[change.pt] = &utxo{ txHash: change.txHash(), vout: change.vout(), - address: changeAddr.String(), + address: btc.stringifyAddress(changeAddr), amount: change.value, } } @@ -2110,7 +2176,7 @@ func (btc *baseWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin, } // 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 @@ -2182,12 +2248,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) } @@ -2290,7 +2356,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) } @@ -2350,13 +2416,48 @@ func (btc *baseWallet) AuditContract(coinID, contract, txData dex.Bytes, rebroad return &asset.AuditInfo{ Coin: newOutput(txHash, vout, uint64(txOut.Value)), - Recipient: receiver.String(), + Recipient: btc.stringifyAddress(receiver), Contract: contract, SecretHash: secretHash, Expiration: time.Unix(int64(stamp), 0).UTC(), }, nil } +// AuditContract retrieves information about a swap contract from the provided +// txData. The extracted information would be used to audit the counter-party's +// contract during a swap. Since this wallet is backed by a full node, txData +// may be empty to attempt retrieval of the transaction output from the network. +func (btc *ExchangeWalletFullNode) AuditContract(coinID, contract, txData dex.Bytes, rebroadcast bool) (*asset.AuditInfo, error) { + if len(txData) != 0 { + return btc.baseWallet.AuditContract(coinID, contract, txData, rebroadcast) + } + + full, ok := btc.node.(*rpcClient) + if !ok { + return nil, fmt.Errorf("wallet backend not a *rpcClient: %T", btc.node) + } + + // In the off chance that the transaction is in mempool or txindex is + // enabled, try getrawtransaction. + txHash, _, err := decodeCoinID(coinID) + if err != nil { + return nil, err + } + tx, err := full.GetRawTransaction(txHash) + if err != nil { + btc.log.Warnf("Contract transaction %v could not be retrieved: %v", txHash, err) + // Try with gettxout in the baseWallet method. + return btc.baseWallet.AuditContract(coinID, contract, txData, rebroadcast) + } + + txData, err = btc.serializeTx(tx) // if error, we'll just pass nil and let it try + if err != nil { + return nil, err + } + + return btc.baseWallet.AuditContract(coinID, contract, txData, rebroadcast) +} + // LocktimeExpired returns true if the specified contract's locktime has // expired, making it possible to issue a Refund. func (btc *baseWallet) LocktimeExpired(contract dex.Bytes) (bool, time.Time, error) { @@ -2400,7 +2501,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 } @@ -2650,12 +2751,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) } @@ -2683,7 +2784,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. @@ -2741,7 +2842,7 @@ func (btc *baseWallet) Address() (string, error) { if err != nil { return "", err } - return addr.String(), nil + return btc.stringifyAddress(addr), nil } // NewAddress returns a new address from the wallet. This satisfies the @@ -2812,7 +2913,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) } @@ -3141,13 +3242,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 } @@ -3216,7 +3317,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 { @@ -3243,10 +3344,10 @@ 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 - btc.log.Debugf("Change output size = %d, addr = %s", changeSize, addr.String()) + changeSize := btc.calcTxSize(baseTx) - vSize0 // may be dexbtc.P2WPKHOutputSize + btc.log.Debugf("Change output size = %d, addr = %s", changeSize, btc.stringifyAddress(addr)) vSize += changeSize fee := feeRate * vSize @@ -3260,7 +3361,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 @@ -3297,16 +3398,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 @@ -3317,25 +3419,41 @@ 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) { - privKey, err := btc.node.privKeyForAddress(addr.String()) + privKey, err := btc.node.privKeyForAddress(btc.stringifyAddress(addr)) if err != nil { return nil, nil, err } + sig, err = btc.signNonSegwit(tx, idx, pkScript, txscript.SigHashAll, privKey, val) if err != nil { return nil, nil, err } + return sig, privKey.PubKey().SerializeCompressed(), nil } @@ -3344,7 +3462,7 @@ func (btc *baseWallet) createSig(tx *wire.MsgTx, idx int, pkScript []byte, addr func (btc *baseWallet) createWitnessSig(tx *wire.MsgTx, idx int, pkScript []byte, addr btcutil.Address, val uint64, sigHashes *txscript.TxSigHashes) (sig, pubkey []byte, err error) { - privKey, err := btc.node.privKeyForAddress(addr.String()) + privKey, err := btc.node.privKeyForAddress(btc.stringifyAddress(addr)) if err != nil { return nil, nil, err } @@ -3469,7 +3587,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 } @@ -3617,6 +3735,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 { @@ -3630,7 +3754,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", @@ -3638,7 +3762,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, } } @@ -3677,6 +3801,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) string { + return addr.String() +} + func deserializeBlock(b []byte) (*wire.MsgBlock, error) { msgBlock := &wire.MsgBlock{} return msgBlock, msgBlock.Deserialize(bytes.NewReader(b)) diff --git a/client/asset/btc/livetest/livetest.go b/client/asset/btc/livetest/livetest.go index 12ed233160..98297d0272 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) } @@ -121,6 +127,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 +140,7 @@ type Config struct { SPV bool FirstWallet *WalletName SecondWallet *WalletName + Unencrypted bool } func Run(t *testing.T, cfg *Config) { @@ -179,24 +189,26 @@ 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.firstWallet = tBackend(tCtx, t, cfg, cfg.FirstWallet, 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.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 +377,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. diff --git a/client/asset/btc/rpcclient.go b/client/asset/btc/rpcclient.go index b9485df491..16f51ce9df 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" @@ -74,8 +75,6 @@ type rpcCore struct { requester RawRequesterWithContext segwit bool decodeAddr dexbtc.AddressDecoder - deserializeBlock func([]byte) (*wire.MsgBlock, error) - arglessChangeAddrRPC bool legacyRawSends bool minNetworkVersion uint64 log dex.Logger @@ -83,8 +82,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 + stringifyAddress func(btcutil.Address) string } // rpcClient is a bitcoind JSON RPC client that uses rpcclient.Client's @@ -140,7 +145,7 @@ 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 } @@ -151,7 +156,7 @@ func (wc *rpcClient) SendRawTransactionLegacy(tx *wire.MsgTx) (*chainhash.Hash, // 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 } @@ -304,12 +309,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. @@ -358,7 +367,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) @@ -400,7 +409,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) } @@ -422,7 +431,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) } @@ -494,10 +503,10 @@ 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) - return ai, wc.call(method, anylist{addr.String()}, ai) + return ai, wc.call(method, anylist{wc.stringifyAddress(addr)}, ai) } // ownsAddress indicates if an address belongs to the wallet. @@ -529,7 +538,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 } @@ -646,7 +655,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 } @@ -667,7 +676,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 } @@ -726,17 +735,3 @@ func msgTxFromBytes(txB []byte) (*wire.MsgTx, error) { } 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/zec/regnet_test.go b/client/asset/zec/regnet_test.go new file mode 100644 index 0000000000..7e3ff0caf9 --- /dev/null +++ b/client/asset/zec/regnet_test.go @@ -0,0 +1,42 @@ +//go:build harness + +package zec + +// Regnet tests expect the ZEC test harness to be running. + +import ( + "testing" + + "decred.org/dcrdex/client/asset/btc/livetest" + "decred.org/dcrdex/dex" + dexzec "decred.org/dcrdex/dex/networks/zec" +) + +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: "zcash.conf", + }, + SecondWallet: &livetest.WalletName{ + Node: "beta", + Filename: "zcash.conf", + }, + Unencrypted: true, + }) +} diff --git a/client/asset/zec/sign.go b/client/asset/zec/sign.go new file mode 100644 index 0000000000..73f7ce0804 --- /dev/null +++ b/client/asset/zec/sign.go @@ -0,0 +1,256 @@ +package zec + +import ( + "bytes" + "encoding/binary" + "fmt" + "math" + + dexzec "decred.org/dcrdex/dex/networks/zec" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/dchest/blake2b" +) + +const ( + // ZIP 143 personalization keys. + blake2BSigHash = "ZcashSigHash" + prevoutsHashPersonalization = "ZcashPrevoutHash" + sequenceHashPersonalization = "ZcashSequencHash" + outputsHashPersonalization = "ZcashOutputsHash" + + sigHashMask = 0x1f + + // versionOverwinterGroupID uint32 = 0x3C48270 + versionSaplingGroupID = 0x892f2085 + versionNU5GroupID = 0x26A7270A +) + +var ( + // Little-endian encoded concensus version IDs. + NoUpgrade = [4]byte{0x00, 0x00, 0x00, 0x00} // 0 + Overwinter = [4]byte{0x19, 0x1B, 0xA8, 0x5B} // 207500 + Sapling = [4]byte{0xBB, 0x09, 0xB8, 0x76} // 280000 + Blossom = [4]byte{0x60, 0x0E, 0xB4, 0x2B} // 653600 + Heartwood = [4]byte{0x0B, 0x23, 0xB9, 0xF5} // 903000 + Canopy = [4]byte{0xA6, 0x75, 0xFF, 0xE9} // 1046400 +) + +// blake2bSignatureHash creates a hash for a transparent input. +// This function will not work with transactions with shielded i/o. +// See https://github.com/zcash/librustzcash/blob/master/zcash_primitives/src/transaction/sighash_v4.rs +// Specifications: +// https://zips.z.cash/protocol/canopy.pdf section 4.9 points to +// https://github.com/zcash/zips/blob/main/zip-0243.rst#specification +// which extends https://zips.z.cash/zip-0143#specification +// See also implementation @ +// https://github.com/iqoption/zecutil/blob/master/sign.go +func blake2bSignatureHash( + subScript []byte, + sigHashes *txscript.TxSigHashes, + hashType txscript.SigHashType, + tx *dexzec.ZecTx, + idx int, + amt int64, + cver [4]byte, // consensus version (little-endian encoded uint32) +) (preimage, sHash []byte, err error) { + if tx.Version < dexzec.VersionSapling { + return nil, nil, fmt.Errorf("version %d transactions unsupported", tx.Version) + } + + if idx > len(tx.TxIn)-1 { + return nil, nil, fmt.Errorf("blake2bSignatureHash error: idx %d but %d txins", idx, len(tx.TxIn)) + } + + var sigHash bytes.Buffer + + // header is tx.Version with the overwintered flag set. + var bVersion [4]byte + binary.LittleEndian.PutUint32(bVersion[:], uint32(tx.Version)|(1<<31)) + sigHash.Write(bVersion[:]) + + // nVersionGroupId + var groupID uint32 = versionSaplingGroupID + if tx.Version == dexzec.VersionCanopy { + groupID = versionNU5GroupID + } + var nVersion [4]byte + binary.LittleEndian.PutUint32(nVersion[:], groupID) + sigHash.Write(nVersion[:]) + + var zeroHash chainhash.Hash + + // hashPrevouts + if hashType&txscript.SigHashAnyOneCanPay == 0 { + sigHash.Write(sigHashes.HashPrevOuts[:]) + } else { + sigHash.Write(zeroHash[:]) + } + + // hashSequence + if hashType&txscript.SigHashAnyOneCanPay == 0 && + hashType&sigHashMask != txscript.SigHashSingle && + hashType&sigHashMask != txscript.SigHashNone { + + sigHash.Write(sigHashes.HashSequence[:]) + } else { + sigHash.Write(zeroHash[:]) + } + + // hashOutputs + if hashType&sigHashMask != txscript.SigHashSingle && hashType&sigHashMask != txscript.SigHashNone { + sigHash.Write(sigHashes.HashOutputs[:]) + } else if hashType&sigHashMask == txscript.SigHashSingle && idx < len(tx.TxOut) { + var ( + b bytes.Buffer + h chainhash.Hash + ) + if err = wire.WriteTxOut(&b, 0, 0, tx.TxOut[idx]); err != nil { + return nil, nil, err + } + + if h, err = blake2bHash(b.Bytes(), []byte(outputsHashPersonalization)); err != nil { + return nil, nil, err + } + sigHash.Write(h.CloneBytes()) + } else { + sigHash.Write(zeroHash[:]) + } + + // hashJoinSplits + sigHash.Write(zeroHash[:]) + + // hashShieldedSpends + if tx.Version == dexzec.VersionSapling { + sigHash.Write(zeroHash[:]) + } + + // hashShieldedOutputs + if tx.Version == dexzec.VersionSapling { + sigHash.Write(zeroHash[:]) + } + + // nLockTime + var lockTime [4]byte + binary.LittleEndian.PutUint32(lockTime[:], tx.LockTime) + sigHash.Write(lockTime[:]) + + // nExpiryHeight + var expiryTime [4]byte + binary.LittleEndian.PutUint32(expiryTime[:], tx.ExpiryHeight) + sigHash.Write(expiryTime[:]) + + // valueBalance + if tx.Version >= dexzec.VersionSapling { + var valueBalance [8]byte + binary.LittleEndian.PutUint64(valueBalance[:], 0) + sigHash.Write(valueBalance[:]) + } + + // hash type + var bHashType [4]byte + binary.LittleEndian.PutUint32(bHashType[:], uint32(hashType)) + sigHash.Write(bHashType[:]) + + if idx != math.MaxUint32 { + // outpoint + sigHash.Write(tx.TxIn[idx].PreviousOutPoint.Hash[:]) + var bIndex [4]byte + binary.LittleEndian.PutUint32(bIndex[:], tx.TxIn[idx].PreviousOutPoint.Index) + sigHash.Write(bIndex[:]) + + // scriptCode + if err = wire.WriteVarBytes(&sigHash, 0, subScript); err != nil { + return nil, nil, err + } + + // value + if err = binary.Write(&sigHash, binary.LittleEndian, amt); err != nil { + return nil, nil, err + } + + // nSequence + var bSequence [4]byte + binary.LittleEndian.PutUint32(bSequence[:], tx.TxIn[idx].Sequence) + sigHash.Write(bSequence[:]) + } + + var h chainhash.Hash + preImage := sigHash.Bytes() + sigHashKey := append([]byte(blake2BSigHash), cver[:]...) + if h, err = blake2bHash(preImage, sigHashKey); err != nil { + return nil, nil, err + } + + return preImage, h.CloneBytes(), nil +} + +// blake2bHash is a BLAKE-2B has of the data with the specified personalization +// key. +func blake2bHash(data, personalizationKey []byte) (h chainhash.Hash, err error) { + bHash, err := blake2b.New(&blake2b.Config{Size: 32, Person: personalizationKey}) + if err != nil { + return h, err + } + + if _, err = bHash.Write(data); err != nil { + return h, err + } + + err = (&h).SetBytes(bHash.Sum(nil)) + return h, err +} + +// NewTxSigHashes is like btcd/txscript.NewTxSigHashes, except the underlying +// hash function is BLAKE-2B instead of double-SHA256. +func NewTxSigHashes(tx *dexzec.ZecTx) (h *txscript.TxSigHashes, err error) { + h = &txscript.TxSigHashes{} + if h.HashPrevOuts, err = calcHashPrevOuts(tx); err != nil { + return + } + if h.HashSequence, err = calcHashSequence(tx); err != nil { + return + } + if h.HashOutputs, err = calcHashOutputs(tx); err != nil { + return + } + return +} + +// calcHashPrevOuts is btcd/txscxript.calcHashPrevOuts, but with BLAKE-2B +// instead of double-SHA256. The personalization key is defined in ZIP 143. +func calcHashPrevOuts(tx *dexzec.ZecTx) (chainhash.Hash, error) { + var b bytes.Buffer + for _, in := range tx.TxIn { + b.Write(in.PreviousOutPoint.Hash[:]) + var buf [4]byte + binary.LittleEndian.PutUint32(buf[:], in.PreviousOutPoint.Index) + b.Write(buf[:]) + } + return blake2bHash(b.Bytes(), []byte(prevoutsHashPersonalization)) +} + +// calcHashSequence is btcd/txscxript.calcHashSequence, but with BLAKE-2B +// instead of double-SHA256. The personalization key is defined in ZIP 143. +func calcHashSequence(tx *dexzec.ZecTx) (chainhash.Hash, 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(sequenceHashPersonalization)) +} + +// calcHashOutputs is btcd/txscxript.calcHashOutputs, but with BLAKE-2B +// instead of double-SHA256. The personalization key is defined in ZIP 143. +func calcHashOutputs(tx *dexzec.ZecTx) (_ chainhash.Hash, 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(outputsHashPersonalization)) +} diff --git a/client/asset/zec/sign_test.go b/client/asset/zec/sign_test.go new file mode 100644 index 0000000000..fb5753231e --- /dev/null +++ b/client/asset/zec/sign_test.go @@ -0,0 +1,90 @@ +//go:build !harness + +package zec + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "fmt" + "testing" + + dexzec "decred.org/dcrdex/dex/networks/zec" + "github.com/btcsuite/btcd/txscript" +) + +func TestSign(t *testing.T) { + tests := []struct { + tx []byte + scriptCode []byte + expPreimage []byte + expSigHash []byte + consensusVersionID [4]byte + }{ + { // Test vector 3 from https://github.com/zcash/zips/blob/main/zip-0243.rst + tx: mustDecodeHex("0400008085202f8901a8c685478265f4c14dada651969c4" + + "5a65e1aeb8cd6791f2f5bb6a1d9952104d9010000006b483045022100a61e5d557568c" + + "2ddc1d9b03a7173c6ce7c996c4daecab007ac8f34bee01e6b9702204d38fdc0bcf2728" + + "a69fde78462a10fb45a9baa27873e6a5fc45fb5c76764202a01210365ffea3efa39089" + + "18a8b8627724af852fc9b86d7375b103ab0543cf418bcaa7ffeffffff02005a6202000" + + "000001976a9148132712c3ff19f3a151234616777420a6d7ef22688ac8b95980000000" + + "0001976a9145453e4698f02a38abdaa521cd1ff2dee6fac187188ac29b0040048b0040" + + "00000000000000000000000"), + scriptCode: mustDecodeHex("76a914507173527b4c3318a2aecd793bf1cfed705950cf88ac"), + expPreimage: mustDecodeHex("0400008085202f89fae31b8dec7b0b77e2c8d6b" + + "6eb0e7e4e55abc6574c26dd44464d9408a8e33f116c80d37f12d89b6f17ff198723e7d" + + "b1247c4811d1a695d74d930f99e98418790d2b04118469b7810a0d1cc59568320aad25" + + "a84f407ecac40b4f605a4e686845400000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000000000" + + "0000000000029b0040048b00400000000000000000001000000a8c685478265f4c14da" + + "da651969c45a65e1aeb8cd6791f2f5bb6a1d9952104d9010000001976a914507173527" + + "b4c3318a2aecd793bf1cfed705950cf88ac80f0fa0200000000feffffff"), + expSigHash: mustDecodeHex("f3148f80dfab5e573d5edfe7a850f5fd39234f80b5429d3a57edcc11e34c585b"), + consensusVersionID: Sapling, + }, + } + + for _, tt := range tests { + tx, err := dexzec.DeserializeZecTx(tt.tx) + if err != nil { + t.Fatalf("ZecTxFromBytes error: %v", err) + } + + cache, err := NewTxSigHashes(tx) + if err != nil { + t.Fatalf("NewTxSigHashes error: %v", err) + } + + amtB, _ := hex.DecodeString("80f0fa0200000000") + amt := binary.LittleEndian.Uint64(amtB) + + pimg, sigHash, err := blake2bSignatureHash(tt.scriptCode, cache, txscript.SigHashAll, tx, 0, int64(amt), tt.consensusVersionID) + if err != nil { + t.Fatalf("blake2bSignatureHash error: %v", err) + } + + if !bytes.Equal(pimg, tt.expPreimage) { + fmt.Printf("expected preimage: %s \n", hex.EncodeToString(tt.expPreimage)) + fmt.Printf("calculated preimage: %s \n", hex.EncodeToString(pimg)) + t.Fatalf("wrong preimage") + } + + // Sighash from test vector will not be correct because of preimage error. + + if !bytes.Equal(sigHash, tt.expSigHash) { + fmt.Printf("expected sighash: %s \n", hex.EncodeToString(tt.expSigHash)) + fmt.Printf("calculated sighash: %s \n", hex.EncodeToString(sigHash)) + t.Fatalf("wrong sighash") + } + } + +} + +func mustDecodeHex(hx string) []byte { + b, err := hex.DecodeString(hx) + if err != nil { + panic("mustDecodeHex: " + err.Error()) + } + return b +} diff --git a/client/asset/zec/zec.go b/client/asset/zec/zec.go new file mode 100644 index 0000000000..668b6bd05f --- /dev/null +++ b/client/asset/zec/zec.go @@ -0,0 +1,255 @@ +// 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" + + "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" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + dcrchaincfg "github.com/decred/dcrd/chaincfg/v3" +) + +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 = 4060051 + walletTypeRPC = "zcashdRPC" +) + +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, + }}, + } +) + +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, network dex.Network) (asset.Wallet, error) { + var btcParams *chaincfg.Params + var addrParams *dcrchaincfg.Params + 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", + } + cloneCFG := &btc.BTCCloneCFG{ + WalletCFG: cfg, + MinNetworkVersion: minNetworkVersion, + WalletInfo: WalletInfo, + Symbol: "zec", + Logger: logger, + Network: network, + 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) string { + s, _ := dexzec.RecodeAddress(addr.String(), addrParams, btcParams) + return s + }, + TxSizeCalculator: dexzec.CalcTxSize, + NonSegwitSigner: signTx, + TxDeserializer: func(b []byte) (*wire.MsgTx, error) { + zecTx, err := dexzec.DeserializeZecTx(b) + if err != nil { + return nil, err + } + return zecTx.MsgTx, nil + }, + BlockDeserializer: func(b []byte) (*wire.MsgBlock, error) { + zecBlock, err := dexzec.DeserializeZecBlock(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 + }, + } + + return btc.BTCCloneWallet(cloneCFG) +} + +func zecTx(tx *wire.MsgTx) *dexzec.ZecTx { + return dexzec.NewZecTxFromMsgTx(tx, dexzec.MaxExpiryHeight, dexzec.VersionSapling) +} + +// 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) { + resp, err := node.RawRequest("estimatefee", nil) + 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, amt uint64) ([]byte, error) { + tx := zecTx(btcTx) + cache, err := NewTxSigHashes(tx) + if err != nil { + return nil, fmt.Errorf("NewTxSigHashes error: %v", err) + } + + // Compare with zcash/zcash TransactionSignatureCreator::CreateSig + // also zcash_transaction_transparent_signature_digest() + + _, sigHash, err := blake2bSignatureHash(pkScript, cache, hashType, tx, idx, int64(amt), Canopy) + if err != nil { + return nil, fmt.Errorf("sighash calculation error: %v", err) + } + + signature, err := key.Sign(sigHash) + if err != nil { + return nil, fmt.Errorf("cannot sign tx input: %s", err) + } + + return append(signature.Serialize(), byte(hashType)), nil +} diff --git a/client/cmd/dexc/main.go b/client/cmd/dexc/main.go index 3903d86965..1104166f73 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/webserver/site/src/img/coins/zec.png b/client/webserver/site/src/img/coins/zec.png new file mode 100644 index 0000000000..34b150f5e7 Binary files /dev/null and b/client/webserver/site/src/img/coins/zec.png differ 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/dex/networks/zec/addr.go b/dex/networks/zec/addr.go new file mode 100644 index 0000000000..1f5af8b8c9 --- /dev/null +++ b/dex/networks/zec/addr.go @@ -0,0 +1,87 @@ +// 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/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/base58" + dcrchaincfg "github.com/decred/dcrd/chaincfg/v3" +) + +// 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, dcrParams *dcrchaincfg.Params, 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 dcrParams.PubKeyHashAddrID: + return btcutil.NewAddressPubKeyHash(data, btcParams) + case dcrParams.ScriptHashAddrID: + return btcutil.NewAddressScriptHashFromHash(data, btcParams) + } + + return nil, fmt.Errorf("unknown address type %v %v", addrID, dcrParams.PubKeyHashAddrID) +} + +// RecodeAddress converts an internal btc address to a ZCash address string. +func RecodeAddress(addr string, dcrParams *dcrchaincfg.Params, btcParams *chaincfg.Params) (string, error) { + btcAddr, err := btcutil.DecodeAddress(addr, btcParams) + if err != nil { + return "", err + } + + switch btcAddr.(type) { + case *btcutil.AddressPubKeyHash: + return b58Encode(btcAddr.ScriptAddress(), dcrParams.PubKeyHashAddrID), nil + case *btcutil.AddressScriptHash: + return b58Encode(btcAddr.ScriptAddress(), dcrParams.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..8e341f64d2 --- /dev/null +++ b/dex/networks/zec/block.go @@ -0,0 +1,125 @@ +// 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" +) + +// ZecBlock extends a wire.MsgBlock to specify ZCash specific fields, or in the +// case of the Nonce, a type-variant. +type ZecBlock struct { + wire.MsgBlock + // Transactions and MsgBlock.Transactions should both be populated. Each + // *ZecTx.MsgTx will be the same as the *MsgTx in MsgBlock.Transactions. + Transactions []*ZecTx + HashBlockCommitments [32]byte // Using NU5 name + Nonce [32]byte // Bitcoin uses uint32 + Solution []byte // length 1344 on main and testnet, 36 on regtest +} + +// DeserializeZecBlock deserializes +func DeserializeZecBlock(b []byte) (*ZecBlock, error) { + zecBlock := &ZecBlock{} + + // 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([]*ZecTx, 0, txCount) + for i := uint64(0); i < txCount; i++ { + tx := &ZecTx{MsgTx: new(wire.MsgTx)} + + if err := tx.ZecDecode(r); err != nil { + return nil, 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 *ZecBlock) decodeBlockHeader(r io.Reader) error { + hdr := &z.MsgBlock.Header + + nVersion, err := readUint32(r) + if err != nil { + return err + } + hdr.Version = int32(nVersion) + + if err = readInternalByteOrder(r, hdr.PrevBlock[:]); err != nil { + return err + } + + if err := readInternalByteOrder(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..05c7b90160 --- /dev/null +++ b/dex/networks/zec/block_test.go @@ -0,0 +1,191 @@ +package zec + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "testing" +) + +func TestBlock(t *testing.T) { + blockB := mustDecodeHex("0400000079a2f1d33fbd51b6fba193893463b44b7257" + + "62f8e18d4d397e713601000000007f7216d545aff45939f7d254d5196ccf991d767f7d" + + "27b00ae77e48d1cfc2ee433d455350e363e7f050abf3c34adb130834e8031b32326483" + + "5a7292974b526ffb433e4e62d0aa011ce2d00000000000000000000000000000000003" + + "0000000000000000004009a4f5fd4005006443aba3157d3ed3842159331256c4ea2d3b" + + "855319937aa7ce72f1f7e14733d356f2217b9a1b1f353e02285e152087c0f8637b8a3d" + + "bdedc72a95425f527027ebae6a5a5534590e34455a0654c3aaab599e948c04a099f840" + + "05ab770a7871bbef7045cdf5b2f7d60c14d999a8d4dadb99449a325dc97e1eedba79d7" + + "3749174789735749511d944003b71ada2d99672cd6ffb5299c0bdb4e0d0c0b11ce5ce0" + + "dd67af0bedf67fdb5f02462e2f038f1b528a30527524305e14fc362cf09e096dd40a18" + + "48489d2fbfd46c4cecaf0b03c3ff3e20053be99ed0e7e0937ddd81a958362ad0fd9f54" + + "55dc1b941fb6dedf073b4b23e249c245d3e5d409fe813e042e09f56e0751c8a64d3225" + + "cc3aeb56737a93cb9c1999d2bc44ada8e991d4193f125b575a722f3f557616e21d88ce" + + "6436f7eaaea4dfecad98893b2cf4e5e126c956a8f80ba8cb33596379a8e76e5126c08b" + + "38c5013bc5c9cdd1becbac6a90abc2a3bab9b5d854534507f32ef225261d313a2c70f8" + + "f421a1606bf2d978c808e8b7422bd935a0cc9e130174dfecd4fc526b2d5561cb7fe009" + + "28efc1b91bc702b44ec4ddf7d311a3140b4a5cb24a97f0a8da7b0879ab588d422dcfbe" + + "53a6407daec0a2da4b1f45f465886247375d92280ffc6821dd933e3490766ab63f031a" + + "187ecbd96ab0cded393c50e644ec157504f61763d3d6aed65ee6bfbd1e1f046120d2eb" + + "1143b8db4a9259f14071c111abaee41f2d6bfa315fa9b9fb946974641d5815ba9fc935" + + "8db908fa0fba4f44145a80eaa227c73809bace4a37ea76426ad6235a567c3f403ff445" + + "05316e1df97171f8f20a1421c599a5a407996343fd8be00337ab6b3e89ea3a5afe8d6b" + + "1dbded5a37066ea541803f8fccbe63731f72c9c19f08070078b05428735d1ffabb595f" + + "72a836bfe1c6c251b814defb7380b267884d543e1a780700f3f57c724e3e18f2e3090d" + + "f4f452b65ab2b636ff172f1d2fefd06e45a8e91177d857bc34fe5b9cb3453a00bbdab3" + + "efc9e5cbb1d53abe55180d7ac95a221c49c197503618b442f574a625b071c9aa5a95df" + + "91b21fae05bba68fc9b5cab924568f36e031de2c77b30a31d6961de61a415b32fc44de" + + "31de5622859f5a1d1f2781097454cd0424c1cd63fca937b73e4e15f6cca829920b4d57" + + "a6a4416f7e743d583e3b677dfebedde506289b58400d8730a85654c53652810a36ea73" + + "f1110eae15f8304534b7bb5fe163ed94ec495a514f6ad34cc98f3472e5f3f5e81af7e7" + + "90f7ae5a91cbbc7a2d5ae6f38c85e08367873cd622534b1ebb690adfaf470bc28ad9cf" + + "038f476506530bd1a80be599b05b8a7e5486d6443da4402daa0e372b7c501f8a4fdef3" + + "25082e7936ac6714426fb334c5c9c24bc071eae5f907e34701b424a12cef39b686e50c" + + "0ad86baa5ad6b98a34013e13dfe8df895dbeb5d2235bf48965055919350e0bbb6fd2cf" + + "4ad72b8a3e712f8a4bd7516f652d0b2702d652a7a2eb7b0ddaabe0b06d44ff49f186b2" + + "afa911a0b98b108558d839a0019d529ddb0574644ea7ee0ce16944dc1033e3638402eb" + + "64712dd6467a92aee70d97f930a3500fd39e67f19fee7caecbf4047fda681942f1724d" + + "f387c9c63426aebc6cde8a92375cec6d5cbd0cf68332be45ac3da7181ec77b96895937" + + "c757023f70457acef3869330827a1f6511d15fb5176b1d0d81a75c03d2fed0bc1b713c" + + "c8f12f95c56c5aad69076e0ab81876cb47dcf143011ad20009ddfd9390c8215f019a06" + + "9d3ac93ace45d86fca9a198913d8720805e4d77b0782a7e355ae4067856e0b7d7e462f" + + "7ba21517fc89e5561adb2129bbc8737d786f0f90df606a154955b845889283b74704ce" + + "c758fbce1bc9713c2f170569962567e27dcdf7f5f4bdee1ac2c3d877988e0104000080" + + "85202f8901000000000000000000000000000000000000000000000000000000000000" + + "0000ffffffff210387c9181c4d696e656420627920416e74506f6f6c36323361001f03" + + "20a3c16b65ffffffff0438c94d010000000017a914df37da178caa3ba0cc7feea71cd8" + + "63c6677f4ea587286bee000000000017a914d45cb1adffb5215a42720532a076f02c7c" + + "778c908740787d010000000017a914931fec54c1fea86e574462cc32013f5400b89129" + + "8780b2e60e000000001976a9145c38e5e20b62bbb5683dd68677ac3715047341ea88ac" + + "00000000000000000000000000000000000000") + + // expHash := mustDecodeHex("00000000015c8a406ff880c5be4d2ae2744eab8be02a33d0179d68f47e51ea82") + // const expHeight = 1624455 + + const expVersion = 4 + expPrevBlock := mustDecodeHex("000000000136717e394d8de1f86257724bb463348993a1fbb651bd3fd3f1a279") + expMerkleRoot := mustDecodeHex("43eec2cfd1487ee70ab0277d7f761d99cf6c19d554d2f73959f4af45d516727f") + // expHashBlockCommitments := mustDecodeHex("30702d1b320e2ea8f603aa8fb54baea5581761d19fccf5061ac82e81d8fdeea4") + expNonce := mustDecodeHex("f5a409400000000000000000000300000000000000000000000000000000d0e2") + expSolution := mustDecodeHex("006443aba3157d3ed3842159331256c4ea2d3b855" + + "319937aa7ce72f1f7e14733d356f2217b9a1b1f353e02285e152087c0f8637b8a3" + + "dbdedc72a95425f527027ebae6a5a5534590e34455a0654c3aaab599e948c04a09" + + "9f84005ab770a7871bbef7045cdf5b2f7d60c14d999a8d4dadb99449a325dc97e1" + + "eedba79d73749174789735749511d944003b71ada2d99672cd6ffb5299c0bdb4e0" + + "d0c0b11ce5ce0dd67af0bedf67fdb5f02462e2f038f1b528a30527524305e14fc3" + + "62cf09e096dd40a1848489d2fbfd46c4cecaf0b03c3ff3e20053be99ed0e7e0937" + + "ddd81a958362ad0fd9f5455dc1b941fb6dedf073b4b23e249c245d3e5d409fe813" + + "e042e09f56e0751c8a64d3225cc3aeb56737a93cb9c1999d2bc44ada8e991d4193" + + "f125b575a722f3f557616e21d88ce6436f7eaaea4dfecad98893b2cf4e5e126c95" + + "6a8f80ba8cb33596379a8e76e5126c08b38c5013bc5c9cdd1becbac6a90abc2a3b" + + "ab9b5d854534507f32ef225261d313a2c70f8f421a1606bf2d978c808e8b7422bd" + + "935a0cc9e130174dfecd4fc526b2d5561cb7fe00928efc1b91bc702b44ec4ddf7d" + + "311a3140b4a5cb24a97f0a8da7b0879ab588d422dcfbe53a6407daec0a2da4b1f4" + + "5f465886247375d92280ffc6821dd933e3490766ab63f031a187ecbd96ab0cded3" + + "93c50e644ec157504f61763d3d6aed65ee6bfbd1e1f046120d2eb1143b8db4a925" + + "9f14071c111abaee41f2d6bfa315fa9b9fb946974641d5815ba9fc9358db908fa0" + + "fba4f44145a80eaa227c73809bace4a37ea76426ad6235a567c3f403ff44505316" + + "e1df97171f8f20a1421c599a5a407996343fd8be00337ab6b3e89ea3a5afe8d6b1" + + "dbded5a37066ea541803f8fccbe63731f72c9c19f08070078b05428735d1ffabb5" + + "95f72a836bfe1c6c251b814defb7380b267884d543e1a780700f3f57c724e3e18f" + + "2e3090df4f452b65ab2b636ff172f1d2fefd06e45a8e91177d857bc34fe5b9cb34" + + "53a00bbdab3efc9e5cbb1d53abe55180d7ac95a221c49c197503618b442f574a62" + + "5b071c9aa5a95df91b21fae05bba68fc9b5cab924568f36e031de2c77b30a31d69" + + "61de61a415b32fc44de31de5622859f5a1d1f2781097454cd0424c1cd63fca937b" + + "73e4e15f6cca829920b4d57a6a4416f7e743d583e3b677dfebedde506289b58400" + + "d8730a85654c53652810a36ea73f1110eae15f8304534b7bb5fe163ed94ec495a5" + + "14f6ad34cc98f3472e5f3f5e81af7e790f7ae5a91cbbc7a2d5ae6f38c85e083678" + + "73cd622534b1ebb690adfaf470bc28ad9cf038f476506530bd1a80be599b05b8a7" + + "e5486d6443da4402daa0e372b7c501f8a4fdef325082e7936ac6714426fb334c5c" + + "9c24bc071eae5f907e34701b424a12cef39b686e50c0ad86baa5ad6b98a34013e1" + + "3dfe8df895dbeb5d2235bf48965055919350e0bbb6fd2cf4ad72b8a3e712f8a4bd" + + "7516f652d0b2702d652a7a2eb7b0ddaabe0b06d44ff49f186b2afa911a0b98b108" + + "558d839a0019d529ddb0574644ea7ee0ce16944dc1033e3638402eb64712dd6467" + + "a92aee70d97f930a3500fd39e67f19fee7caecbf4047fda681942f1724df387c9c" + + "63426aebc6cde8a92375cec6d5cbd0cf68332be45ac3da7181ec77b96895937c75" + + "7023f70457acef3869330827a1f6511d15fb5176b1d0d81a75c03d2fed0bc1b713" + + "cc8f12f95c56c5aad69076e0ab81876cb47dcf143011ad20009ddfd9390c8215f0" + + "19a069d3ac93ace45d86fca9a198913d8720805e4d77b0782a7e355ae4067856e0" + + "b7d7e462f7ba21517fc89e5561adb2129bbc8737d786f0f90df606a154955b8458" + + "89283b74704cec758fbce1bc9713c2f170569962567e27dcdf7f5f4bdee1ac2c3d" + + "877988e") + + expBits := binary.LittleEndian.Uint32(mustDecodeHex("d0aa011c")) + const expTime = 1649294915 + + zecBlock, err := DeserializeZecBlock(blockB) + 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 !bytes.Equal(expPrevBlock, hdr.PrevBlock[:]) { + t.Fatal("wrong previous block", expPrevBlock, hdr.PrevBlock[:]) + } + + if !bytes.Equal(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[:], expSolution) { + 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) { + blockB := mustDecodeHex("04000000b001ef4cf473a983d7d0eccf23195979d92dc4b420" + + "fcbdc6a13213486598b00c6e046134fc6cd860745eb62e0f154a86c2f5a1a017b55d23" + + "685042db9e6faf6d81bc5c1e65dcd92f388d8de8ffd57369885c1997046566f3e633ad" + + "5b795f2005be3d4e620f0f0f2004008fa513bd505a57c5a0831dbc01943b12d74e2f0a" + + "9e766fe5767436a900002400e3ce180372a971362443d91ef2df1dbfdd15314c9232a3" + + "39b1de3ca4501da5bbc371fe") + + zecBlock := &ZecBlock{} + if err := zecBlock.decodeBlockHeader(bytes.NewReader(blockB)); 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..006ea4f41b --- /dev/null +++ b/dex/networks/zec/params.go @@ -0,0 +1,86 @@ +// 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" + dcrchaincfg "github.com/decred/dcrd/chaincfg/v3" +) + +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: "Sats", + 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 = &dcrchaincfg.Params{ + ScriptHashAddrID: [2]byte{0x1C, 0xBD}, + PubKeyHashAddrID: [2]byte{0x1C, 0xB8}, + } + + // TestNet4AddressParams are used for string address parsing. + TestNet4AddressParams = &dcrchaincfg.Params{ + ScriptHashAddrID: [2]byte{0x1C, 0xBA}, + PubKeyHashAddrID: [2]byte{0x1D, 0x25}, + } + + // RegressionNetAddressParams are used for string address parsing. + RegressionNetAddressParams = &dcrchaincfg.Params{ + 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/tx.go b/dex/networks/zec/tx.go new file mode 100644 index 0000000000..8166338829 --- /dev/null +++ b/dex/networks/zec/tx.go @@ -0,0 +1,417 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org + +package zec + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" +) + +const ( + VersionSapling int32 = 4 + VersionCanopy int32 = 5 + MaxExpiryHeight = 499999999 // https://zips.z.cash/zip-0203 + + versionSaplingGroupID = 0x892f2085 + + overwinterMask = ^uint32(1 << 31) + pver = 0 +) + +// ZecTx +type ZecTx struct { + *wire.MsgTx + ExpiryHeight uint32 +} + +func NewZecTxFromMsgTx(tx *wire.MsgTx, expiryHeight uint32, zecVersion int32) *ZecTx { + zecTx := &ZecTx{ + MsgTx: tx, + ExpiryHeight: expiryHeight, + } + zecTx.Version = zecVersion + return zecTx +} + +// TxHash generates the Hash for the transaction. +func (tx *ZecTx) TxHash() chainhash.Hash { + b, _ := tx.Bytes() + return chainhash.DoubleHashH(b) +} + +// ZecEncode 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. +func (tx *ZecTx) Bytes() ([]byte, error) { + w := new(bytes.Buffer) + if tx.Version != 4 { + return nil, fmt.Errorf("only version 4 (sapling) supported") + } + + err := putUint32(w, uint32(tx.Version)|(1<<31)) + if err != nil { + return nil, err + } + + err = putUint32(w, versionSaplingGroupID) + if err != nil { + return nil, err + } + + count := uint64(len(tx.MsgTx.TxIn)) + err = wire.WriteVarInt(w, pver, count) + if err != nil { + return nil, err + } + + for _, ti := range tx.TxIn { + err = writeTxIn(w, tx.Version, ti) + if err != nil { + return nil, err + } + } + + count = uint64(len(tx.TxOut)) + err = wire.WriteVarInt(w, pver, count) + if err != nil { + return nil, err + } + + for _, to := range tx.TxOut { + err = wire.WriteTxOut(w, pver, tx.Version, to) + if err != nil { + return nil, err + } + } + + if err = putUint32(w, tx.LockTime); err != nil { + return nil, err + } + + if err = putUint32(w, tx.ExpiryHeight); err != nil { + return nil, err + } + + if tx.Version == VersionSapling { + // valueBalance + if err := putUint64(w, 0); err != nil { + return nil, err + } + + // nShieldedSpend + err = wire.WriteVarInt(w, pver, 0) + if err != nil { + return nil, err + } + + // nShieldedOutput + err = wire.WriteVarInt(w, pver, 0) + if err != nil { + return nil, err + } + } + + if err := wire.WriteVarInt(w, pver, 0); err != nil { + return nil, err + } + + return w.Bytes(), nil +} + +// see https://zips.z.cash/protocol/protocol.pdf section 7.1 +func DeserializeZecTx(b []byte) (*ZecTx, error) { + tx := &ZecTx{MsgTx: new(wire.MsgTx)} + r := bytes.NewReader(b) + if err := tx.ZecDecode(r); err != nil { + return nil, err + } + return tx, nil +} + +// ZecDecode reads the serialized transaction from the reader and populates the +// *ZecTx's fields. +func (tx *ZecTx) ZecDecode(r io.Reader) error { + ver, err := readUint32(r) + if err != nil { + return err + } + + // overWintered := (ver & (1 << 31)) > 0 + ver &= overwinterMask // Clear the overwinter bit + tx.Version = int32(ver) + + if ver > 4 { + return fmt.Errorf("unsupported tx version %d > 4", ver) + } + + if ver > 3 { + _, err := readUint32(r) + if err != nil { + return 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, pver, int32(ver), 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, pver, int32(ver), to); err != nil { + return err + } + tx.TxOut = append(tx.TxOut, to) + } + + tx.LockTime, err = readUint32(r) + if err != nil { + return err + } + + if ver > 3 { + tx.ExpiryHeight, err = readUint32(r) + if err != nil { + return err + } + } + + // Empty the rest of the buffer. + if ver < 2 { + return nil + } + + var bindingSigRequired bool + if ver >= 4 { + // valueBalanceSpending + if _, err := readUint64(r); err != nil { + return fmt.Errorf("error reading valueBalanceSpending: %w", err) + } + + if nSpendsSapling, err := wire.ReadVarInt(r, pver); err != nil { + return fmt.Errorf("error reading nSpendsSapling: %w", err) + } else if nSpendsSapling > 0 { + // vSpendsSapling - discard + bindingSigRequired = true + if _, err := io.ReadFull(r, make([]byte, nSpendsSapling*384)); err != nil { + return fmt.Errorf("error reading vSpendsSapling: %w", err) + } + } + + if nOutputsSapling, err := wire.ReadVarInt(r, pver); err != nil { + return fmt.Errorf("error reading nOutputsSapling: %w", err) + } else if nOutputsSapling > 0 { + // vOutputsSapling - discard + bindingSigRequired = true + if _, err := io.ReadFull(r, make([]byte, nOutputsSapling*948)); err != nil { + return fmt.Errorf("error reading vOutputsSapling: %w", err) + } + } + + } + + if nJoinSplit, err := wire.ReadVarInt(r, pver); err != nil { + return fmt.Errorf("error reading nJoinSplit: %w", err) + } else if nJoinSplit > 0 { + // vJoinSplit - discard + sz := 1802 * nJoinSplit + if ver == 4 { + sz = 1698 * nJoinSplit + } + if _, err := io.ReadFull(r, make([]byte, sz)); err != nil { + return fmt.Errorf("error reading vJoinSplit: %w", err) + } + var joinSplitPubKey [32]byte + if _, err := io.ReadFull(r, joinSplitPubKey[:]); err != nil { + return fmt.Errorf("error reading joinSplitPubKey: %w", err) + } + + var joinSplitSig [64]byte + if _, err := io.ReadFull(r, joinSplitSig[:]); err != nil { + return fmt.Errorf("error reading joinSplitSig: %w", err) + } + } + + if ver == 4 && bindingSigRequired { + var bindingSigSapling [64]byte + if _, err := io.ReadFull(r, bindingSigSapling[:]); err != nil { + return fmt.Errorf("error reading bindingSigSapling: %w", err) + } + } + + return nil +} + +// writeTxIn encodes ti to the bitcoin protocol encoding for a transaction +// input (TxIn) to w. +func writeTxIn(w io.Writer, version int32, ti *wire.TxIn) error { + err := writeOutPoint(w, version, &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, version int32, op *wire.OutPoint) error { + _, err := w.Write(op.Hash[:]) + if err != nil { + return err + } + return putUint32(w, op.Index) +} + +// putUint32 writes a little-endian encoded uint32 to the Writer. +func putUint32(w io.Writer, v uint32) error { + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, v) + _, err := w.Write(b) + return err +} + +// putUint32 writes a little-endian encoded uint64 to the Writer. +func putUint64(w io.Writer, v uint64) error { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, v) + _, err := w.Write(b) + 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 +} + +// readTxIn reads the next sequence of bytes from r as a transaction input. +func readTxIn(r io.Reader, pver uint32, version int32, ti *wire.TxIn) error { + err := readOutPoint(r, pver, version, &ti.PreviousOutPoint) + if err != nil { + return err + } + + ti.SignatureScript, err = readScript(r, pver) + 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, pver uint32, version int32, to *wire.TxOut) error { + v, err := readUint64(r) + if err != nil { + return err + } + to.Value = int64(v) + + to.PkScript, err = readScript(r, pver) + return err +} + +// readOutPoint reads the next sequence of bytes from r as an OutPoint. +func readOutPoint(r io.Reader, pver uint32, version int32, 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, pver uint32) ([]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 +} + +// 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 { + var sz uint64 = 4 // header + ver := tx.Version + if ver > 3 { + sz += 4 // nVersionGroup + } + sz += uint64(wire.VarIntSerializeSize(uint64(len(tx.TxIn)))) + for _, txIn := range tx.TxIn { + sz += 32 /* prev hash */ + 4 /* prev index */ + 4 /* sequence */ + sz += uint64(wire.VarIntSerializeSize(uint64(len(txIn.SignatureScript)))) + uint64(len(txIn.SignatureScript)) + } + sz += uint64(wire.VarIntSerializeSize(uint64(len(tx.TxOut)))) + for _, txOut := range tx.TxOut { + sz += 8 /* Value */ + sz += uint64(wire.VarIntSerializeSize(uint64(len(txOut.PkScript)))) + uint64(len(txOut.PkScript)) + } + sz += 4 // lockTime + if ver >= 3 { + sz += 4 // nExpiryHeight + } + if ver >= 4 { + sz += 8 // valueBalanceSapling + sz += 1 // nSpendsSapling varint + sz += 1 // nOutputsSapling varint + } + if ver > 2 { + sz += 1 // nJoinSplit varint = 0 + } + return sz +} diff --git a/dex/networks/zec/tx_test.go b/dex/networks/zec/tx_test.go new file mode 100644 index 0000000000..30b186db83 --- /dev/null +++ b/dex/networks/zec/tx_test.go @@ -0,0 +1,143 @@ +package zec + +import ( + "bytes" + "encoding/hex" + "testing" +) + +func TestTxDeserialize(t *testing.T) { + // mainnet tx fb70397806afddcc07b9607e844ff29f2fb09e9972a051c3fe4d56fe18147e77 + txB, _ := hex.DecodeString("0400008085202f8901690d272a3549a3f2b57148019a942412" + + "6a94e8791762bd4154331d00ac4705df000000006a47304402201656a4834651f39ac5" + + "2eb866042ca7ede052ac843a914da4790573122c8e2ab302200af617e856abf4f8fb8d" + + "8086825dc63766943b4866ad3d6b4c8f222017c9b402012102d547eb1c5672a4d212de" + + "3c797a87b2b8fe731c2b502db6d7ad044850fe11d78fffffffff02a0fe7d0300000000" + + "1976a91462b9991cb8310a8a74bd6359e954fb812fa7d37b88acced877020000000019" + + "76a9144ff496917bae33309a8ad70bec81355bbf92988988ac00000000000000000000" + + "000000000000000000") + + tx, err := DeserializeZecTx(txB) + if err != nil { + t.Fatalf("error decoding tx: %v", err) + } + + 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(txB)) { + t.Fatalf("wrong calculated tx size. wanted %d, got %d", len(txB), sz) + } + + serializedTx, err := tx.Bytes() + if err != nil { + t.Fatalf("error re-serializing: %v", err) + } + if !bytes.Equal(serializedTx, txB) { + t.Fatalf("re-encoding does not match original") + } +} + +func TestShieldedTx(t *testing.T) { + txB, _ := hex.DecodeString("0400008085202f89000000000000e6ba1800e8030000000" + + "000000164873d5fc82f2480cba8b884e0854eae7f5b688740f7e9747741edb459732ee" + + "151088c01837a0d0bee69c35b058af0f317b641b9b978f988242e8d9ddbf29b524964d" + + "d00cef61f4f841960c7e8ef7fc00bc2762a05acc375920b0301908c81d73489be2ed11" + + "54f9b9e3e1aa98a97cefaec95458f4c04876c54219db5e39b06dbb47a4cf7d33c5cd57" + + "98ded111230d70c1f02ad118f54aabd6ff5f90404a0516dd09db9f8ed2107b21d975f2" + + "ddd9b40b0a82f323df447b247b99d347167e7fc4a0d3d82664cb8ecf6b56c83891ce50" + + "6c85206ea3d3715823ccc388a799aed215616d7cac52abc254a5773bb272685392a64b" + + "6bbcbda279dd3dd0c4a58f6658d6b07b08f33a57c3311a4bfc945f1dc0798a0a42a565" + + "1bdafa643ae168af3bc3ff0786e8f6fe6760f33eb4ccfea8e42680344c2ef2f995360b" + + "ed128714440248c095fb8ad678e6c5d2bd55f540c0d3cd0e7f5b85912bf53b22d60fe6" + + "8b050d7dee38267461edf0c3e26a0c109df3c66dc7f926832da044e81d6ed1079b22f4" + + "ed50102917b35881933eb2d62e6f40f5f7ce5fa5c1696b2f9cb673b1391ba52b37c651" + + "7a99dac81338796fff6f148cc0687ab239053c2deeb9714d6f367ffd0859b0733236cd" + + "cb28485f8e4397ec9cbb51fad781a239d4489f42b22fb3210e485afb4020ddd2e04d8b" + + "38b17f56fbbf14f1071f8bbec9eecf1b0fb305f7cd157129057a638e4226ef189bc30f" + + "151e0445f865aa0fa7b1e0c85d76ee319b33e86257366982556498f8629d9a7c5a3cda" + + "b3b5657514c5726ce22177e5087664ebf328d38d80e44bbf009b8db43ced4e025d54da" + + "930aa5f28a5ca1f8b6e903cc03377ce94b2dbec72602448ecf8418eaf7ceff76e15d4d" + + "12289f06a0df37598b587960448a1cf38ec7969b9a42beb8ddc35f200dd5beace2e0ab" + + "283508dbed45a5e394fe8571cee5d828bcb16e01870ff770254f64d71809292e80e813" + + "d8e0c683df3da69a8b42bc10b6bcbdbd8b705b0d5be474ef58f54497b9c943694a517f" + + "851c6b7d6c2da569fe2a5d7d2b4da9a4b99b635299d33c0357616b0d67d06a67d88158" + + "51a058c875520d3090e7b51d562ba7648e6ba7e21a28492e79b65b009f79b973f7995a" + + "c053fe490b252da3f5e51f1b6316256e03989d12b3a69dd0a9e451290d2bdf00054a12" + + "0813cc0d74c07793b2000a4c8a4243952d540bf971673f01b2de9a7679507043d73e89" + + "b27759c898f3a40bfcc0ccd1d56352334d3622e3ec0e2036b6ac5d3238b667e2629838" + + "348013d58506995c24e209e67a1d5cbe99bd8764bd571744bf206731c3e35b1afc8542" + + "aec0dc85a69e1985d9797a2e218dddd39df094ee4995a267b7fb65340356904693cd69" + + "d23bee1e3e12c03e9a59262eaa016581b530deb72b6f313a040ba4d90153a1509e7b24" + + "df7c317e898f90044578feab15117c04b30e9d06bf0de9319387702a0d4f21a296315f" + + "e6c2167e88749c991b2b51fec5207e5d0edf8990d493a793a0bcf373c03f8b6b436ff9" + + "b48832f4833c28270640ea25800e6dc41980820b02076de0990de1891717a0f1f4a85a" + + "b7f54134151fb55e1d302348d026edc30f524627c04ae1099b7b3d8c7ea0fc257001a5" + + "0d95afc93b2df78a3aeda58f65f2e820a3b53d4f03fc4977cda9115dc7e24b2301450b" + + "664e69235eaff857eb1b5206f22e5f18b4fa20b9b0707169223b4e37fe7f19ebca2f5b" + + "6baea625d98cff9a7e6e9d2eba255800af0e4674bd0aea7eedef463bd74e16379930e0" + + "e328b20e8c7b3056b21be67dd740de7b68549674cec867076b94b8265b31a452b5d942" + + "34b3c5ccc3292e5acd6ff0c087eddbe1978909748bc56aaf98ad6494bd5132b38f1894" + + "ac1d2a274ccdfc7bfbde908def501940819050196c451d656e1fad85d7eca7a68a901d" + + "3726331ee5fdfec0db09e1213cd5ef3334f22d465c9329bf1794f2e4c1d21e47df8ca0" + + "2ffe71a33979a0ea1df046240b661931eddeb3a61323c5af09bf8e58d243f2d2362ce2" + + "6892ccb58743c3399ae203aba7585a8e4e31bfb834d1d6d2e3f43c5eb46aaddddf214c" + + "3ade0a5174881868f53597e63ce63f0f06e13ce92935a8fe0c953b62bcd68a5f8d26c7" + + "7979ae1002d8e8b95984cadd8f0c152fa2de6133b28b2176bab50542b2736549e815b5" + + "354b1adb5a1859ef32ae81d70ed015a012674f7c8054802a631e12793620ca9d2e0475" + + "a7b3abcaa76cd12c5da569e4b04f9f7aea91e244ea3f65cbb09805ee77c991d1f51c80" + + "8ec22160e8aaab6aada6cc7319adc3efc69330336dd53d935ae6987bad0398820270f5" + + "34abb4c5149acc1b8f391ab1e001560d7ff7d0e84f92fba31f274a18ccae54aac97bec" + + "7f99c8114301f8f2abaa4f8ca89afab825bf44803259c21d18840ef76d169f25512a66" + + "4e8d2081664cb941dc7198243248d2f11372f2045c828d516b896865fe9c3f15be24f3" + + "9a7e4289580cd101a1ee60eb53cba37797ec54391e8531c601c183cc45a94098e5e372" + + "8888cc9488365da3026bdf7a4203c5eadd93146ea848d9606c8fae22e041700ac95da0" + + "2b64c89f7219003cecb2b74e7e001fe7d83c3a9598496760bb6c09830f5c0a87998e37" + + "5f98d13d2bdfbc7d8111570e88c6c1e4342f78d1d16ff13fc88e8a15eb66fa88e75ce5" + + "ae65d8739a4876ee60c559580a3dd568f9deb7b93026bae4dd955d3e9e0a229049bcc3" + + "0b4b941b8635ddc341f7a80386ad1a8ab8be9c4a30e69d2c5ddc66b06e511fbbe3cbe2" + + "af8bd5b2c02d2ff3a2180d807cae66300498093446036845a0bd901eba50fec22f2aad" + + "272390a125245b72aabdd51ce90e48fe5ff15324cad083fc4d159d49a3a8a51daba21d" + + "e3d189c41be121dbc0d1279fcfe05c75f003541e7fbd6b069c7724c877a328fe6d7dd2" + + "76f4baa102c458c722f1005927ebd2a41ee00ba57709e90fb8f4bbe8982e47433cac69" + + "6552bf1dc33760509a854e37ed74afbbe7665c857e011ce2af742dad1b2a20543bd68d" + + "078362e61c5b09f14e2d89a0fa4fe438b9542a8123097426a42a5e41b5b20cfddfac3a" + + "2b9effd3b058b95fb0eeb5dfa4af0c45bb47611a33b33211c61510897d19cc5eabc18a" + + "39d14be1b9696742c8008f65213a57e3a636472716cbfc784e29fb0400ed993af69a49" + + "7e25ceced20d6169738fdcd9693d892bfe276753be8902ad50f4619b64c838ce9aba7d" + + "aeabe9970bebf7a51d10008a570ee2859b707146423485df9b620e10f0175617e605d7" + + "4b025c0cbad92ecedb1f715c9d1ee102bdff8919b6acb360dc89dd68bef2d94da8e622" + + "feaa7d50c") + + // Just make sure it doesn't error. + + if _, err := DeserializeZecTx(txB); err != nil { + t.Fatalf("error decoding tx: %v", err) + } +} diff --git a/dex/testing/dcrdex/harness.sh b/dex/testing/dcrdex/harness.sh index 672a52ed23..180c067fc1 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": 100000, + "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/zcash.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 100644 index 0000000000..f215800a4e --- /dev/null +++ b/dex/testing/zec/harness.sh @@ -0,0 +1,232 @@ +#!/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" +ALPHA_RPC_PORT="33766" +BETA_RPC_PORT="33767" + +set -ex +NODES_ROOT=~/dextest/${SYMBOL} +rm -rf "${NODES_ROOT}" +SOURCE_DIR=$(pwd) + +ALPHA_DIR="${NODES_ROOT}/alpha" +BETA_DIR="${NODES_ROOT}/beta" +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" + +# 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 ${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}/zcash.conf" < "${BETA_DIR}/zcash.conf" < "./alpha" < "./mine-alpha" < "./beta" < "./mine-beta" < "./reorg" < "./new-wallet" < "${HARNESS_DIR}/quit" < 0 { + in += uint64(tx.ValueBalance) + } else if tx.ValueBalance < 0 { + out += uint64(-1 * tx.ValueBalance) + } + 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/doge/doge.go b/server/asset/doge/doge.go index da983c28bb..5567657f0e 100644 --- a/server/asset/doge/doge.go +++ b/server/asset/doge/doge.go @@ -92,7 +92,6 @@ func NewBackend(configPath string, logger dex.Logger, network dex.Network) (asse ChainParams: params, Ports: ports, FeeEstimator: func(cl *btc.RPCClient) (uint64, error) { - var r float64 if err := cl.Call("estimatefee", []interface{}{feeConfs}, &r); err != nil { return 0, err 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..c1c7cd0d65 --- /dev/null +++ b/server/asset/zec/zec.go @@ -0,0 +1,146 @@ +// 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 ( + "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/chaincfg" + "github.com/btcsuite/btcutil" + dcrchaincfg "github.com/decred/dcrd/chaincfg/v3" +) + +// 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 *dcrchaincfg.Params + 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) + }, + FeeEstimator: func(cl *btc.RPCClient) (uint64, error) { + // ZCash estimateFee + var r float64 + if err := cl.Call("estimatefee", []interface{}{feeConfs}, &r); err != nil { + return 0, err + } + if r <= 0 { + return 0, fmt.Errorf("fee could not be estimated") + } + return uint64(math.Round(r * 1e8)), nil + }, + InitTxSize: dexzec.InitTxSize, + InitTxSizeBase: dexzec.InitTxSizeBase, + NumericGetRawRPC: true, + }) + 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 *dcrchaincfg.Params +} + +// 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 +} 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)