diff --git a/client/wallet/dcr/apisemver.go b/client/wallet/dcr/apisemver.go new file mode 100644 index 0000000000..48148a4342 --- /dev/null +++ b/client/wallet/dcr/apisemver.go @@ -0,0 +1,95 @@ +// 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 dcr + +import ( + "fmt" + + dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2" +) + +var ( + // Expected api and daemon versions. Currently placeholders. + dcrd = &dcrdtypes.VersionResult{ + VersionString: "1.5.0", + Major: 1, + Minor: 5, + Patch: 0, + } + + dcrdjsonrpcapi = &dcrdtypes.VersionResult{ + VersionString: "6.1.0", + Major: 6, + Minor: 1, + Patch: 0, + } + + dcrwalletjsonrpcapi = &dcrdtypes.VersionResult{ + VersionString: "6.2.0", + Major: 6, + Minor: 2, + Patch: 0, + } +) + +// checkSemVer asserts the provided semantic version is at least equal to or +// better than the expected version. +func checkSemVer(id string, expected *dcrdtypes.VersionResult, provided *dcrdtypes.VersionResult) error { + if provided.Major < expected.Major { + return fmt.Errorf("%s's major version (%s) is lower than "+ + "expected (%v)", id, provided.VersionString, + expected.VersionString) + } + + if provided.Major == expected.Major { + if provided.Minor < expected.Minor { + return fmt.Errorf("%s's minor version (%s) is lower than "+ + "expected (%v)", id, provided.VersionString, + expected.VersionString) + } + + if provided.Minor == expected.Minor { + if provided.Patch < expected.Patch { + return fmt.Errorf("%s's patch version (%s) is lower than "+ + "expected (%v)", id, provided.VersionString, + expected.VersionString) + } + } + } + + return nil +} + +// checkVersionInfo ensures the provided api version info are at least +// the expected or better. +func checkVersionInfo(versionInfo map[string]dcrdtypes.VersionResult, api ...string) error { + for _, id := range api { + semver, ok := versionInfo[id] + if !ok { + return fmt.Errorf("no version info found for %s", id) + } + + var expected *dcrdtypes.VersionResult + switch id { + case "dcrd": + expected = dcrd + + case "dcrdjsonrpcapi": + expected = dcrdjsonrpcapi + + case "dcrwalletjsonrpcapi": + expected = dcrwalletjsonrpcapi + + default: + return fmt.Errorf("unknown api id provided: %s", id) + } + + err := checkSemVer(id, expected, &semver) + if err != nil { + return err + } + } + + return nil +} diff --git a/client/wallet/dcr/apisemver_test.go b/client/wallet/dcr/apisemver_test.go new file mode 100644 index 0000000000..1ee0cf0914 --- /dev/null +++ b/client/wallet/dcr/apisemver_test.go @@ -0,0 +1,216 @@ +package dcr + +import ( + "testing" + + dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2" +) + +func TestCheckSemVer(t *testing.T) { + tests := []struct { + expected *dcrdtypes.VersionResult + provided *dcrdtypes.VersionResult + wantErr bool + }{ + { + expected: &dcrdtypes.VersionResult{ + VersionString: "1.5.1", + Major: 1, + Minor: 5, + Patch: 1, + }, + provided: &dcrdtypes.VersionResult{ + VersionString: "1.5.1", + Major: 1, + Minor: 5, + Patch: 1, + }, + wantErr: false, + }, + { + expected: &dcrdtypes.VersionResult{ + VersionString: "1.5.1", + Major: 1, + Minor: 5, + Patch: 1, + }, + provided: &dcrdtypes.VersionResult{ + VersionString: "1.5.2", + Major: 1, + Minor: 5, + Patch: 2, + }, + wantErr: false, + }, + { + expected: &dcrdtypes.VersionResult{ + VersionString: "1.5.1", + Major: 1, + Minor: 5, + Patch: 1, + }, + provided: &dcrdtypes.VersionResult{ + VersionString: "1.5.0", + Major: 1, + Minor: 5, + Patch: 0, + }, + wantErr: true, + }, + { + expected: &dcrdtypes.VersionResult{ + VersionString: "1.5.1", + Major: 1, + Minor: 5, + Patch: 1, + }, + provided: &dcrdtypes.VersionResult{ + VersionString: "1.4.1", + Major: 1, + Minor: 4, + Patch: 1, + }, + wantErr: true, + }, + { + expected: &dcrdtypes.VersionResult{ + VersionString: "1.5.1", + Major: 1, + Minor: 5, + Patch: 1, + }, + provided: &dcrdtypes.VersionResult{ + VersionString: "0.4.1", + Major: 0, + Minor: 4, + Patch: 1, + }, + wantErr: true, + }, + { + expected: &dcrdtypes.VersionResult{ + VersionString: "1.5.1", + Major: 1, + Minor: 5, + Patch: 1, + }, + provided: &dcrdtypes.VersionResult{ + VersionString: "2.0.0", + Major: 2, + Minor: 0, + Patch: 0, + }, + wantErr: false, + }, + } + + for idx, tc := range tests { + err := checkSemVer("dcrd", tc.expected, tc.provided) + if (err != nil) != tc.wantErr { + t.Errorf("[checkSemVer] #%d: error: %v, wantErr: %v", + idx+1, err, tc.wantErr) + } + } +} + +func TestCheckVersionInfo(t *testing.T) { + tests := []struct { + info map[string]dcrdtypes.VersionResult + wantErr bool + }{ + { + info: map[string]dcrdtypes.VersionResult{ + "dcrd": dcrdtypes.VersionResult{ + VersionString: "1.5.0", + Major: 1, + Minor: 5, + Patch: 0, + }, + "dcrdjsonrpcapi": dcrdtypes.VersionResult{ + VersionString: "6.1.0", + Major: 6, + Minor: 1, + Patch: 0, + }, + "dcrwalletjsonrpcapi": dcrdtypes.VersionResult{ + VersionString: "6.2.0", + Major: 6, + Minor: 2, + Patch: 0, + }, + }, + wantErr: false, + }, + { + info: map[string]dcrdtypes.VersionResult{ + "wrongid": dcrdtypes.VersionResult{ + VersionString: "1.5.0", + Major: 1, + Minor: 5, + Patch: 0, + }, + "dcrdjsonrpcapi": dcrdtypes.VersionResult{ + VersionString: "6.1.0", + Major: 6, + Minor: 1, + Patch: 0, + }, + "dcrwalletjsonrpcapi": dcrdtypes.VersionResult{ + VersionString: "6.2.0", + Major: 6, + Minor: 2, + Patch: 0, + }, + }, + wantErr: true, + }, + { + info: map[string]dcrdtypes.VersionResult{ + "dcrd": dcrdtypes.VersionResult{ + VersionString: "1.4.0", + Major: 1, + Minor: 4, + Patch: 0, + }, + "dcrdjsonrpcapi": dcrdtypes.VersionResult{ + VersionString: "6.1.0", + Major: 6, + Minor: 1, + Patch: 0, + }, + "dcrwalletjsonrpcapi": dcrdtypes.VersionResult{ + VersionString: "6.2.0", + Major: 6, + Minor: 2, + Patch: 0, + }, + }, + wantErr: true, + }, + { + info: map[string]dcrdtypes.VersionResult{ + "dcrd": dcrdtypes.VersionResult{ + VersionString: "1.5.0", + Major: 1, + Minor: 5, + Patch: 0, + }, + "dcrwalletjsonrpcapi": dcrdtypes.VersionResult{ + VersionString: "6.2.0", + Major: 6, + Minor: 2, + Patch: 0, + }, + }, + wantErr: true, + }, + } + + for idx, tc := range tests { + err := checkVersionInfo(tc.info, "dcrd", "dcrdjsonrpcapi", "dcrwalletjsonrpcapi") + if (err != nil) != tc.wantErr { + t.Errorf("[checkVersionInfo] #%d: error: %v, wantErr: %v", + idx+1, err, tc.wantErr) + } + } +} diff --git a/client/wallet/dcr/client.go b/client/wallet/dcr/client.go new file mode 100644 index 0000000000..422402eb7f --- /dev/null +++ b/client/wallet/dcr/client.go @@ -0,0 +1,289 @@ +// 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 dcr + +import ( + "fmt" + "time" + + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/chaincfg/v2" + "github.com/decred/dcrd/dcrutil/v2" + "github.com/decred/dcrd/rpcclient/v5" + "github.com/decred/dcrd/wire" + "github.com/decred/dcrwallet/rpc/jsonrpc/types" +) + +// Config represents the configuration details for a decred wallet client. +type Config struct { + // The wallet rpc user. + User string + // The wallet rpc user password. + Pass string + // The wallet password. + WalletPass string + // The host address. + Host string + // The wallet TLS certificate bytes. + Certs []byte +} + +// Client represents a decred wallet client. +type Client struct { + cfg *Config + params *chaincfg.Params + rpcc *rpcclient.Client + relayFeePerKb dcrutil.Amount +} + +var ( + // Wallet unlock timeout in seconds. + rpcUnlockTimeoutSecs = 5 +) + +// NewClient creates a decred wallet client. The created client is required to +// have a connection to a consensus daemon and also able to unlock the wallet. +func NewClient(cfg *Config) (*Client, error) { + rpcCfg := &rpcclient.ConnConfig{ + Host: cfg.Host, + Endpoint: "ws", + User: cfg.User, + Pass: cfg.Pass, + Certificates: cfg.Certs, + } + + rpcc, err := rpcclient.New(rpcCfg, nil) + if err != nil { + return nil, err + } + + c := &Client{ + cfg: cfg, + rpcc: rpcc, + } + + // Ensure the wallet is connected to a consensus daemon. + walletInfo, err := c.rpcc.WalletInfo() + if err != nil { + return nil, err + } + + if !walletInfo.DaemonConnected { + return nil, fmt.Errorf("the decred wallet client requires a " + + "daemon connection") + } + + // Get the tx fee per kb. + txFee, err := dcrutil.NewAmount(walletInfo.TxFee) + if err != nil { + return nil, fmt.Errorf("unable to parse tx fee per kb: %v", err) + } + + c.relayFeePerKb = txFee + + // Get the current network. + net, err := c.rpcc.GetCurrentNet() + if err != nil { + return nil, fmt.Errorf("unable to get the current network: %v", err) + } + + switch net { + case wire.MainNet: + c.params = chaincfg.MainNetParams() + + case wire.TestNet3: + c.params = chaincfg.TestNet3Params() + + case wire.SimNet: + c.params = chaincfg.SimNetParams() + + default: + return nil, fmt.Errorf("unknown network: %v", net) + } + + // Ensure the daemon and jsonrpc api versions are the expected or better. + vInfo, err := c.rpcc.Version() + if err != nil { + return nil, err + } + + err = checkVersionInfo(vInfo, "dcrd", "dcrdjsonrpcapi", "dcrwalletjsonrpcapi") + if err != nil { + return nil, err + } + + // Ensure the client can unlock the wallet. + err = c.unlock() + if err != nil { + return nil, err + } + + return c, nil +} + +// unlock is a convenience function that unlocks the wallet with a reasonable +// timeout. +func (c *Client) unlock() error { + err := c.rpcc.WalletPassphrase(c.cfg.WalletPass, int64(rpcUnlockTimeoutSecs)) + if err != nil { + return err + } + + return nil +} + +// ListUnspent is a wrapper for the listunspent rpc. +func (c *Client) ListUnspent() ([]types.ListUnspentResult, error) { + err := c.unlock() + if err != nil { + return nil, err + } + + return c.rpcc.ListUnspent() +} + +// LockUnspent is a wrapper for the lockunspent rpc. +func (c *Client) LockUnspent(unlock bool, ops []types.ListUnspentResult) error { + err := c.unlock() + if err != nil { + return err + } + + outputs := make([]*wire.OutPoint, len(ops)) + for idx, out := range ops { + hash, err := chainhash.NewHashFromStr(out.TxID) + if err != nil { + return err + } + + outputs[idx] = &wire.OutPoint{ + Hash: *hash, + Index: out.Vout, + Tree: out.Tree, + } + } + + return c.rpcc.LockUnspent(unlock, outputs) +} + +// ListLockUnspent is a wrapper for the listlockunspent rpc. +func (c *Client) ListLockUnspent() ([]*wire.OutPoint, error) { + err := c.unlock() + if err != nil { + return nil, err + } + + return c.rpcc.ListLockUnspent() +} + +// signRawTransaction is a wrapper for the signrawtransaction rpc. +func (c *Client) signRawTransaction(tx *wire.MsgTx) (*wire.MsgTx, bool, error) { + err := c.unlock() + if err != nil { + return nil, false, err + } + + return c.rpcc.SignRawTransaction(tx) +} + +// sendRawTransaction is a wrapper for the sendrawtransaction rpc. +func (c *Client) sendRawTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { + err := c.unlock() + if err != nil { + return nil, err + } + + return c.rpcc.SendRawTransaction(tx, true) +} + +// SignMessage is a wrapper for the signmessage rpc. +func (c *Client) SignMessage(address string, message string) (string, error) { + err := c.unlock() + if err != nil { + return "", err + } + + addr, err := dcrutil.DecodeAddress(address, c.params) + if err != nil { + return "", err + } + + return c.rpcc.SignMessage(addr, message) +} + +// getNewAddress is a wrapper for the getnewaddress rpc, it unlocks the wallet +// before calling the rpc.- +func (c *Client) getNewAddress() (dcrutil.Address, error) { + err := c.unlock() + if err != nil { + return nil, err + } + + addr, err := c.rpcc.GetNewAddress("default", c.params) // what about other accounts? + return addr, err +} + +// generateNewAddresses is a helper for generating a collection of new addresses. +func (c *Client) generateNewAddresses(count uint64) ([]string, error) { + addrs := make([]string, count) + for idx := uint64(0); idx < count; idx++ { + addr, err := c.getNewAddress() + if err != nil { + return nil, err + } + + addrs[idx] = addr.String() + } + + return addrs, nil +} + +// Swap creates, signs and broadcasts a swap transaction using the provided +// inputs and counterparty destinations and associated amounts. +func (c *Client) Swap(inputs []types.ListUnspentResult, spends map[string]int64) (string, error) { + redeemAddrs, err := c.generateNewAddresses(uint64(len(spends))) + if err != nil { + return "", fmt.Errorf("unable to generate redeem addresses: %v", err) + } + + ctpAddrs := make([]string, 0) + for addr := range spends { + ctpAddrs = append(ctpAddrs, addr) + } + + locktime := uint32(time.Now().Add(48 * time.Hour).Unix()) + + contracts, err := generateContracts(redeemAddrs, ctpAddrs, locktime, c.params) + if err != nil { + return "", fmt.Errorf("unable to generate swap contracts: %v", err) + } + + contractPkScripts, err := generateSwapPkScripts(spends, contracts, c.params) + if err != nil { + return "", fmt.Errorf("unable to generate contract pksripts: %v", err) + } + + changeAddr, err := c.getNewAddress() + if err != nil { + return "", fmt.Errorf("unable to generate change address: %v", err) + } + + tx, err := createTransaction(inputs, spends, contractPkScripts, locktime, + c.relayFeePerKb, changeAddr, c.params) + if err != nil { + return "", fmt.Errorf("unable to create swap transaction: %v", err) + } + + signedTx, _, err := c.signRawTransaction(tx) + if err != nil { + return "", fmt.Errorf("unable to sign transaction: %v", err) + } + + txHash, err := c.sendRawTransaction(signedTx) + if err != nil { + return "", fmt.Errorf("unable to send transaction: %v", err) + } + + return txHash.String(), nil +} diff --git a/client/wallet/dcr/contract.go b/client/wallet/dcr/contract.go new file mode 100644 index 0000000000..db036a74ff --- /dev/null +++ b/client/wallet/dcr/contract.go @@ -0,0 +1,214 @@ +// Copyright (c) 2016 The btcsuite developers +// Copyright (c) 2016-2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package dcr + +import ( + "crypto/rand" + "crypto/sha256" + "fmt" + + "github.com/decred/dcrd/chaincfg/v2" + "github.com/decred/dcrd/dcrutil/v2" + "github.com/decred/dcrd/txscript/v2" + "github.com/decred/dcrd/wire" + "golang.org/x/crypto/ripemd160" +) + +const ( + secretSize = 32 +) + +// atomicSwapContract returns an output script that may be redeemed by one of +// two signature scripts: +// +// 1 +// +// 0 +// +// The first signature script is the normal redemption path done by the other +// party and requires the initiator's secret. The second signature script is +// the refund path performed by us, but the refund can only be performed after +// locktime. +func atomicSwapContract(pkhMe, pkhThem *[ripemd160.Size]byte, locktime int64, secretHash []byte) ([]byte, error) { + b := txscript.NewScriptBuilder() + + b.AddOp(txscript.OP_IF) // Normal redeem path + { + // Require initiator's secret to be a known length that the redeeming + // party can audit. This is used to prevent fraud attacks between two + // currencies that have different maximum data sizes. + b.AddOp(txscript.OP_SIZE) + b.AddInt64(secretSize) + b.AddOp(txscript.OP_EQUALVERIFY) + + // Require initiator's secret to be known to redeem the output. + b.AddOp(txscript.OP_SHA256) + b.AddData(secretHash) + b.AddOp(txscript.OP_EQUALVERIFY) + + // Verify their signature is being used to redeem the output. This + // would normally end with OP_EQUALVERIFY OP_CHECKSIG but this has been + // moved outside of the branch to save a couple bytes. + b.AddOp(txscript.OP_DUP) + b.AddOp(txscript.OP_HASH160) + b.AddData(pkhThem[:]) + } + b.AddOp(txscript.OP_ELSE) // Refund path + { + // Verify locktime and drop it off the stack (which is not done by + // CLTV). + b.AddInt64(locktime) + b.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY) + b.AddOp(txscript.OP_DROP) + + // Verify our signature is being used to redeem the output. This would + // normally end with OP_EQUALVERIFY OP_CHECKSIG but this has been moved + // outside of the branch to save a couple bytes. + b.AddOp(txscript.OP_DUP) + b.AddOp(txscript.OP_HASH160) + b.AddData(pkhMe[:]) + } + b.AddOp(txscript.OP_ENDIF) + + // Complete the signature check. + b.AddOp(txscript.OP_EQUALVERIFY) + b.AddOp(txscript.OP_CHECKSIG) + + return b.Script() +} + +// redeemP2SHContract returns the signature script to redeem a contract output +// using the redeemer's signature and the initiator's secret. This function +// assumes P2SH and appends the contract as the final data push. +func redeemP2SHContract(contract, sig, pubkey, secret []byte) ([]byte, error) { + b := txscript.NewScriptBuilder() + b.AddData(sig) + b.AddData(pubkey) + b.AddData(secret) + b.AddInt64(1) + b.AddData(contract) + return b.Script() +} + +// refundP2SHContract returns the signature script to refund a contract output +// using the contract author's signature after the locktime has been reached. +// This function assumes P2SH and appends the contract as the final data push. +func refundP2SHContract(contract, sig, pubkey []byte) ([]byte, error) { + b := txscript.NewScriptBuilder() + b.AddData(sig) + b.AddData(pubkey) + b.AddInt64(0) + b.AddData(contract) + return b.Script() +} + +// sha254Hash returns a sha256 hash of the provided bytes. +func sha256Hash(x []byte) []byte { + h := sha256.Sum256(x) + return h[:] +} + +// newSecret generated a random 32-byte secret. +func newSecret() ([]byte, error) { + var secret [secretSize]byte + _, err := rand.Read(secret[:]) + if err != nil { + return nil, err + } + + return secret[:], nil +} + +// generateContractP2SHPkScript creates a P2SHPk script of the provided contract. +func generateContractP2SHPkScript(contract []byte, params *chaincfg.Params) ([]byte, error) { + contractP2SH, err := dcrutil.NewAddressScriptHash(contract, params) + if err != nil { + return nil, err + } + + contractP2SHPkScript, err := txscript.PayToAddrScript(contractP2SH) + if err != nil { + return nil, err + } + + return contractP2SHPkScript, nil +} + +// Contract represents the swap contract details. +type Contract struct { + Secret []byte + RedeemAddr dcrutil.Address + CounterPartyAddr dcrutil.Address + ContractData []byte + LockTime uint32 +} + +// newContract creates a swap contract from the provided address and lock time. +func newContract(redeemAddr string, counterpartyAddr string, lockTime uint32, params *chaincfg.Params) (*Contract, error) { + if uint32(lockTime) > wire.MaxTxInSequenceNum { + return nil, fmt.Errorf("lockTime out of range") + } + + secret, err := newSecret() + if err != nil { + return nil, err + } + + addrMe, err := dcrutil.DecodeAddress(redeemAddr, params) + if err != nil { + return nil, err + } + + addrThem, err := dcrutil.DecodeAddress(counterpartyAddr, params) + if err != nil { + return nil, err + } + + secretHash := sha256Hash(secret) + + contract, err := atomicSwapContract(addrMe.Hash160(), addrThem.Hash160(), + int64(lockTime), secretHash) + if err != nil { + return nil, err + } + + return &Contract{ + Secret: secret, + RedeemAddr: addrMe, + CounterPartyAddr: addrThem, + ContractData: contract, + LockTime: lockTime, + }, nil +} + +// generateContracts create contracts from the provided redeem and counterparty addresses. +func generateContracts(redeemAddrs []string, counterpartyAddrs []string, lockTime uint32, params *chaincfg.Params) ([]*Contract, error) { + if len(counterpartyAddrs) == 0 { + return nil, fmt.Errorf("no counterparty addresses provided") + } + + if len(redeemAddrs) == 0 { + return nil, fmt.Errorf("no redeem addresses provided") + } + + if len(redeemAddrs) != len(counterpartyAddrs) { + return nil, fmt.Errorf("counterparty addresses size should be equal" + + " to the redeem addresses size") + } + + contracts := make([]*Contract, len(redeemAddrs)) + for idx, addr := range redeemAddrs { + ctpAddr := counterpartyAddrs[idx] + contract, err := newContract(addr, ctpAddr, lockTime, params) + if err != nil { + return nil, fmt.Errorf("unable to create contract: %v", err) + } + + contracts[idx] = contract + } + + return contracts, nil +} diff --git a/client/wallet/dcr/contract_test.go b/client/wallet/dcr/contract_test.go new file mode 100644 index 0000000000..b1902252e3 --- /dev/null +++ b/client/wallet/dcr/contract_test.go @@ -0,0 +1,85 @@ +package dcr + +import ( + "bytes" + "testing" + + "github.com/decred/dcrd/chaincfg/v2" + "github.com/decred/dcrd/dcrutil/v2" +) + +func TestGenerateContracts(t *testing.T) { + tests := []struct { + redeemAddrs []string + ctpAddrs []string + params *chaincfg.Params + wantErr bool + }{ + { + redeemAddrs: []string{ + "DsaRVfWLBrGYpvUW6o8jDS3iSoHQQcx1wx2", + "DsnHdhaBNtaahvjDPAgmsMCb9QH6LW69XsU", + }, + ctpAddrs: []string{ + "DsmEiHKgE1PkovBCB6xkFGDLxdhhXQ31sgV", + "DsYy9NLvAmhbpBPSDrfF87GJyZ9rSm9XXP3", + }, + params: chaincfg.MainNetParams(), + wantErr: false, + }, + { + redeemAddrs: []string{ + "DsaRVfWLBrGYpvUW6o8jDS3iSoHQQcx1wx2", + "DsnHdhaBNtaahvjDPAgmsMCb9QH6LW69XsU", + }, + ctpAddrs: []string{ + "DsmEiHKgE1PkovBCB6xkFGDLxdhhXQ31sgV", + }, + params: chaincfg.MainNetParams(), + wantErr: true, + }, + } + + for idx, tc := range tests { + contracts, err := generateContracts(tc.redeemAddrs, tc.ctpAddrs, 100, tc.params) + if (err != nil) != tc.wantErr { + t.Fatalf("[generateContracts] #%d: error: %v, wantErr: %v", + idx+1, err, tc.wantErr) + } + + if !tc.wantErr { + for i, addr := range tc.redeemAddrs { + redeemAddr, err := dcrutil.DecodeAddress(addr, tc.params) + if err != nil { + t.Fatalf("unexpected redeem address err %v", addr) + } + + ctpAddr, err := dcrutil.DecodeAddress(tc.ctpAddrs[i], tc.params) + if err != nil { + t.Fatalf("unexpected counterparty address err %v", addr) + } + + contract := contracts[i] + if contract.RedeemAddr.String() != redeemAddr.String() { + t.Fatalf("expected %s redeem address for contract", addr) + } + + if contract.CounterPartyAddr.String() != ctpAddr.String() { + t.Fatalf("expected %s counterparty address for contract", ctpAddr) + } + + expectedContractData, err := atomicSwapContract(redeemAddr.Hash160(), ctpAddr.Hash160(), + int64(contract.LockTime), sha256Hash(contract.Secret)) + if err != nil { + t.Fatalf("unexpected contract error %v", err) + } + + if !bytes.Equal(expectedContractData, contract.ContractData) { + t.Fatalf("generated contract data (%x) is not equal to "+ + " expected contract data (%x)", expectedContractData, + contract.ContractData) + } + } + } + } +} diff --git a/client/wallet/dcr/createtx.go b/client/wallet/dcr/createtx.go new file mode 100644 index 0000000000..f36810340a --- /dev/null +++ b/client/wallet/dcr/createtx.go @@ -0,0 +1,229 @@ +// Copyright (c) 2016 The btcsuite developers +// Copyright (c) 2016-2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package dcr + +import ( + "encoding/hex" + "fmt" + + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/chaincfg/v2" + "github.com/decred/dcrd/dcrutil/v2" + "github.com/decred/dcrd/txscript/v2" + "github.com/decred/dcrd/wire" + "github.com/decred/dcrwallet/rpc/jsonrpc/types" + "github.com/decred/dcrwallet/wallet/v3/txrules" +) + +// sumOutputValues returns the total output amount of the provided output set. +func sumOutputValues(outputs []*wire.TxOut) dcrutil.Amount { + var totalOutput dcrutil.Amount + for _, txOut := range outputs { + totalOutput += dcrutil.Amount(txOut.Value) + } + return totalOutput +} + +// sumInputValues returns the total input amount of the provided input set. +func sumInputValues(inputs []*wire.TxIn) dcrutil.Amount { + var totalInput dcrutil.Amount + for _, txIn := range inputs { + totalInput += dcrutil.Amount(txIn.ValueIn) + } + return totalInput +} + +// createTransaction generates a transaction using the provided inputs and amounts. +// The transaction defaults to creating P2PKH scripts if no pkScripts are provided. +func createTransaction(inputs []types.ListUnspentResult, amounts map[string]int64, + pkScripts map[string][]byte, lockTime uint32, relayFee dcrutil.Amount, + changeAddr dcrutil.Address, params *chaincfg.Params) (*wire.MsgTx, error) { + if lockTime > wire.MaxTxInSequenceNum { + return nil, fmt.Errorf("lockTime out of range") + } + + inputSizes := make([]int, len(inputs)) + + // Add all transaction inputs to a new transaction after performing + // some validity checks. + tx := wire.NewMsgTx() + for idx, input := range inputs { + txHash, err := chainhash.NewHashFromStr(input.TxID) + if err != nil { + return nil, err + } + + if !(input.Tree == wire.TxTreeRegular || input.Tree == wire.TxTreeStake) { + return nil, fmt.Errorf("tx tree must be regular or stake") + } + + amt, err := dcrutil.NewAmount(input.Amount) + if err != nil { + return nil, err + } + + prevOutV := int64(amt) + prevOut := wire.NewOutPoint(txHash, input.Vout, input.Tree) + txIn := wire.NewTxIn(prevOut, prevOutV, []byte{}) + if lockTime != 0 { + txIn.Sequence = wire.MaxTxInSequenceNum - 1 + } + + pkScript, err := hex.DecodeString(input.ScriptPubKey) + if err != nil { + return nil, fmt.Errorf("unable to decode pkScript: %v", err) + } + + // Unspent credits are currently expected to be either P2PKH or + // P2PK, P2PKH/P2SH nested in a revocation/stakechange/vote output. + scriptClass := txscript.GetScriptClass(0, pkScript) + + switch scriptClass { + case txscript.PubKeyHashTy: + inputSizes[idx] = RedeemP2PKHSigScriptSize + + case txscript.PubKeyTy: + inputSizes[idx] = RedeemP2PKSigScriptSize + + case txscript.StakeRevocationTy, txscript.StakeSubChangeTy, txscript.StakeGenTy: + scriptClass, err = txscript.GetStakeOutSubclass(pkScript) + if err != nil { + return nil, fmt.Errorf("failed to extract nested script "+ + "in stake output: %v", err) + } + + // For stake transactions we expect P2PKH and P2SH script class + // types only but ignore P2SH script type since it can pay + // to any script which the wallet may not recognize. + if scriptClass != txscript.PubKeyHashTy { + return nil, fmt.Errorf("unexpected nested script class "+ + " for input #%d sourced from txid %s", idx, input.TxID) + } + + inputSizes[idx] = RedeemP2PKHSigScriptSize + + default: + return nil, fmt.Errorf("unexpected script class (%v) for "+ + "input #%d sourced from txid %s", scriptClass, idx, input.TxID) + } + + tx.AddTxIn(txIn) + } + + outputSizes := make([]int, 0) + + // Add all transaction outputs with their associated pkScripts to the + // transaction after performing some validity checks. + for encodedAddr, amt := range amounts { + amount := dcrutil.Amount(amt) + + // Ensure the amount is in the valid range for monetary amounts. + if amount <= 0 || amount > dcrutil.MaxAmount { + return nil, fmt.Errorf("Invalid amount: 0 >= %v > %v", + amount, dcrutil.MaxAmount) + } + + addr, err := dcrutil.DecodeAddress(encodedAddr, params) + if err != nil { + return nil, fmt.Errorf("Could not decode address: %v", err) + } + + // Ensure the address is one of the supported types. + switch addr.(type) { + case *dcrutil.AddressPubKeyHash: + case *dcrutil.AddressScriptHash: + default: + return nil, fmt.Errorf("Invalid address type: %T", addr) + } + + var txOut *wire.TxOut + // If the pkScripts are not provided the transaction is simply paying + // amounts to the addresses provided. When provided the transaction is + // paying to swap contract scripts. + if pkScripts == nil { + // Create a new script which pays to the provided address. + pkScript, err := txscript.PayToAddrScript(addr) + if err != nil { + return nil, err + } + + outputSizes = append(outputSizes, P2PKHPkScriptSize) + txOut = wire.NewTxOut(amt, pkScript) + } else { + // Fetch the associated script for the provided address. + pkScript, ok := pkScripts[encodedAddr] + if !ok { + return nil, fmt.Errorf("no pkscript found for address %s", + encodedAddr) + } + + outputSizes = append(outputSizes, P2SHPkScriptSize) + txOut = wire.NewTxOut(amt, pkScript) + } + + tx.AddTxOut(txOut) + } + + tx.LockTime = lockTime + + totalIn := sumInputValues(tx.TxIn) + totalOut := sumOutputValues(tx.TxOut) + + changeScript, err := txscript.PayToAddrScript(changeAddr) + if err != nil { + return nil, err + } + + changeScriptSize := len(changeScript) + estSignedSize := EstimateSerializeSizeFromScriptSizes(inputSizes, outputSizes, changeScriptSize) + txFee := txrules.FeeForSerializeSize(relayFee, estSignedSize) + changeAmt := totalIn - totalOut - txFee + + if changeAmt != 0 && !txrules.IsDustAmount(changeAmt, changeScriptSize, relayFee) { + change := &wire.TxOut{ + Value: int64(changeAmt), + Version: wire.DefaultPkScriptVersion, + PkScript: changeScript, + } + l := len(tx.TxOut) + tx.TxOut = append(tx.TxOut[:l:l], change) + } + + return tx, nil +} + +// generateSwapPkScripts creates the associated pkScripts of the provided +// output addresses and amounts. +func generateSwapPkScripts(amounts map[string]int64, contracts []*Contract, params *chaincfg.Params) (map[string][]byte, error) { + if len(amounts) != len(contracts) { + return nil, fmt.Errorf("expected equal number of amounts "+ + "and contracts, got %v contracts, %v amounts", + len(amounts), len(contracts)) + } + + pkScripts := make(map[string][]byte, len(contracts)) + for encodedAddr := range amounts { + for _, contract := range contracts { + if encodedAddr == contract.RedeemAddr.String() { + contractP2SHPkScript, err := + generateContractP2SHPkScript(contract.ContractData, params) + if err != nil { + return nil, err + } + + pkScripts[encodedAddr] = contractP2SHPkScript + } + } + } + + if len(pkScripts) != len(amounts) { + return nil, fmt.Errorf("expected equal number of amounts "+ + "and contract scripts, got %v contract scripts, %v amounts", + len(pkScripts), len(amounts)) + } + + return pkScripts, nil +} diff --git a/client/wallet/dcr/go.mod b/client/wallet/dcr/go.mod new file mode 100644 index 0000000000..80f22f4d80 --- /dev/null +++ b/client/wallet/dcr/go.mod @@ -0,0 +1,20 @@ +module github.com/decred/dcrdex/client/wallet/dcr + +go 1.13 + +require ( + github.com/decred/dcrd/chaincfg v1.5.2 + github.com/decred/dcrd/chaincfg/chainhash v1.0.2 + github.com/decred/dcrd/chaincfg/v2 v2.3.0 + github.com/decred/dcrd/dcrjson/v3 v3.0.1 + github.com/decred/dcrd/dcrutil/v2 v2.0.1 + github.com/decred/dcrd/rpc/jsonrpc/types v1.0.1 + github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.0 + github.com/decred/dcrd/rpcclient/v5 v5.0.0 + github.com/decred/dcrd/txscript/v2 v2.1.0 + github.com/decred/dcrd/wire v1.3.0 + github.com/decred/dcrwallet/rpc/jsonrpc/types v1.3.0 + github.com/decred/dcrwallet/wallet/v3 v3.0.4 + github.com/decred/slog v1.0.0 + golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 +) diff --git a/client/wallet/dcr/go.sum b/client/wallet/dcr/go.sum new file mode 100644 index 0000000000..63dd4c9c17 --- /dev/null +++ b/client/wallet/dcr/go.sum @@ -0,0 +1,128 @@ +decred.org/cspp v0.2.0/go.mod h1:KVnB49sueBFCldRa/ivZCaWZbrPNEiXWwxHCf1jTYKI= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/blake256 v1.0.0/go.mod h1:xXNWCE1jsAP8DAjP+rKw2MbeqLczjI3TRx2VK+9OEYY= +github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4= +github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= +github.com/decred/base58 v1.0.0/go.mod h1:LLY1p5e3g91byL/UO1eiZaYd+uRoVRarybgcoymu9Ks= +github.com/decred/base58 v1.0.1 h1:w5qTcb0hYpKuIBYIn4Ckirkj1aOWrSq8onPQpb3eGg8= +github.com/decred/base58 v1.0.1/go.mod h1:H2ENcsJjye1G7CbRa67kV9OFaui0LGr56ntKKoY5g9c= +github.com/decred/dcrd/blockchain/stake/v2 v2.0.0/go.mod h1:jv/rKMcZ87lhvVkHot/tElxeAYEUJ3mnKPHJ7WPq86U= +github.com/decred/dcrd/blockchain/stake/v2 v2.0.2 h1:tRrJTywABGsUpf6qrTrtdIOKXyZflA51b0sqWf7p5gk= +github.com/decred/dcrd/blockchain/stake/v2 v2.0.2/go.mod h1:o2TT/l/YFdrt15waUdlZ3g90zfSwlA0WgQqHV9UGJF4= +github.com/decred/dcrd/blockchain/standalone v1.1.0 h1:yclvVGEY09Gf8A4GSAo+NCtL1dW2TYJ4OKp4+g0ICI0= +github.com/decred/dcrd/blockchain/standalone v1.1.0/go.mod h1:6K8ZgzlWM1Kz2TwXbrtiAvfvIwfAmlzrtpA7CVPCUPE= +github.com/decred/dcrd/blockchain/v2 v2.1.0/go.mod h1:DBmX26fUDTQocIozF44Ydo5+m+QzaC6aMYMBFFsCOJs= +github.com/decred/dcrd/chaincfg v1.5.2 h1:dd6l9rqcpxg2GF5neBmE2XxRc5Lqda45fWmN4XOJRW8= +github.com/decred/dcrd/chaincfg v1.5.2/go.mod h1:FukMzTjkwzjPU+hK7CqDMQe3NMbSZAYU5PAcsx1wlv0= +github.com/decred/dcrd/chaincfg/chainhash v1.0.1/go.mod h1:OVfvaOsNLS/A1y4Eod0Ip/Lf8qga7VXCQjUQLbkY0Go= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2 h1:rt5Vlq/jM3ZawwiacWjPa+smINyLRN07EO0cNBV6DGU= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= +github.com/decred/dcrd/chaincfg/v2 v2.0.2/go.mod h1:hpKvhLCDAD/xDZ3V1Pqpv9fIKVYYi11DyxETguazyvg= +github.com/decred/dcrd/chaincfg/v2 v2.1.0/go.mod h1:hpKvhLCDAD/xDZ3V1Pqpv9fIKVYYi11DyxETguazyvg= +github.com/decred/dcrd/chaincfg/v2 v2.3.0 h1:ItmU+7DeUtyiabrcW+16MJFgY/BBeeYaPfkBLrFLyjo= +github.com/decred/dcrd/chaincfg/v2 v2.3.0/go.mod h1:7qUJTvn+y/kswSRZ4sT2+EmvlDTDyy2InvNFtX/hxk0= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/crypto/ripemd160 v1.0.0 h1:MciTnR4NfBqDFRFjFkrn8WPLP4Vo7t6ww6ghfn6wcXQ= +github.com/decred/dcrd/crypto/ripemd160 v1.0.0/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg= +github.com/decred/dcrd/database/v2 v2.0.0/go.mod h1:Sj2lvTRB0mfSu9uD7ObfwCY/eJ954GFU/X+AndJIyfE= +github.com/decred/dcrd/database/v2 v2.0.1 h1:ghLzkKpVpwvjrdRv3njrEfkvygQpYQX66sGVs8ha+E8= +github.com/decred/dcrd/database/v2 v2.0.1/go.mod h1:ZOaWTv3IlNqCA+y7q3q5EozgmiDOmNwCSq3ntZn2CDo= +github.com/decred/dcrd/dcrec v1.0.0 h1:W+z6Es+Rai3MXYVoPAxYr5U1DGis0Co33scJ6uH2J6o= +github.com/decred/dcrd/dcrec v1.0.0/go.mod h1:HIaqbEJQ+PDzQcORxnqen5/V1FR3B4VpIfmePklt8Q8= +github.com/decred/dcrd/dcrec/edwards v1.0.0 h1:UDcPNzclKiJlWqV3x1Fl8xMCJrolo4PB4X9t8LwKDWU= +github.com/decred/dcrd/dcrec/edwards v1.0.0/go.mod h1:HblVh1OfMt7xSxUL1ufjToaEvpbjpWvvTAUx4yem8BI= +github.com/decred/dcrd/dcrec/edwards/v2 v2.0.0 h1:E5KszxGgpjpmW8vN811G6rBAZg0/S/DftdGqN4FW5x4= +github.com/decred/dcrd/dcrec/edwards/v2 v2.0.0/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc= +github.com/decred/dcrd/dcrec/secp256k1 v1.0.1/go.mod h1:lhu4eZFSfTJWUnR3CFRcpD+Vta0KUAqnhTsTksHXgy0= +github.com/decred/dcrd/dcrec/secp256k1 v1.0.2 h1:awk7sYJ4pGWmtkiGHFfctztJjHMKGLV8jctGQhAbKe0= +github.com/decred/dcrd/dcrec/secp256k1 v1.0.2/go.mod h1:CHTUIVfmDDd0KFVFpNX1pFVCBUegxW387nN0IGwNKR0= +github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0 h1:3GIJYXQDAKpLEFriGFN8SbSffak10UXHGdIcFaMPykY= +github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0/go.mod h1:3s92l0paYkZoIHuj4X93Teg/HB7eGM9x/zokGw+u4mY= +github.com/decred/dcrd/dcrjson/v3 v3.0.1 h1:b9cpplNJG+nutE2jS8K/BtSGIJihEQHhFjFAsvJF/iI= +github.com/decred/dcrd/dcrjson/v3 v3.0.1/go.mod h1:fnTHev/ABGp8IxFudDhjGi9ghLiXRff1qZz/wvq12Mg= +github.com/decred/dcrd/dcrutil/v2 v2.0.0/go.mod h1:gUshVAXpd51DlcEhr51QfWL2HJGkMDM1U8chY+9VvQg= +github.com/decred/dcrd/dcrutil/v2 v2.0.1 h1:aL+c7o7Q66HV1gIif+XkNYo9DeorN3l01Vns8mh0mqs= +github.com/decred/dcrd/dcrutil/v2 v2.0.1/go.mod h1:JdEgF6eh0TTohPeiqDxqDSikTSvAczq0J7tFMyyeD+k= +github.com/decred/dcrd/gcs v1.1.0 h1:djuYzaFUzUTJR+6ulMSRZOQ+P9rxtIyuxQeViAEfB8s= +github.com/decred/dcrd/gcs v1.1.0/go.mod h1:yBjhj217Vw5lw3aKnCdHip7fYb9zwMos8bCy5s79M9w= +github.com/decred/dcrd/gcs/v2 v2.0.0 h1:nCc3q9iIwIpF0khTSiC7xYgojKoKnPrqrgVjboOBXDE= +github.com/decred/dcrd/gcs/v2 v2.0.0/go.mod h1:3XjKcrtvB+r2ezhIsyNCLk6dRnXRJVyYmsd1P3SkU3o= +github.com/decred/dcrd/hdkeychain/v2 v2.1.0 h1:NVNIz36HPukOnaysBDsLO+2kWqijLM4tvLUsLLyLfME= +github.com/decred/dcrd/hdkeychain/v2 v2.1.0/go.mod h1:DR+lD4uV8G0i3c9qnUJwjiGaaEWK+nSrbWCz1BRHBL8= +github.com/decred/dcrd/rpc/jsonrpc/types v1.0.1 h1:sWsGtWzdmrna6aysDCHwjANTJh+Lxt2xp6S10ahP79Y= +github.com/decred/dcrd/rpc/jsonrpc/types v1.0.1/go.mod h1:dJUp9PoyFYklzmlImpVkVLOr6j4zKuUv66YgemP2sd8= +github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.0 h1:uyvwjO+90KHxZIIztobB9cG+qVSHhCT+aGSiZF1vGAg= +github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.0/go.mod h1:c5S+PtQWNIA2aUakgrLhrlopkMadcOv51dWhCEdo49c= +github.com/decred/dcrd/rpcclient/v5 v5.0.0 h1:dQAPuZU9D+/CP8DcyVjtNxLjT4Ew+L6QhYd/MWhSFvw= +github.com/decred/dcrd/rpcclient/v5 v5.0.0/go.mod h1:lg7e2kpulSpynHkS2JXJ+trQ4PWHaHLQcp/Q0eSIvBc= +github.com/decred/dcrd/txscript/v2 v2.0.0/go.mod h1:WStcyYYJa+PHJB4XjrLDRzV96/Z4thtsu8mZoVrU6C0= +github.com/decred/dcrd/txscript/v2 v2.1.0 h1:IKIpNm0lPmNQoaZ2zxZm1qMwfmLb/XXeahxXlfc+MrA= +github.com/decred/dcrd/txscript/v2 v2.1.0/go.mod h1:XaJAVrZU4NWRx4UEzTiDAs86op1m8GRJLz24SDBKOi0= +github.com/decred/dcrd/wire v1.2.0/go.mod h1:/JKOsLInOJu6InN+/zH5AyCq3YDIOW/EqcffvU8fJHM= +github.com/decred/dcrd/wire v1.3.0 h1:X76I2/a8esUmxXmFpJpAvXEi014IA4twgwcOBeIS8lE= +github.com/decred/dcrd/wire v1.3.0/go.mod h1:fnKGlUY2IBuqnpxx5dYRU5Oiq392OBqAuVjRVSkIoXM= +github.com/decred/dcrwallet/deployments/v2 v2.0.0/go.mod h1:fY1HV1vIeeY5bHjrMknUhB/ZOVIfthBiUlSgRqFFKrg= +github.com/decred/dcrwallet/errors/v2 v2.0.0 h1:b3QHoQNjKkrcO0GSpueeHvFKp5eqtRv9aw649MDyejA= +github.com/decred/dcrwallet/errors/v2 v2.0.0/go.mod h1:2HYvtRuCE9XqDNCWhKmBuzLG364xUgcUIsJu02r0F5Q= +github.com/decred/dcrwallet/rpc/client/dcrd v1.0.0/go.mod h1:qrJri+p+cn+obQ8nkW5hTtagPcOnCqKPGBq1t02gBc0= +github.com/decred/dcrwallet/rpc/jsonrpc/types v1.3.0 h1:yCxtFqK7X6GvZWQzHXjCwoGCy9YVe3tGEwxCjW5rYQk= +github.com/decred/dcrwallet/rpc/jsonrpc/types v1.3.0/go.mod h1:Xvekb43GtfMiRbyIY4ZJ9Uhd9HRIAcnp46f3q2eIExU= +github.com/decred/dcrwallet/validate v1.1.1/go.mod h1:T++tlVcCOh2oSrEq4r5CKCvmftaQdq9uZwO7jSNYZaw= +github.com/decred/dcrwallet/wallet/v3 v3.0.4 h1:5wyV5bySU+Tg2p6URb3Fu5X8OS9VRIF+AzUyZKGwBpQ= +github.com/decred/dcrwallet/wallet/v3 v3.0.4/go.mod h1:SJ+++gtMdcUeqMv6iIO3gVGlGJfM+4iY2QSaAakhbUw= +github.com/decred/go-socks v1.1.0 h1:dnENcc0KIqQo3HSXdgboXAHgqsCIutkqq6ntQjYtm2U= +github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0= +github.com/decred/slog v1.0.0 h1:Dl+W8O6/JH6n2xIFN2p3DNjCmjYwvrXsjlSJTQQ4MhE= +github.com/decred/slog v1.0.0/go.mod h1:zR98rEZHSnbZ4WHZtO0iqmSZjDLKhkXfrPTZQKtAonQ= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/bitset v1.0.0/go.mod h1:ZOYB5Uvkla7wIEY4FEssPVi3IQXa02arznRaYaAEPe4= +github.com/jrick/wsrpc/v2 v2.0.0/go.mod h1:naH/fojac6vQWYgAA0e7b9TX/bShsWoVL7CwrdvFmUk= +github.com/jrick/wsrpc/v2 v2.2.0/go.mod h1:naH/fojac6vQWYgAA0e7b9TX/bShsWoVL7CwrdvFmUk= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/client/wallet/dcr/log.go b/client/wallet/dcr/log.go new file mode 100644 index 0000000000..8ae5e9d0b4 --- /dev/null +++ b/client/wallet/dcr/log.go @@ -0,0 +1,24 @@ +// 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 dcr + +import ( + "github.com/decred/slog" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log = slog.Disabled + +// DisableLog disables all library log output. Logging output is disabled +// by default until UseLogger is called. +func DisableLog() { + log = slog.Disabled +} + +// UseLogger uses a specified Logger to output package logging info. +func UseLogger(logger slog.Logger) { + log = logger +} diff --git a/client/wallet/dcr/sizeest.go b/client/wallet/dcr/sizeest.go new file mode 100644 index 0000000000..7a019ade51 --- /dev/null +++ b/client/wallet/dcr/sizeest.go @@ -0,0 +1,263 @@ +// Copyright (c) 2016 The btcsuite developers +// Copyright (c) 2016-2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package dcr + +import ( + "github.com/decred/dcrd/txscript/v2" + "github.com/decred/dcrd/wire" +) + +// Worst case script and input/output size estimates. +const ( + // RedeemP2PKSigScriptSize is the worst case (largest) serialize size + // of a transaction input script that redeems a compressed P2PK output. + // It is calculated as: + // + // - OP_DATA_73 + // - 72 bytes DER signature + 1 byte sighash + RedeemP2PKSigScriptSize = 1 + 73 + + // RedeemP2PKHSigScriptSize is the worst case (largest) serialize size + // of a transaction input script that redeems a compressed P2PKH output. + // It is calculated as: + // + // - OP_DATA_73 + // - 72 bytes DER signature + 1 byte sighash + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + RedeemP2PKHSigScriptSize = 1 + 73 + 1 + 33 + + // RedeemP2SHSigScriptSize is the worst case (largest) serialize size + // of a transaction input script that redeems a P2SH output. + // It is calculated as: + // + // - OP_DATA_73 + // - 73-byte signature + // - OP_DATA_35 + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_CHECKSIG + RedeemP2SHSigScriptSize = 1 + 73 + 1 + 1 + 33 + 1 + + // RedeemP2PKHInputSize is the worst case (largest) serialize size of a + // transaction input redeeming a compressed P2PKH output. It is + // calculated as: + // + // - 32 bytes previous tx + // - 4 bytes output index + // - 1 byte tree + // - 8 bytes amount + // - 4 bytes block height + // - 4 bytes block index + // - 1 byte compact int encoding value 107 + // - 107 bytes signature script + // - 4 bytes sequence + RedeemP2PKHInputSize = 32 + 4 + 1 + 8 + 4 + 4 + 1 + RedeemP2PKHSigScriptSize + 4 + + // P2PKHPkScriptSize is the size of a transaction output script that + // pays to a compressed pubkey hash. It is calculated as: + // + // - OP_DUP + // - OP_HASH160 + // - OP_DATA_20 + // - 20 bytes pubkey hash + // - OP_EQUALVERIFY + // - OP_CHECKSIG + P2PKHPkScriptSize = 1 + 1 + 1 + 20 + 1 + 1 + + // P2SHPkScriptSize is the size of a transaction output script that + // pays to a script hash. It is calculated as: + // + // - OP_HASH160 + // - OP_DATA_20 + // - 20 bytes script hash + // - OP_EQUAL + P2SHPkScriptSize = 1 + 1 + 20 + 1 + + // TicketCommitmentScriptSize is the size of a ticket purchase commitment + // script. It is calculated as: + // + // - OP_RETURN + // - OP_DATA_30 + // - 20 bytes P2SH/P2PKH + // - 8 byte amount + // - 2 byte fee range limits + TicketCommitmentScriptSize = 1 + 1 + 20 + 8 + 2 + + // P2PKHOutputSize is the serialize size of a transaction output with a + // P2PKH output script. It is calculated as: + // + // - 8 bytes output value + // - 2 bytes version + // - 1 byte compact int encoding value 25 + // - 25 bytes P2PKH output script + P2PKHOutputSize = 8 + 2 + 1 + 25 + + // redeemAtomicSwapSigScriptSize is the worst case (largest) serialize size + // of a transaction input script to redeem the atomic swap contract. This + // does not include final push for the contract itself. + // + // - OP_DATA_73 + // - 72 bytes DER signature + 1 byte sighash + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_DATA_32 + // - 32 bytes secret + // - OP_TRUE + redeemAtomicSwapSigScriptSize = 1 + 73 + 1 + 33 + 1 + 32 + 1 + + // refundAtomicSwapSigScriptSize is the worst case (largest) serialize size + // of a transaction input script that refunds a P2SH atomic swap output. + // This does not include final push for the contract itself. + // + // - OP_DATA_73 + // - 72 bytes DER signature + 1 byte sighash + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_FALSE + refundAtomicSwapSigScriptSize = 1 + 73 + 1 + 33 + 1 +) + +// EstimateSerializeSize returns a worst case serialize size estimate for a +// signed transaction that spends a number of outputs and contains each +// transaction output from txOuts. The estimated size is incremented for an +// additional change output if changeScriptSize is greater than 0. Passing 0 +// does not add a change output. +func EstimateSerializeSize(scriptSizes []int, txOuts []*wire.TxOut, changeScriptSize int) int { + // Generate and sum up the estimated sizes of the inputs. + txInsSize := 0 + for _, size := range scriptSizes { + txInsSize += EstimateInputSize(size) + } + + inputCount := len(scriptSizes) + outputCount := len(txOuts) + changeSize := 0 + if changeScriptSize > 0 { + changeSize = EstimateOutputSize(changeScriptSize) + outputCount++ + } + + // 12 additional bytes are for version, locktime and expiry. + return 12 + (2 * wire.VarIntSerializeSize(uint64(inputCount))) + + wire.VarIntSerializeSize(uint64(outputCount)) + + txInsSize + + sumOutputSerializeSizes(txOuts) + + changeSize +} + +// EstimateSerializeSizeFromScriptSizes returns a worst case serialize size +// estimate for a signed transaction that spends len(inputSizes) previous +// outputs and pays to len(outputSizes) outputs with scripts of the provided +// worst-case sizes. The estimated size is incremented for an additional +// change output if changeScriptSize is greater than 0. Passing 0 does not +// add a change output. +func EstimateSerializeSizeFromScriptSizes(inputSizes []int, outputSizes []int, changeScriptSize int) int { + // Generate and sum up the estimated sizes of the inputs. + txInsSize := 0 + for _, inputSize := range inputSizes { + txInsSize += EstimateInputSize(inputSize) + } + + // Generate and sum up the estimated sizes of the outputs. + txOutsSize := 0 + for _, outputSize := range outputSizes { + txOutsSize += EstimateOutputSize(outputSize) + } + + inputCount := len(inputSizes) + outputCount := len(outputSizes) + changeSize := 0 + if changeScriptSize > 0 { + changeSize = EstimateOutputSize(changeScriptSize) + outputCount++ + } + + // 12 additional bytes are for version, locktime and expiry. + return 12 + (2 * wire.VarIntSerializeSize(uint64(inputCount))) + + wire.VarIntSerializeSize(uint64(outputCount)) + + txInsSize + txOutsSize + changeSize +} + +// EstimateInputSize returns the worst case serialize size estimate for a tx input +// - 32 bytes previous tx +// - 4 bytes output index +// - 1 byte tree +// - 8 bytes amount +// - 4 bytes block height +// - 4 bytes block index +// - the compact int representation of the script size +// - the supplied script size +// - 4 bytes sequence +func EstimateInputSize(scriptSize int) int { + return 32 + 4 + 1 + 8 + 4 + 4 + wire.VarIntSerializeSize(uint64(scriptSize)) + scriptSize + 4 +} + +// EstimateOutputSize returns the worst case serialize size estimate for a tx output +// - 8 bytes amount +// - 2 bytes version +// - the compact int representation of the script size +// - the supplied script size +func EstimateOutputSize(scriptSize int) int { + return 8 + 2 + wire.VarIntSerializeSize(uint64(scriptSize)) + scriptSize +} + +func sumOutputSerializeSizes(outputs []*wire.TxOut) (serializeSize int) { + for _, txOut := range outputs { + serializeSize += txOut.SerializeSize() + } + return serializeSize +} + +// inputSize returns the size of the transaction input needed to include a +// signature script with size sigScriptSize. It is calculated as: +// +// - 32 bytes previous tx +// - 4 bytes output index +// - 1 byte tree +// - 8 bytes amount +// - 4 bytes block height +// - 4 bytes block index +// - Compact int encoding sigScriptSize +// - sigScriptSize bytes signature script +// - 4 bytes sequence +func inputSize(sigScriptSize int) int { + return 32 + 4 + 1 + 8 + 4 + 4 + wire.VarIntSerializeSize(uint64(sigScriptSize)) + sigScriptSize + 4 +} + +// estimateRedeemSerializeSize returns a worst case serialize size estimates for +// a transaction that redeems an atomic swap P2SH output. +func estimateRedeemSerializeSize(contract []byte, txOuts []*wire.TxOut) int { + contractPush, err := txscript.NewScriptBuilder().AddData(contract).Script() + if err != nil { + // Should never be hit since this script does exceed the limits. + panic(err) + } + contractPushSize := len(contractPush) + + // 12 additional bytes are for version, locktime and expiry. + return 12 + (2 * wire.VarIntSerializeSize(1)) + + wire.VarIntSerializeSize(1) + + inputSize(redeemAtomicSwapSigScriptSize+contractPushSize) + + sumOutputSerializeSizes(txOuts) +} + +// estimateRefundSerializeSize returns a worst case serialize size estimates for +// a transaction that refunds an atomic swap P2SH output. +func estimateRefundSerializeSize(contract []byte, txOuts []*wire.TxOut) int { + contractPush, err := txscript.NewScriptBuilder().AddData(contract).Script() + if err != nil { + // Should never be hit since this script does exceed the limits. + panic(err) + } + contractPushSize := len(contractPush) + + // 12 additional bytes are for version, locktime and expiry. + return 12 + (2 * wire.VarIntSerializeSize(1)) + + wire.VarIntSerializeSize(1) + + inputSize(refundAtomicSwapSigScriptSize+contractPushSize) + + sumOutputSerializeSizes(txOuts) +}