diff --git a/votingpool/README.md b/votingpool/README.md deleted file mode 100644 index 75a51af..0000000 --- a/votingpool/README.md +++ /dev/null @@ -1,40 +0,0 @@ -votingpool -======== - -[![Build Status](https://travis-ci.org/btcsuite/btcwallet.png?branch=master)] -(https://travis-ci.org/btcsuite/btcwallet) - -Package votingpool provides voting pool functionality for btcwallet as -described here: -[Voting Pools](http://opentransactions.org/wiki/index.php?title=Category:Voting_Pools). - -A suite of tests is provided to ensure proper functionality. See -`test_coverage.txt` for the gocov coverage report. Alternatively, if you are -running a POSIX OS, you can run the `cov_report.sh` script for a real-time -report. Package votingpool is licensed under the liberal ISC license. - -Note that this is still a work in progress. - -## Feature Overview - -- Create/Load pools -- Create series -- Replace series -- Create deposit addresses -- Comprehensive test coverage - -## Documentation - -[![GoDoc](https://godoc.org/github.com/btcsuite/btcwallet/votingpool?status.png)] -(http://godoc.org/github.com/btcsuite/btcwallet/votingpool) - -Full `go doc` style documentation for the project can be viewed online without -installing this package by using the GoDoc site here: -http://godoc.org/github.com/btcsuite/btcwallet/votingpool - -You can also view the documentation locally once the package is installed with -the `godoc` tool by running `godoc -http=":6060"` and pointing your browser to -http://localhost:6060/pkg/github.com/btcsuite/btcwallet/votingpool - -Package votingpool is licensed under the [copyfree](http://copyfree.org) ISC -License. diff --git a/votingpool/common_test.go b/votingpool/common_test.go deleted file mode 100644 index b4059a9..0000000 --- a/votingpool/common_test.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2014 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package votingpool - -import ( - "os" - "reflect" - "runtime" - "testing" - - "github.com/btcsuite/btclog" - "github.com/btcsuite/btcwallet/waddrmgr" - "github.com/btcsuite/btcwallet/walletdb" -) - -func init() { - runtime.GOMAXPROCS(runtime.NumCPU()) - - // Enable logging (Debug level) to aid debugging failing tests. - logger := btclog.NewBackend(os.Stdout).Logger("TEST") - logger.SetLevel(btclog.LevelDebug) - UseLogger(logger) -} - -// TstCheckError ensures the passed error is a votingpool.Error with an error -// code that matches the passed error code. -func TstCheckError(t *testing.T, testName string, gotErr error, wantErrCode ErrorCode) { - vpErr, ok := gotErr.(Error) - if !ok { - t.Errorf("%s: unexpected error type - got %T (%s), want %T", - testName, gotErr, gotErr, Error{}) - } - if vpErr.ErrorCode != wantErrCode { - t.Errorf("%s: unexpected error code - got %s (%s), want %s", - testName, vpErr.ErrorCode, vpErr, wantErrCode) - } -} - -// TstRunWithManagerUnlocked calls the given callback with the manager unlocked, -// and locks it again before returning. -func TstRunWithManagerUnlocked(t *testing.T, mgr *waddrmgr.Manager, addrmgrNs walletdb.ReadBucket, callback func()) { - if err := mgr.Unlock(addrmgrNs, privPassphrase); err != nil { - t.Fatal(err) - } - defer mgr.Lock() - callback() -} - -// TstCheckWithdrawalStatusMatches compares s1 and s2 using reflect.DeepEqual -// and calls t.Fatal() if they're not identical. -func TstCheckWithdrawalStatusMatches(t *testing.T, s1, s2 WithdrawalStatus) { - if s1.Fees() != s2.Fees() { - t.Fatalf("Wrong amount of network fees; want %d, got %d", s1.Fees(), s2.Fees()) - } - - if !reflect.DeepEqual(s1.Sigs(), s2.Sigs()) { - t.Fatalf("Wrong tx signatures; got %x, want %x", s1.Sigs(), s2.Sigs()) - } - - if !reflect.DeepEqual(s1.NextInputAddr(), s2.NextInputAddr()) { - t.Fatalf("Wrong NextInputAddr; got %v, want %v", s1.NextInputAddr(), s2.NextInputAddr()) - } - - if !reflect.DeepEqual(s1.NextChangeAddr(), s2.NextChangeAddr()) { - t.Fatalf("Wrong NextChangeAddr; got %v, want %v", s1.NextChangeAddr(), s2.NextChangeAddr()) - } - - if !reflect.DeepEqual(s1.Outputs(), s2.Outputs()) { - t.Fatalf("Wrong WithdrawalOutputs; got %v, want %v", s1.Outputs(), s2.Outputs()) - } - - if !reflect.DeepEqual(s1.transactions, s2.transactions) { - t.Fatalf("Wrong transactions; got %v, want %v", s1.transactions, s2.transactions) - } - - // The above checks could be replaced by this one, but when they fail the - // failure msg wouldn't give us much clue as to what is not equal, so we do - // the individual checks above and use this one as a catch-all check in case - // we forget to check any of the individual fields. - if !reflect.DeepEqual(s1, s2) { - t.Fatalf("Wrong WithdrawalStatus; got %v, want %v", s1, s2) - } -} diff --git a/votingpool/cov_report.sh b/votingpool/cov_report.sh deleted file mode 100644 index 307f05b..0000000 --- a/votingpool/cov_report.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -# This script uses gocov to generate a test coverage report. -# The gocov tool my be obtained with the following command: -# go get github.com/axw/gocov/gocov -# -# It will be installed to $GOPATH/bin, so ensure that location is in your $PATH. - -# Check for gocov. -type gocov >/dev/null 2>&1 -if [ $? -ne 0 ]; then - echo >&2 "This script requires the gocov tool." - echo >&2 "You may obtain it with the following command:" - echo >&2 "go get github.com/axw/gocov/gocov" - exit 1 -fi -gocov test | gocov report diff --git a/votingpool/db.go b/votingpool/db.go deleted file mode 100644 index 311a5f2..0000000 --- a/votingpool/db.go +++ /dev/null @@ -1,592 +0,0 @@ -// Copyright (c) 2014 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package votingpool - -import ( - "bytes" - "encoding/binary" - "encoding/gob" - "fmt" - - "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/snacl" - "github.com/btcsuite/btcwallet/walletdb" -) - -// These constants define the serialized length for a given encrypted extended -// public or private key. -const ( - // We can calculate the encrypted extended key length this way: - // snacl.Overhead == overhead for encrypting (16) - // actual base58 extended key length = (111) - // snacl.NonceSize == nonce size used for encryption (24) - seriesKeyLength = snacl.Overhead + 111 + snacl.NonceSize - // 4 bytes version + 1 byte active + 4 bytes nKeys + 4 bytes reqSigs - seriesMinSerial = 4 + 1 + 4 + 4 - // 15 is the max number of keys in a voting pool, 1 each for - // pubkey and privkey - seriesMaxSerial = seriesMinSerial + 15*seriesKeyLength*2 - // version of serialized Series that we support - seriesMaxVersion = 1 -) - -var ( - usedAddrsBucketName = []byte("usedaddrs") - seriesBucketName = []byte("series") - withdrawalsBucketName = []byte("withdrawals") - // string representing a non-existent private key - seriesNullPrivKey = [seriesKeyLength]byte{} -) - -type dbSeriesRow struct { - version uint32 - active bool - reqSigs uint32 - pubKeysEncrypted [][]byte - privKeysEncrypted [][]byte -} - -type dbWithdrawalRow struct { - Requests []dbOutputRequest - StartAddress dbWithdrawalAddress - ChangeStart dbChangeAddress - LastSeriesID uint32 - DustThreshold btcutil.Amount - Status dbWithdrawalStatus -} - -type dbWithdrawalAddress struct { - SeriesID uint32 - Branch Branch - Index Index -} - -type dbChangeAddress struct { - SeriesID uint32 - Index Index -} - -type dbOutputRequest struct { - Addr string - Amount btcutil.Amount - Server string - Transaction uint32 -} - -type dbWithdrawalOutput struct { - // We store the OutBailmentID here as we need a way to look up the - // corresponding dbOutputRequest in dbWithdrawalRow when deserializing. - OutBailmentID OutBailmentID - Status outputStatus - Outpoints []dbOutBailmentOutpoint -} - -type dbOutBailmentOutpoint struct { - Ntxid Ntxid - Index uint32 - Amount btcutil.Amount -} - -type dbChangeAwareTx struct { - SerializedMsgTx []byte - ChangeIdx int32 -} - -type dbWithdrawalStatus struct { - NextInputAddr dbWithdrawalAddress - NextChangeAddr dbChangeAddress - Fees btcutil.Amount - Outputs map[OutBailmentID]dbWithdrawalOutput - Sigs map[Ntxid]TxSigs - Transactions map[Ntxid]dbChangeAwareTx -} - -// getUsedAddrBucketID returns the used addresses bucket ID for the given series -// and branch. It has the form seriesID:branch. -func getUsedAddrBucketID(seriesID uint32, branch Branch) []byte { - var bucketID [9]byte - binary.LittleEndian.PutUint32(bucketID[0:4], seriesID) - bucketID[4] = ':' - binary.LittleEndian.PutUint32(bucketID[5:9], uint32(branch)) - return bucketID[:] -} - -// putUsedAddrHash adds an entry (key==index, value==encryptedHash) to the used -// addresses bucket of the given pool, series and branch. -func putUsedAddrHash(ns walletdb.ReadWriteBucket, poolID []byte, seriesID uint32, branch Branch, - index Index, encryptedHash []byte) error { - - usedAddrs := ns.NestedReadWriteBucket(poolID).NestedReadWriteBucket(usedAddrsBucketName) - bucket, err := usedAddrs.CreateBucketIfNotExists(getUsedAddrBucketID(seriesID, branch)) - if err != nil { - return newError(ErrDatabase, "failed to store used address hash", err) - } - return bucket.Put(uint32ToBytes(uint32(index)), encryptedHash) -} - -// getUsedAddrHash returns the addr hash with the given index from the used -// addresses bucket of the given pool, series and branch. -func getUsedAddrHash(ns walletdb.ReadBucket, poolID []byte, seriesID uint32, branch Branch, - index Index) []byte { - - usedAddrs := ns.NestedReadBucket(poolID).NestedReadBucket(usedAddrsBucketName) - bucket := usedAddrs.NestedReadBucket(getUsedAddrBucketID(seriesID, branch)) - if bucket == nil { - return nil - } - return bucket.Get(uint32ToBytes(uint32(index))) -} - -// getMaxUsedIdx returns the highest used index from the used addresses bucket -// of the given pool, series and branch. -func getMaxUsedIdx(ns walletdb.ReadBucket, poolID []byte, seriesID uint32, branch Branch) (Index, error) { - maxIdx := Index(0) - usedAddrs := ns.NestedReadBucket(poolID).NestedReadBucket(usedAddrsBucketName) - bucket := usedAddrs.NestedReadBucket(getUsedAddrBucketID(seriesID, branch)) - if bucket == nil { - return maxIdx, nil - } - // FIXME: This is far from optimal and should be optimized either by storing - // a separate key in the DB with the highest used idx for every - // series/branch or perhaps by doing a large gap linear forward search + - // binary backwards search (e.g. check for 1000000, 2000000, .... until it - // doesn't exist, and then use a binary search to find the max using the - // discovered bounds). - err := bucket.ForEach( - func(k, v []byte) error { - idx := Index(bytesToUint32(k)) - if idx > maxIdx { - maxIdx = idx - } - return nil - }) - if err != nil { - return Index(0), newError(ErrDatabase, "failed to get highest idx of used addresses", err) - } - return maxIdx, nil -} - -// putPool stores a voting pool in the database, creating a bucket named -// after the voting pool id and two other buckets inside it to store series and -// used addresses for that pool. -func putPool(ns walletdb.ReadWriteBucket, poolID []byte) error { - poolBucket, err := ns.CreateBucket(poolID) - if err != nil { - return newError(ErrDatabase, fmt.Sprintf("cannot create pool %v", poolID), err) - } - _, err = poolBucket.CreateBucket(seriesBucketName) - if err != nil { - return newError(ErrDatabase, fmt.Sprintf("cannot create series bucket for pool %v", - poolID), err) - } - _, err = poolBucket.CreateBucket(usedAddrsBucketName) - if err != nil { - return newError(ErrDatabase, fmt.Sprintf("cannot create used addrs bucket for pool %v", - poolID), err) - } - _, err = poolBucket.CreateBucket(withdrawalsBucketName) - if err != nil { - return newError( - ErrDatabase, fmt.Sprintf("cannot create withdrawals bucket for pool %v", poolID), err) - } - return nil -} - -// loadAllSeries returns a map of all the series stored inside a voting pool -// bucket, keyed by id. -func loadAllSeries(ns walletdb.ReadBucket, poolID []byte) (map[uint32]*dbSeriesRow, error) { - bucket := ns.NestedReadBucket(poolID).NestedReadBucket(seriesBucketName) - allSeries := make(map[uint32]*dbSeriesRow) - err := bucket.ForEach( - func(k, v []byte) error { - seriesID := bytesToUint32(k) - series, err := deserializeSeriesRow(v) - if err != nil { - return err - } - allSeries[seriesID] = series - return nil - }) - if err != nil { - return nil, err - } - return allSeries, nil -} - -// existsPool checks the existence of a bucket named after the given -// voting pool id. -func existsPool(ns walletdb.ReadBucket, poolID []byte) bool { - bucket := ns.NestedReadBucket(poolID) - return bucket != nil -} - -// putSeries stores the given series inside a voting pool bucket named after -// poolID. The voting pool bucket does not need to be created beforehand. -func putSeries(ns walletdb.ReadWriteBucket, poolID []byte, version, ID uint32, active bool, reqSigs uint32, pubKeysEncrypted, privKeysEncrypted [][]byte) error { - row := &dbSeriesRow{ - version: version, - active: active, - reqSigs: reqSigs, - pubKeysEncrypted: pubKeysEncrypted, - privKeysEncrypted: privKeysEncrypted, - } - return putSeriesRow(ns, poolID, ID, row) -} - -// putSeriesRow stores the given series row inside a voting pool bucket named -// after poolID. The voting pool bucket does not need to be created -// beforehand. -func putSeriesRow(ns walletdb.ReadWriteBucket, poolID []byte, ID uint32, row *dbSeriesRow) error { - bucket, err := ns.CreateBucketIfNotExists(poolID) - if err != nil { - str := fmt.Sprintf("cannot create bucket %v", poolID) - return newError(ErrDatabase, str, err) - } - bucket, err = bucket.CreateBucketIfNotExists(seriesBucketName) - if err != nil { - return err - } - serialized, err := serializeSeriesRow(row) - if err != nil { - return err - } - err = bucket.Put(uint32ToBytes(ID), serialized) - if err != nil { - str := fmt.Sprintf("cannot put series %v into bucket %v", serialized, poolID) - return newError(ErrDatabase, str, err) - } - return nil -} - -// deserializeSeriesRow deserializes a series storage into a dbSeriesRow struct. -func deserializeSeriesRow(serializedSeries []byte) (*dbSeriesRow, error) { - // The serialized series format is: - // ... - // - // 4 bytes version + 1 byte active + 4 bytes reqSigs + 4 bytes nKeys - // + seriesKeyLength * 2 * nKeys (1 for priv, 1 for pub) - - // Given the above, the length of the serialized series should be - // at minimum the length of the constants. - if len(serializedSeries) < seriesMinSerial { - str := fmt.Sprintf("serialized series is too short: %v", serializedSeries) - return nil, newError(ErrSeriesSerialization, str, nil) - } - - // Maximum number of public keys is 15 and the same for public keys - // this gives us an upper bound. - if len(serializedSeries) > seriesMaxSerial { - str := fmt.Sprintf("serialized series is too long: %v", serializedSeries) - return nil, newError(ErrSeriesSerialization, str, nil) - } - - // Keeps track of the position of the next set of bytes to deserialize. - current := 0 - row := dbSeriesRow{} - - row.version = bytesToUint32(serializedSeries[current : current+4]) - if row.version > seriesMaxVersion { - str := fmt.Sprintf("deserialization supports up to version %v not %v", - seriesMaxVersion, row.version) - return nil, newError(ErrSeriesVersion, str, nil) - } - current += 4 - - row.active = serializedSeries[current] == 0x01 - current++ - - row.reqSigs = bytesToUint32(serializedSeries[current : current+4]) - current += 4 - - nKeys := bytesToUint32(serializedSeries[current : current+4]) - current += 4 - - // Check to see if we have the right number of bytes to consume. - if len(serializedSeries) < current+int(nKeys)*seriesKeyLength*2 { - str := fmt.Sprintf("serialized series has not enough data: %v", serializedSeries) - return nil, newError(ErrSeriesSerialization, str, nil) - } else if len(serializedSeries) > current+int(nKeys)*seriesKeyLength*2 { - str := fmt.Sprintf("serialized series has too much data: %v", serializedSeries) - return nil, newError(ErrSeriesSerialization, str, nil) - } - - // Deserialize the pubkey/privkey pairs. - row.pubKeysEncrypted = make([][]byte, nKeys) - row.privKeysEncrypted = make([][]byte, nKeys) - for i := 0; i < int(nKeys); i++ { - pubKeyStart := current + seriesKeyLength*i*2 - pubKeyEnd := current + seriesKeyLength*i*2 + seriesKeyLength - privKeyEnd := current + seriesKeyLength*(i+1)*2 - row.pubKeysEncrypted[i] = serializedSeries[pubKeyStart:pubKeyEnd] - privKeyEncrypted := serializedSeries[pubKeyEnd:privKeyEnd] - if bytes.Equal(privKeyEncrypted, seriesNullPrivKey[:]) { - row.privKeysEncrypted[i] = nil - } else { - row.privKeysEncrypted[i] = privKeyEncrypted - } - } - - return &row, nil -} - -// serializeSeriesRow serializes a dbSeriesRow struct into storage format. -func serializeSeriesRow(row *dbSeriesRow) ([]byte, error) { - // The serialized series format is: - // ... - // - // 4 bytes version + 1 byte active + 4 bytes reqSigs + 4 bytes nKeys - // + seriesKeyLength * 2 * nKeys (1 for priv, 1 for pub) - serializedLen := 4 + 1 + 4 + 4 + (seriesKeyLength * 2 * len(row.pubKeysEncrypted)) - - if len(row.privKeysEncrypted) != 0 && - len(row.pubKeysEncrypted) != len(row.privKeysEncrypted) { - str := fmt.Sprintf("different # of pub (%v) and priv (%v) keys", - len(row.pubKeysEncrypted), len(row.privKeysEncrypted)) - return nil, newError(ErrSeriesSerialization, str, nil) - } - - if row.version > seriesMaxVersion { - str := fmt.Sprintf("serialization supports up to version %v, not %v", - seriesMaxVersion, row.version) - return nil, newError(ErrSeriesVersion, str, nil) - } - - serialized := make([]byte, 0, serializedLen) - serialized = append(serialized, uint32ToBytes(row.version)...) - if row.active { - serialized = append(serialized, 0x01) - } else { - serialized = append(serialized, 0x00) - } - serialized = append(serialized, uint32ToBytes(row.reqSigs)...) - nKeys := uint32(len(row.pubKeysEncrypted)) - serialized = append(serialized, uint32ToBytes(nKeys)...) - - var privKeyEncrypted []byte - for i, pubKeyEncrypted := range row.pubKeysEncrypted { - // check that the encrypted length is correct - if len(pubKeyEncrypted) != seriesKeyLength { - str := fmt.Sprintf("wrong length of Encrypted Public Key: %v", - pubKeyEncrypted) - return nil, newError(ErrSeriesSerialization, str, nil) - } - serialized = append(serialized, pubKeyEncrypted...) - - if len(row.privKeysEncrypted) == 0 { - privKeyEncrypted = seriesNullPrivKey[:] - } else { - privKeyEncrypted = row.privKeysEncrypted[i] - } - - if privKeyEncrypted == nil { - serialized = append(serialized, seriesNullPrivKey[:]...) - } else if len(privKeyEncrypted) != seriesKeyLength { - str := fmt.Sprintf("wrong length of Encrypted Private Key: %v", - len(privKeyEncrypted)) - return nil, newError(ErrSeriesSerialization, str, nil) - } else { - serialized = append(serialized, privKeyEncrypted...) - } - } - return serialized, nil -} - -// serializeWithdrawal constructs a dbWithdrawalRow and serializes it (using -// encoding/gob) so that it can be stored in the DB. -func serializeWithdrawal(requests []OutputRequest, startAddress WithdrawalAddress, - lastSeriesID uint32, changeStart ChangeAddress, dustThreshold btcutil.Amount, - status WithdrawalStatus) ([]byte, error) { - - dbStartAddr := dbWithdrawalAddress{ - SeriesID: startAddress.SeriesID(), - Branch: startAddress.Branch(), - Index: startAddress.Index(), - } - dbChangeStart := dbChangeAddress{ - SeriesID: startAddress.SeriesID(), - Index: startAddress.Index(), - } - dbRequests := make([]dbOutputRequest, len(requests)) - for i, request := range requests { - dbRequests[i] = dbOutputRequest{ - Addr: request.Address.EncodeAddress(), - Amount: request.Amount, - Server: request.Server, - Transaction: request.Transaction, - } - } - dbOutputs := make(map[OutBailmentID]dbWithdrawalOutput, len(status.outputs)) - for oid, output := range status.outputs { - dbOutpoints := make([]dbOutBailmentOutpoint, len(output.outpoints)) - for i, outpoint := range output.outpoints { - dbOutpoints[i] = dbOutBailmentOutpoint{ - Ntxid: outpoint.ntxid, - Index: outpoint.index, - Amount: outpoint.amount, - } - } - dbOutputs[oid] = dbWithdrawalOutput{ - OutBailmentID: output.request.outBailmentID(), - Status: output.status, - Outpoints: dbOutpoints, - } - } - dbTransactions := make(map[Ntxid]dbChangeAwareTx, len(status.transactions)) - for ntxid, tx := range status.transactions { - var buf bytes.Buffer - buf.Grow(tx.SerializeSize()) - if err := tx.Serialize(&buf); err != nil { - return nil, err - } - dbTransactions[ntxid] = dbChangeAwareTx{ - SerializedMsgTx: buf.Bytes(), - ChangeIdx: tx.changeIdx, - } - } - nextChange := status.nextChangeAddr - dbStatus := dbWithdrawalStatus{ - NextChangeAddr: dbChangeAddress{ - SeriesID: nextChange.seriesID, - Index: nextChange.index, - }, - Fees: status.fees, - Outputs: dbOutputs, - Sigs: status.sigs, - Transactions: dbTransactions, - } - row := dbWithdrawalRow{ - Requests: dbRequests, - StartAddress: dbStartAddr, - LastSeriesID: lastSeriesID, - ChangeStart: dbChangeStart, - DustThreshold: dustThreshold, - Status: dbStatus, - } - var buf bytes.Buffer - if err := gob.NewEncoder(&buf).Encode(row); err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -// deserializeWithdrawal deserializes the given byte slice into a dbWithdrawalRow, -// converts it into an withdrawalInfo and returns it. This function must run -// with the address manager unlocked. -func deserializeWithdrawal(p *Pool, ns, addrmgrNs walletdb.ReadBucket, serialized []byte) (*withdrawalInfo, error) { - var row dbWithdrawalRow - if err := gob.NewDecoder(bytes.NewReader(serialized)).Decode(&row); err != nil { - return nil, newError(ErrWithdrawalStorage, "cannot deserialize withdrawal information", - err) - } - wInfo := &withdrawalInfo{ - lastSeriesID: row.LastSeriesID, - dustThreshold: row.DustThreshold, - } - chainParams := p.Manager().ChainParams() - wInfo.requests = make([]OutputRequest, len(row.Requests)) - // A map of requests indexed by OutBailmentID; needed to populate - // WithdrawalStatus.Outputs later on. - requestsByOID := make(map[OutBailmentID]OutputRequest) - for i, req := range row.Requests { - addr, err := btcutil.DecodeAddress(req.Addr, chainParams) - if err != nil { - return nil, newError(ErrWithdrawalStorage, - "cannot deserialize addr for requested output", err) - } - pkScript, err := txscript.PayToAddrScript(addr) - if err != nil { - return nil, newError(ErrWithdrawalStorage, "invalid addr for requested output", err) - } - request := OutputRequest{ - Address: addr, - Amount: req.Amount, - PkScript: pkScript, - Server: req.Server, - Transaction: req.Transaction, - } - wInfo.requests[i] = request - requestsByOID[request.outBailmentID()] = request - } - startAddr := row.StartAddress - wAddr, err := p.WithdrawalAddress(ns, addrmgrNs, startAddr.SeriesID, startAddr.Branch, startAddr.Index) - if err != nil { - return nil, newError(ErrWithdrawalStorage, "cannot deserialize startAddress", err) - } - wInfo.startAddress = *wAddr - - cAddr, err := p.ChangeAddress(row.ChangeStart.SeriesID, row.ChangeStart.Index) - if err != nil { - return nil, newError(ErrWithdrawalStorage, "cannot deserialize changeStart", err) - } - wInfo.changeStart = *cAddr - - // TODO: Copy over row.Status.nextInputAddr. Not done because StartWithdrawal - // does not update that yet. - nextChangeAddr := row.Status.NextChangeAddr - cAddr, err = p.ChangeAddress(nextChangeAddr.SeriesID, nextChangeAddr.Index) - if err != nil { - return nil, newError(ErrWithdrawalStorage, - "cannot deserialize nextChangeAddress for withdrawal", err) - } - wInfo.status = WithdrawalStatus{ - nextChangeAddr: *cAddr, - fees: row.Status.Fees, - outputs: make(map[OutBailmentID]*WithdrawalOutput, len(row.Status.Outputs)), - sigs: row.Status.Sigs, - transactions: make(map[Ntxid]changeAwareTx, len(row.Status.Transactions)), - } - for oid, output := range row.Status.Outputs { - outpoints := make([]OutBailmentOutpoint, len(output.Outpoints)) - for i, outpoint := range output.Outpoints { - outpoints[i] = OutBailmentOutpoint{ - ntxid: outpoint.Ntxid, - index: outpoint.Index, - amount: outpoint.Amount, - } - } - wInfo.status.outputs[oid] = &WithdrawalOutput{ - request: requestsByOID[output.OutBailmentID], - status: output.Status, - outpoints: outpoints, - } - } - for ntxid, tx := range row.Status.Transactions { - var msgtx wire.MsgTx - if err := msgtx.Deserialize(bytes.NewBuffer(tx.SerializedMsgTx)); err != nil { - return nil, newError(ErrWithdrawalStorage, "cannot deserialize transaction", err) - } - wInfo.status.transactions[ntxid] = changeAwareTx{ - MsgTx: &msgtx, - changeIdx: tx.ChangeIdx, - } - } - return wInfo, nil -} - -func putWithdrawal(ns walletdb.ReadWriteBucket, poolID []byte, roundID uint32, serialized []byte) error { - bucket := ns.NestedReadWriteBucket(poolID) - return bucket.Put(uint32ToBytes(roundID), serialized) -} - -func getWithdrawal(ns walletdb.ReadBucket, poolID []byte, roundID uint32) []byte { - bucket := ns.NestedReadBucket(poolID) - return bucket.Get(uint32ToBytes(roundID)) -} - -// uint32ToBytes converts a 32 bit unsigned integer into a 4-byte slice in -// little-endian order: 1 -> [1 0 0 0]. -func uint32ToBytes(number uint32) []byte { - buf := make([]byte, 4) - binary.LittleEndian.PutUint32(buf, number) - return buf -} - -// bytesToUint32 converts a 4-byte slice in little-endian order into a 32 bit -// unsigned integer: [1 0 0 0] -> 1. -func bytesToUint32(encoded []byte) uint32 { - return binary.LittleEndian.Uint32(encoded) -} diff --git a/votingpool/db_wb_test.go b/votingpool/db_wb_test.go deleted file mode 100644 index 10d2d22..0000000 --- a/votingpool/db_wb_test.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) 2015 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package votingpool - -import ( - "bytes" - "reflect" - "testing" - - "github.com/btcsuite/btcwallet/walletdb" -) - -func TestPutUsedAddrHash(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dummyHash := bytes.Repeat([]byte{0x09}, 10) - err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { - ns, _ := TstRWNamespaces(tx) - return putUsedAddrHash(ns, pool.ID, 0, 0, 0, dummyHash) - }) - if err != nil { - t.Fatal(err) - } - - var storedHash []byte - err = walletdb.View(db, func(tx walletdb.ReadTx) error { - ns, _ := TstRNamespaces(tx) - storedHash = getUsedAddrHash(ns, pool.ID, 0, 0, 0) - return nil - }) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(storedHash, dummyHash) { - t.Fatalf("Wrong stored hash; got %x, want %x", storedHash, dummyHash) - } -} - -func TestGetMaxUsedIdx(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { - ns, _ := TstRWNamespaces(tx) - for i, idx := range []int{0, 7, 9, 3001, 41, 500, 6} { - dummyHash := bytes.Repeat([]byte{byte(i)}, 10) - err := putUsedAddrHash(ns, pool.ID, 0, 0, Index(idx), dummyHash) - if err != nil { - return err - } - } - return nil - }) - if err != nil { - t.Fatal(err) - } - - var maxIdx Index - err = walletdb.View(db, func(tx walletdb.ReadTx) error { - ns, _ := TstRNamespaces(tx) - var err error - maxIdx, err = getMaxUsedIdx(ns, pool.ID, 0, 0) - return err - }) - if err != nil { - t.Fatal(err) - } - if maxIdx != Index(3001) { - t.Fatalf("Wrong max idx; got %d, want %d", maxIdx, Index(3001)) - } -} - -func TestWithdrawalSerialization(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, addrmgrNs := TstRWNamespaces(dbtx) - - roundID := uint32(0) - wi := createAndFulfillWithdrawalRequests(t, dbtx, pool, roundID) - - serialized, err := serializeWithdrawal(wi.requests, wi.startAddress, wi.lastSeriesID, - wi.changeStart, wi.dustThreshold, wi.status) - if err != nil { - t.Fatal(err) - } - - var wInfo *withdrawalInfo - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - wInfo, err = deserializeWithdrawal(pool, ns, addrmgrNs, serialized) - if err != nil { - t.Fatal(err) - } - }) - - if !reflect.DeepEqual(wInfo.startAddress, wi.startAddress) { - t.Fatalf("Wrong startAddr; got %v, want %v", wInfo.startAddress, wi.startAddress) - } - - if !reflect.DeepEqual(wInfo.changeStart, wi.changeStart) { - t.Fatalf("Wrong changeStart; got %v, want %v", wInfo.changeStart, wi.changeStart) - } - - if wInfo.lastSeriesID != wi.lastSeriesID { - t.Fatalf("Wrong LastSeriesID; got %d, want %d", wInfo.lastSeriesID, wi.lastSeriesID) - } - - if wInfo.dustThreshold != wi.dustThreshold { - t.Fatalf("Wrong DustThreshold; got %d, want %d", wInfo.dustThreshold, wi.dustThreshold) - } - - if !reflect.DeepEqual(wInfo.requests, wi.requests) { - t.Fatalf("Wrong output requests; got %v, want %v", wInfo.requests, wi.requests) - } - - TstCheckWithdrawalStatusMatches(t, wInfo.status, wi.status) -} - -func TestPutAndGetWithdrawal(t *testing.T) { - tearDown, db, _ := TstCreatePool(t) - defer tearDown() - - serialized := bytes.Repeat([]byte{1}, 10) - poolID := []byte{0x00} - roundID := uint32(0) - err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { - ns, _ := TstRWNamespaces(tx) - return putWithdrawal(ns, poolID, roundID, serialized) - }) - if err != nil { - t.Fatal(err) - } - - var retrieved []byte - err = walletdb.View(db, func(tx walletdb.ReadTx) error { - ns, _ := TstRNamespaces(tx) - retrieved = getWithdrawal(ns, poolID, roundID) - return nil - }) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(retrieved, serialized) { - t.Fatalf("Wrong value retrieved from DB; got %x, want %x", retrieved, serialized) - } -} diff --git a/votingpool/doc.go b/votingpool/doc.go deleted file mode 100644 index a9d00ee..0000000 --- a/votingpool/doc.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2014 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -/* -Package votingpool provides voting pool functionality for btcwallet. - -Overview - -The purpose of the voting pool package is to make it possible to store -bitcoins using m-of-n multisig transactions. A pool can have multiple -series, each of them with a set of pubkeys (one for each of the members -in that pool's series) and the minimum number of required signatures (m) -needed to spend the pool's coins. Each member will hold a private key -matching one of the series' public keys, and at least m members will -need to be in agreement when spending the pool's coins. - -More details about voting pools as well as some of its use cases can -be found at http://opentransactions.org/wiki/index.php?title=Category:Voting_Pools - -This package depends on the waddrmgr and walletdb packages. - -Creating a voting pool - -A voting pool is created via the Create function. This function -accepts a database namespace which will be used to store all -information related to that pool under a bucket whose key is the -pool's ID. - -Loading an existing pool - -An existing voting pool is loaded via the Load function, which accepts -the database name used when creating the pool as well as the poolID. - -Creating a series - -A series can be created via the CreateSeries method, which accepts a -version number, a series identifier, a number of required signatures -(m in m-of-n multisig), and a set of public keys. - -Deposit Addresses - -A deposit address can be created via the DepositScriptAddress -method, which returns a series-specific P2SH address from the multi-sig -script constructed with the index-th child of the series' public keys and -sorted according to the given branch. The procedure to construct multi-sig -deposit addresses is described in detail at -http://opentransactions.org/wiki/index.php/Deposit_Address_(voting_pools) - -Replacing a series - -A series can be replaced via the ReplaceSeries method. It accepts -the same parameters as the CreateSeries method. - -Empowering a series - -For security reasons, most private keys will be maintained offline and -only brought online when they're needed. In order to bring a key online, -one must use the EmpowerSeries method, which takes just the series ID -and a raw private key matching one of the series' public keys. - -Starting withdrawals - -When withdrawing coins from the pool, we employ a deterministic process -in order to minimise the cost of coordinating transaction signing. For -this to work, members of the pool have to perform an out-of-band consensus -process () -to define the following parameters, that should be passed to the -StartWithdrawal method: - - roundID: the unique identifier of a given consensus round - requests: a list with outputs requested by users of the voting pool - startAddress: the seriesID, branch and indes where we should start looking for inputs - lastSeriesID: the ID of the last series where we should take inputs from - changeStart: the first change address to use - dustThreshold: the minimum amount of satoshis an input needs to be considered eligible - -StartWithdrawal will then select all eligible inputs in the given address -range (following the algorithim at ) -and use them to construct transactions () -that fulfill the output requests. It returns a WithdrawalStatus containing -the state of every requested output, the raw signatures for the constructed -transactions, the network fees included in those transactions and the input -range to use in the next withdrawal. - -*/ -package votingpool diff --git a/votingpool/error.go b/votingpool/error.go deleted file mode 100644 index 4d83d94..0000000 --- a/votingpool/error.go +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (c) 2014 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package votingpool - -import "fmt" - -// ErrorCode identifies a kind of error -type ErrorCode int - -const ( - // ErrInputSelection indicates an error in the input selection - // algorithm. - ErrInputSelection ErrorCode = iota - - // ErrWithdrawalProcessing indicates an internal error when processing a - // withdrawal request. - ErrWithdrawalProcessing - - // ErrUnknownPubKey indicates a pubkey that does not belong to a given - // series. - ErrUnknownPubKey - - // ErrSeriesSerialization indicates that an error occurred while - // serializing or deserializing one or more series for storing into - // the database. - ErrSeriesSerialization - - // ErrSeriesVersion indicates that we've been asked to deal with a series - // whose version is unsupported - ErrSeriesVersion - - // ErrSeriesNotExists indicates that an attempt has been made to access - // a series that does not exist. - ErrSeriesNotExists - - // ErrSeriesAlreadyExists indicates that an attempt has been made to - // create a series that already exists. - ErrSeriesAlreadyExists - - // ErrSeriesAlreadyEmpowered indicates that an already empowered series - // was used where a not empowered one was expected. - ErrSeriesAlreadyEmpowered - - // ErrSeriesNotActive indicates that an active series was needed but the - // selected one is not. - ErrSeriesNotActive - - // ErrKeyIsPrivate indicates that a private key was used where a public - // one was expected. - ErrKeyIsPrivate - - // ErrKeyIsPublic indicates that a public key was used where a private - // one was expected. - ErrKeyIsPublic - - // ErrKeyNeuter indicates a problem when trying to neuter a private key. - ErrKeyNeuter - - // ErrKeyMismatch indicates that the key is not the expected one. - ErrKeyMismatch - - // ErrKeysPrivatePublicMismatch indicates that the number of private and - // public keys is not the same. - ErrKeysPrivatePublicMismatch - - // ErrKeyDuplicate indicates that a key is duplicated. - ErrKeyDuplicate - - // ErrTooFewPublicKeys indicates that a required minimum of public - // keys was not met. - ErrTooFewPublicKeys - - // ErrPoolAlreadyExists indicates that an attempt has been made to - // create a voting pool that already exists. - ErrPoolAlreadyExists - - // ErrPoolNotExists indicates that an attempt has been made to access - // a voting pool that does not exist. - ErrPoolNotExists - - // ErrScriptCreation indicates that the creation of a deposit script - // failed. - ErrScriptCreation - - // ErrTooManyReqSignatures indicates that too many required - // signatures are requested. - ErrTooManyReqSignatures - - // ErrInvalidBranch indicates that the given branch number is not valid - // for a given set of public keys. - ErrInvalidBranch - - // ErrInvalidValue indicates that the value of a given function argument - // is invalid. - ErrInvalidValue - - // ErrDatabase indicates an error with the underlying database. - ErrDatabase - - // ErrKeyChain indicates an error with the key chain typically either - // due to the inability to create an extended key or deriving a child - // extended key. - ErrKeyChain - - // ErrCrypto indicates an error with the cryptography related operations - // such as decrypting or encrypting data, parsing an EC public key, - // or deriving a secret key from a password. - ErrCrypto - - // ErrRawSigning indicates an error in the process of generating raw - // signatures for a transaction input. - ErrRawSigning - - // ErrPreconditionNotMet indicates a programming error since a - // preconditon has not been met. - ErrPreconditionNotMet - - // ErrTxSigning indicates an error when signing a transaction. - ErrTxSigning - - // ErrSeriesIDNotSequential indicates an attempt to create a series with - // an ID that is not sequantial. - ErrSeriesIDNotSequential - - // ErrInvalidScriptHash indicates an invalid P2SH. - ErrInvalidScriptHash - - // ErrWithdrawFromUnusedAddr indicates an attempt to withdraw funds from - // an address which has not been used before. - ErrWithdrawFromUnusedAddr - - // ErrSeriesIDInvalid indicates an attempt to create a series with an - // invalid ID. - ErrSeriesIDInvalid - - // ErrWithdrawalTxStorage indicates an error when storing withdrawal - // transactions. - ErrWithdrawalTxStorage - - // ErrWithdrawalStorage indicates an error occurred when serializing or - // deserializing withdrawal information. - ErrWithdrawalStorage - - // lastErr is used for testing, making it possible to iterate over - // the error codes in order to check that they all have proper - // translations in errorCodeStrings. - lastErr -) - -// Map of ErrorCode values back to their constant names for pretty printing. -var errorCodeStrings = map[ErrorCode]string{ - ErrInputSelection: "ErrInputSelection", - ErrWithdrawalProcessing: "ErrWithdrawalProcessing", - ErrUnknownPubKey: "ErrUnknownPubKey", - ErrSeriesSerialization: "ErrSeriesSerialization", - ErrSeriesVersion: "ErrSeriesVersion", - ErrSeriesNotExists: "ErrSeriesNotExists", - ErrSeriesAlreadyExists: "ErrSeriesAlreadyExists", - ErrSeriesAlreadyEmpowered: "ErrSeriesAlreadyEmpowered", - ErrSeriesIDNotSequential: "ErrSeriesIDNotSequential", - ErrSeriesIDInvalid: "ErrSeriesIDInvalid", - ErrSeriesNotActive: "ErrSeriesNotActive", - ErrKeyIsPrivate: "ErrKeyIsPrivate", - ErrKeyIsPublic: "ErrKeyIsPublic", - ErrKeyNeuter: "ErrKeyNeuter", - ErrKeyMismatch: "ErrKeyMismatch", - ErrKeysPrivatePublicMismatch: "ErrKeysPrivatePublicMismatch", - ErrKeyDuplicate: "ErrKeyDuplicate", - ErrTooFewPublicKeys: "ErrTooFewPublicKeys", - ErrPoolAlreadyExists: "ErrPoolAlreadyExists", - ErrPoolNotExists: "ErrPoolNotExists", - ErrScriptCreation: "ErrScriptCreation", - ErrTooManyReqSignatures: "ErrTooManyReqSignatures", - ErrInvalidBranch: "ErrInvalidBranch", - ErrInvalidValue: "ErrInvalidValue", - ErrDatabase: "ErrDatabase", - ErrKeyChain: "ErrKeyChain", - ErrCrypto: "ErrCrypto", - ErrRawSigning: "ErrRawSigning", - ErrPreconditionNotMet: "ErrPreconditionNotMet", - ErrTxSigning: "ErrTxSigning", - ErrInvalidScriptHash: "ErrInvalidScriptHash", - ErrWithdrawFromUnusedAddr: "ErrWithdrawFromUnusedAddr", - ErrWithdrawalTxStorage: "ErrWithdrawalTxStorage", - ErrWithdrawalStorage: "ErrWithdrawalStorage", -} - -// String returns the ErrorCode as a human-readable name. -func (e ErrorCode) String() string { - if s := errorCodeStrings[e]; s != "" { - return s - } - return fmt.Sprintf("Unknown ErrorCode (%d)", int(e)) -} - -// Error is a typed error for all errors arising during the -// operation of the voting pool. -type Error struct { - ErrorCode ErrorCode // Describes the kind of error - Description string // Human readable description of the issue - Err error // Underlying error -} - -// Error satisfies the error interface and prints human-readable errors. -func (e Error) Error() string { - if e.Err != nil { - return e.Description + ": " + e.Err.Error() - } - return e.Description -} - -// newError creates a new Error. -func newError(c ErrorCode, desc string, err error) Error { - return Error{ErrorCode: c, Description: desc, Err: err} -} diff --git a/votingpool/error_test.go b/votingpool/error_test.go deleted file mode 100644 index 0ada043..0000000 --- a/votingpool/error_test.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) 2014 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package votingpool_test - -import ( - "testing" - - vp "github.com/btcsuite/btcwallet/votingpool" -) - -// TestErrorCodeStringer tests that all error codes has a text -// representation and that text representation is still correct, -// ie. that a refactoring and renaming of the error code has not -// drifted from the textual representation. -func TestErrorCodeStringer(t *testing.T) { - // All the errors in ths - tests := []struct { - in vp.ErrorCode - want string - }{ - {vp.ErrInputSelection, "ErrInputSelection"}, - {vp.ErrWithdrawalProcessing, "ErrWithdrawalProcessing"}, - {vp.ErrUnknownPubKey, "ErrUnknownPubKey"}, - {vp.ErrSeriesSerialization, "ErrSeriesSerialization"}, - {vp.ErrSeriesVersion, "ErrSeriesVersion"}, - {vp.ErrSeriesNotExists, "ErrSeriesNotExists"}, - {vp.ErrSeriesAlreadyExists, "ErrSeriesAlreadyExists"}, - {vp.ErrSeriesAlreadyEmpowered, "ErrSeriesAlreadyEmpowered"}, - {vp.ErrSeriesIDNotSequential, "ErrSeriesIDNotSequential"}, - {vp.ErrSeriesIDInvalid, "ErrSeriesIDInvalid"}, - {vp.ErrSeriesNotActive, "ErrSeriesNotActive"}, - {vp.ErrKeyIsPrivate, "ErrKeyIsPrivate"}, - {vp.ErrKeyIsPublic, "ErrKeyIsPublic"}, - {vp.ErrKeyNeuter, "ErrKeyNeuter"}, - {vp.ErrKeyMismatch, "ErrKeyMismatch"}, - {vp.ErrKeysPrivatePublicMismatch, "ErrKeysPrivatePublicMismatch"}, - {vp.ErrKeyDuplicate, "ErrKeyDuplicate"}, - {vp.ErrTooFewPublicKeys, "ErrTooFewPublicKeys"}, - {vp.ErrPoolAlreadyExists, "ErrPoolAlreadyExists"}, - {vp.ErrPoolNotExists, "ErrPoolNotExists"}, - {vp.ErrScriptCreation, "ErrScriptCreation"}, - {vp.ErrTooManyReqSignatures, "ErrTooManyReqSignatures"}, - {vp.ErrInvalidBranch, "ErrInvalidBranch"}, - {vp.ErrInvalidValue, "ErrInvalidValue"}, - {vp.ErrDatabase, "ErrDatabase"}, - {vp.ErrKeyChain, "ErrKeyChain"}, - {vp.ErrCrypto, "ErrCrypto"}, - {vp.ErrRawSigning, "ErrRawSigning"}, - {vp.ErrPreconditionNotMet, "ErrPreconditionNotMet"}, - {vp.ErrTxSigning, "ErrTxSigning"}, - {vp.ErrInvalidScriptHash, "ErrInvalidScriptHash"}, - {vp.ErrWithdrawFromUnusedAddr, "ErrWithdrawFromUnusedAddr"}, - {vp.ErrWithdrawalTxStorage, "ErrWithdrawalTxStorage"}, - {vp.ErrWithdrawalStorage, "ErrWithdrawalStorage"}, - {0xffff, "Unknown ErrorCode (65535)"}, - } - - if int(vp.TstLastErr) != len(tests)-1 { - t.Errorf("Wrong number of errorCodeStrings. Got: %d, want: %d", - int(vp.TstLastErr), len(tests)) - } - - for i, test := range tests { - result := test.in.String() - if result != test.want { - t.Errorf("String #%d\ngot: %s\nwant: %s", i, result, - test.want) - } - } -} diff --git a/votingpool/example_test.go b/votingpool/example_test.go deleted file mode 100644 index b865e65..0000000 --- a/votingpool/example_test.go +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Copyright (c) 2014-2017 The btcsuite developers - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -package votingpool_test - -import ( - "bytes" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "time" - - "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/votingpool" - "github.com/btcsuite/btcwallet/waddrmgr" - "github.com/btcsuite/btcwallet/walletdb" - _ "github.com/btcsuite/btcwallet/walletdb/bdb" - "github.com/btcsuite/btcwallet/wtxmgr" -) - -var ( - pubPassphrase = []byte("pubPassphrase") - privPassphrase = []byte("privPassphrase") - seed = bytes.Repeat([]byte{0x2a, 0x64, 0xdf, 0x08}, 8) - fastScrypt = &waddrmgr.ScryptOptions{N: 16, R: 8, P: 1} -) - -func createWaddrmgr(ns walletdb.ReadWriteBucket, params *chaincfg.Params) (*waddrmgr.Manager, error) { - err := waddrmgr.Create(ns, seed, pubPassphrase, privPassphrase, params, - fastScrypt, time.Now()) - if err != nil { - return nil, err - } - return waddrmgr.Open(ns, pubPassphrase, params) -} - -func ExampleCreate() { - // Create a new walletdb.DB. See the walletdb docs for instructions on how - // to do that. - db, dbTearDown, err := createWalletDB() - if err != nil { - fmt.Println(err) - return - } - defer dbTearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - fmt.Println(err) - return - } - defer dbtx.Commit() - - // Create a new walletdb namespace for the address manager. - mgrNamespace, err := dbtx.CreateTopLevelBucket([]byte("waddrmgr")) - if err != nil { - fmt.Println(err) - return - } - - // Create the address manager. - mgr, err := createWaddrmgr(mgrNamespace, &chaincfg.MainNetParams) - if err != nil { - fmt.Println(err) - return - } - - // Create a walletdb namespace for votingpools. - vpNamespace, err := dbtx.CreateTopLevelBucket([]byte("votingpool")) - if err != nil { - fmt.Println(err) - return - } - - // Create a voting pool. - _, err = votingpool.Create(vpNamespace, mgr, []byte{0x00}) - if err != nil { - fmt.Println(err) - return - } - - // Output: - // -} - -// This example demonstrates how to create a voting pool with one -// series and get a deposit address for that series. -func Example_depositAddress() { - // Create the address manager and votingpool DB namespace. See the example - // for the Create() function for more info on how this is done. - teardown, db, mgr := exampleCreateDBAndMgr() - defer teardown() - - err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { - ns := votingpoolNamespace(tx) - - // Create the voting pool. - pool, err := votingpool.Create(ns, mgr, []byte{0x00}) - if err != nil { - return err - } - - // Create a 2-of-3 series. - seriesID := uint32(1) - requiredSignatures := uint32(2) - pubKeys := []string{ - "xpub661MyMwAqRbcFDDrR5jY7LqsRioFDwg3cLjc7tML3RRcfYyhXqqgCH5SqMSQdpQ1Xh8EtVwcfm8psD8zXKPcRaCVSY4GCqbb3aMEs27GitE", - "xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9", - "xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh", - } - err = pool.CreateSeries(ns, votingpool.CurrentVersion, seriesID, requiredSignatures, pubKeys) - if err != nil { - return err - } - - // Create a deposit address. - addr, err := pool.DepositScriptAddress(seriesID, votingpool.Branch(0), votingpool.Index(1)) - if err != nil { - return err - } - fmt.Println("Generated deposit address:", addr.EncodeAddress()) - return nil - }) - if err != nil { - fmt.Println(err) - return - } - - // Output: - // Generated deposit address: 3QTzpc9d3tTbNLJLB7xwt87nWM38boAhAw -} - -// This example demonstrates how to empower a series by loading the private -// key for one of the series' public keys. -func Example_empowerSeries() { - // Create the address manager and votingpool DB namespace. See the example - // for the Create() function for more info on how this is done. - teardown, db, mgr := exampleCreateDBAndMgr() - defer teardown() - - // Create a pool and a series. See the DepositAddress example for more info - // on how this is done. - pool, seriesID := exampleCreatePoolAndSeries(db, mgr) - - err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { - ns := votingpoolNamespace(tx) - addrmgrNs := addrmgrNamespace(tx) - - // Now empower the series with one of its private keys. Notice that in order - // to do that we need to unlock the address manager. - err := mgr.Unlock(addrmgrNs, privPassphrase) - if err != nil { - return err - } - defer mgr.Lock() - privKey := "xprv9s21ZrQH143K2j9PK4CXkCu8sgxkpUxCF7p1KVwiV5tdnkeYzJXReUkxz5iB2FUzTXC1L15abCDG4RMxSYT5zhm67uvsnLYxuDhZfoFcB6a" - return pool.EmpowerSeries(ns, seriesID, privKey) - }) - if err != nil { - fmt.Println(err) - return - } - - // Output: - // -} - -// This example demonstrates how to use the Pool.StartWithdrawal method. -func Example_startWithdrawal() { - // Create the address manager and votingpool DB namespace. See the example - // for the Create() function for more info on how this is done. - teardown, db, mgr := exampleCreateDBAndMgr() - defer teardown() - - // Create a pool and a series. See the DepositAddress example for more info - // on how this is done. - pool, seriesID := exampleCreatePoolAndSeries(db, mgr) - - err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { - ns := votingpoolNamespace(tx) - addrmgrNs := addrmgrNamespace(tx) - txmgrNs := txmgrNamespace(tx) - - // Create the transaction store for later use. - txstore := exampleCreateTxStore(txmgrNs) - - // Unlock the manager - err := mgr.Unlock(addrmgrNs, privPassphrase) - if err != nil { - return err - } - defer mgr.Lock() - - addr, _ := btcutil.DecodeAddress("1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX", mgr.ChainParams()) - pkScript, _ := txscript.PayToAddrScript(addr) - requests := []votingpool.OutputRequest{ - { - PkScript: pkScript, - Address: addr, - Amount: 1e6, - Server: "server-id", - Transaction: 123, - }, - } - changeStart, err := pool.ChangeAddress(seriesID, votingpool.Index(0)) - if err != nil { - return err - } - // This is only needed because we have not used any deposit addresses from - // the series, and we cannot create a WithdrawalAddress for an unused - // branch/idx pair. - err = pool.EnsureUsedAddr(ns, addrmgrNs, seriesID, votingpool.Branch(1), votingpool.Index(0)) - if err != nil { - return err - } - startAddr, err := pool.WithdrawalAddress(ns, addrmgrNs, seriesID, votingpool.Branch(1), votingpool.Index(0)) - if err != nil { - return err - } - lastSeriesID := seriesID - dustThreshold := btcutil.Amount(1e4) - currentBlock := int32(19432) - roundID := uint32(0) - _, err = pool.StartWithdrawal(ns, addrmgrNs, - roundID, requests, *startAddr, lastSeriesID, *changeStart, txstore, txmgrNs, currentBlock, - dustThreshold) - return err - }) - if err != nil { - fmt.Println(err) - return - } - - // Output: - // -} - -func createWalletDB() (walletdb.DB, func(), error) { - dir, err := ioutil.TempDir("", "votingpool_example") - if err != nil { - return nil, nil, err - } - db, err := walletdb.Create("bdb", filepath.Join(dir, "wallet.db")) - if err != nil { - return nil, nil, err - } - dbTearDown := func() { - db.Close() - os.RemoveAll(dir) - } - return db, dbTearDown, nil -} - -var ( - addrmgrNamespaceKey = []byte("addrmgr") - txmgrNamespaceKey = []byte("txmgr") - votingpoolNamespaceKey = []byte("votingpool") -) - -func addrmgrNamespace(dbtx walletdb.ReadWriteTx) walletdb.ReadWriteBucket { - return dbtx.ReadWriteBucket(addrmgrNamespaceKey) -} - -func txmgrNamespace(dbtx walletdb.ReadWriteTx) walletdb.ReadWriteBucket { - return dbtx.ReadWriteBucket(txmgrNamespaceKey) -} - -func votingpoolNamespace(dbtx walletdb.ReadWriteTx) walletdb.ReadWriteBucket { - return dbtx.ReadWriteBucket(votingpoolNamespaceKey) -} - -func exampleCreateDBAndMgr() (teardown func(), db walletdb.DB, mgr *waddrmgr.Manager) { - db, dbTearDown, err := createWalletDB() - if err != nil { - dbTearDown() - panic(err) - } - - // Create a new walletdb namespace for the address manager. - err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { - addrmgrNs, err := tx.CreateTopLevelBucket(addrmgrNamespaceKey) - if err != nil { - return err - } - _, err = tx.CreateTopLevelBucket(votingpoolNamespaceKey) - if err != nil { - return err - } - _, err = tx.CreateTopLevelBucket(txmgrNamespaceKey) - if err != nil { - return err - } - // Create the address manager - mgr, err = createWaddrmgr(addrmgrNs, &chaincfg.MainNetParams) - return err - }) - if err != nil { - dbTearDown() - panic(err) - } - - teardown = func() { - mgr.Close() - dbTearDown() - } - - return teardown, db, mgr -} - -func exampleCreatePoolAndSeries(db walletdb.DB, mgr *waddrmgr.Manager) (pool *votingpool.Pool, seriesID uint32) { - err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { - ns := votingpoolNamespace(tx) - var err error - pool, err = votingpool.Create(ns, mgr, []byte{0x00}) - if err != nil { - return err - } - - // Create a 2-of-3 series. - seriesID = uint32(1) - requiredSignatures := uint32(2) - pubKeys := []string{ - "xpub661MyMwAqRbcFDDrR5jY7LqsRioFDwg3cLjc7tML3RRcfYyhXqqgCH5SqMSQdpQ1Xh8EtVwcfm8psD8zXKPcRaCVSY4GCqbb3aMEs27GitE", - "xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9", - "xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh", - } - err = pool.CreateSeries(ns, votingpool.CurrentVersion, seriesID, requiredSignatures, pubKeys) - if err != nil { - return err - } - return pool.ActivateSeries(ns, seriesID) - }) - if err != nil { - panic(err) - } - - return pool, seriesID -} - -func exampleCreateTxStore(ns walletdb.ReadWriteBucket) *wtxmgr.Store { - err := wtxmgr.Create(ns) - if err != nil { - panic(err) - } - s, err := wtxmgr.Open(ns, &chaincfg.MainNetParams) - if err != nil { - panic(err) - } - return s -} diff --git a/votingpool/factory_test.go b/votingpool/factory_test.go deleted file mode 100644 index 992785d..0000000 --- a/votingpool/factory_test.go +++ /dev/null @@ -1,472 +0,0 @@ -/* - * Copyright (c) 2014-2016 The btcsuite developers - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -// Helpers to create parameterized objects to use in tests. - -package votingpool - -import ( - "bytes" - "io/ioutil" - "os" - "path/filepath" - "sync/atomic" - "testing" - "time" - - "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" - "github.com/btcsuite/btcutil/hdkeychain" - "github.com/btcsuite/btcwallet/waddrmgr" - "github.com/btcsuite/btcwallet/walletdb" - "github.com/btcsuite/btcwallet/wtxmgr" -) - -var ( - // seed is the master seed used to create extended keys. - seed = bytes.Repeat([]byte{0x2a, 0x64, 0xdf, 0x08}, 8) - pubPassphrase = []byte("_DJr{fL4H0O}*-0\n:V1izc)(6BomK") - privPassphrase = []byte("81lUHXnOMZ@?XXd7O9xyDIWIbXX-lj") - uniqueCounter = uint32(0) - // The block height where all our test inputs are created. - TstInputsBlock = int32(10) -) - -func getUniqueID() uint32 { - return atomic.AddUint32(&uniqueCounter, 1) -} - -// createWithdrawalTx creates a withdrawalTx with the given input and output amounts. -func createWithdrawalTx(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, inputAmounts []int64, outputAmounts []int64) *withdrawalTx { - net := pool.Manager().ChainParams() - tx := newWithdrawalTx(defaultTxOptions) - _, credits := TstCreateCreditsOnNewSeries(t, dbtx, pool, inputAmounts) - for _, c := range credits { - tx.addInput(c) - } - for i, amount := range outputAmounts { - request := TstNewOutputRequest( - t, uint32(i), "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", btcutil.Amount(amount), net) - tx.addOutput(request) - } - return tx -} - -func createMsgTx(pkScript []byte, amts []int64) *wire.MsgTx { - msgtx := &wire.MsgTx{ - Version: 1, - TxIn: []*wire.TxIn{ - { - PreviousOutPoint: wire.OutPoint{ - Hash: chainhash.Hash{}, - Index: 0xffffffff, - }, - SignatureScript: []byte{txscript.OP_NOP}, - Sequence: 0xffffffff, - }, - }, - LockTime: 0, - } - - for _, amt := range amts { - msgtx.AddTxOut(wire.NewTxOut(amt, pkScript)) - } - return msgtx -} - -func TstNewDepositScript(t *testing.T, p *Pool, seriesID uint32, branch Branch, idx Index) []byte { - script, err := p.DepositScript(seriesID, branch, idx) - if err != nil { - t.Fatalf("Failed to create deposit script for series %d, branch %d, index %d: %v", - seriesID, branch, idx, err) - } - return script -} - -func TstRNamespaces(tx walletdb.ReadTx) (votingpoolNs, addrmgrNs walletdb.ReadBucket) { - return tx.ReadBucket(votingpoolNamespaceKey), tx.ReadBucket(addrmgrNamespaceKey) -} - -func TstRWNamespaces(tx walletdb.ReadWriteTx) (votingpoolNs, addrmgrNs walletdb.ReadWriteBucket) { - return tx.ReadWriteBucket(votingpoolNamespaceKey), tx.ReadWriteBucket(addrmgrNamespaceKey) -} - -func TstTxStoreRWNamespace(tx walletdb.ReadWriteTx) walletdb.ReadWriteBucket { - return tx.ReadWriteBucket(txmgrNamespaceKey) -} - -// TstEnsureUsedAddr ensures the addresses defined by the given series/branch and -// index==0..idx are present in the set of used addresses for the given Pool. -func TstEnsureUsedAddr(t *testing.T, dbtx walletdb.ReadWriteTx, p *Pool, seriesID uint32, branch Branch, idx Index) []byte { - ns, addrmgrNs := TstRWNamespaces(dbtx) - addr, err := p.getUsedAddr(ns, addrmgrNs, seriesID, branch, idx) - if err != nil { - t.Fatal(err) - } else if addr != nil { - var script []byte - TstRunWithManagerUnlocked(t, p.Manager(), addrmgrNs, func() { - script, err = addr.Script() - }) - if err != nil { - t.Fatal(err) - } - return script - } - TstRunWithManagerUnlocked(t, p.Manager(), addrmgrNs, func() { - err = p.EnsureUsedAddr(ns, addrmgrNs, seriesID, branch, idx) - }) - if err != nil { - t.Fatal(err) - } - return TstNewDepositScript(t, p, seriesID, branch, idx) -} - -func TstCreatePkScript(t *testing.T, dbtx walletdb.ReadWriteTx, p *Pool, seriesID uint32, branch Branch, idx Index) []byte { - script := TstEnsureUsedAddr(t, dbtx, p, seriesID, branch, idx) - addr, err := p.addressFor(script) - if err != nil { - t.Fatal(err) - } - pkScript, err := txscript.PayToAddrScript(addr) - if err != nil { - t.Fatal(err) - } - return pkScript -} - -type TstSeriesDef struct { - ReqSigs uint32 - PubKeys []string - PrivKeys []string - SeriesID uint32 - Inactive bool -} - -// TstCreateSeries creates a new Series for every definition in the given slice -// of TstSeriesDef. If the definition includes any private keys, the Series is -// empowered with them. -func TstCreateSeries(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, definitions []TstSeriesDef) { - ns, addrmgrNs := TstRWNamespaces(dbtx) - for _, def := range definitions { - err := pool.CreateSeries(ns, CurrentVersion, def.SeriesID, def.ReqSigs, def.PubKeys) - if err != nil { - t.Fatalf("Cannot creates series %d: %v", def.SeriesID, err) - } - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - for _, key := range def.PrivKeys { - if err := pool.EmpowerSeries(ns, def.SeriesID, key); err != nil { - t.Fatal(err) - } - } - }) - pool.Series(def.SeriesID).active = !def.Inactive - } -} - -func TstCreateMasterKey(t *testing.T, seed []byte) *hdkeychain.ExtendedKey { - key, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) - if err != nil { - t.Fatal(err) - } - return key -} - -// createMasterKeys creates count master ExtendedKeys with unique seeds. -func createMasterKeys(t *testing.T, count int) []*hdkeychain.ExtendedKey { - keys := make([]*hdkeychain.ExtendedKey, count) - for i := range keys { - keys[i] = TstCreateMasterKey(t, bytes.Repeat(uint32ToBytes(getUniqueID()), 4)) - } - return keys -} - -// TstCreateSeriesDef creates a TstSeriesDef with a unique SeriesID, the given -// reqSigs and the raw public/private keys extracted from the list of private -// keys. The new series will be empowered with all private keys. -func TstCreateSeriesDef(t *testing.T, pool *Pool, reqSigs uint32, keys []*hdkeychain.ExtendedKey) TstSeriesDef { - pubKeys := make([]string, len(keys)) - privKeys := make([]string, len(keys)) - for i, key := range keys { - privKeys[i] = key.String() - pubkey, _ := key.Neuter() - pubKeys[i] = pubkey.String() - } - seriesID := uint32(len(pool.seriesLookup)) + 1 - return TstSeriesDef{ - ReqSigs: reqSigs, SeriesID: seriesID, PubKeys: pubKeys, PrivKeys: privKeys} -} - -func TstCreatePoolAndTxStore(t *testing.T) (tearDown func(), db walletdb.DB, pool *Pool, store *wtxmgr.Store) { - teardown, db, pool := TstCreatePool(t) - store = TstCreateTxStore(t, db) - return teardown, db, pool, store -} - -// TstCreateCreditsOnNewSeries creates a new Series (with a unique ID) and a -// slice of credits locked to the series' address with branch==1 and index==0. -// The new Series will use a 2-of-3 configuration and will be empowered with -// all of its private keys. -func TstCreateCreditsOnNewSeries(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, amounts []int64) (uint32, []credit) { - masters := []*hdkeychain.ExtendedKey{ - TstCreateMasterKey(t, bytes.Repeat(uint32ToBytes(getUniqueID()), 4)), - TstCreateMasterKey(t, bytes.Repeat(uint32ToBytes(getUniqueID()), 4)), - TstCreateMasterKey(t, bytes.Repeat(uint32ToBytes(getUniqueID()), 4)), - } - def := TstCreateSeriesDef(t, pool, 2, masters) - TstCreateSeries(t, dbtx, pool, []TstSeriesDef{def}) - return def.SeriesID, TstCreateSeriesCredits(t, dbtx, pool, def.SeriesID, amounts) -} - -// TstCreateSeriesCredits creates a new credit for every item in the amounts -// slice, locked to the given series' address with branch==1 and index==0. -func TstCreateSeriesCredits(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, seriesID uint32, amounts []int64) []credit { - addr := TstNewWithdrawalAddress(t, dbtx, pool, seriesID, Branch(1), Index(0)) - pkScript, err := txscript.PayToAddrScript(addr.addr) - if err != nil { - t.Fatal(err) - } - msgTx := createMsgTx(pkScript, amounts) - txHash := msgTx.TxHash() - credits := make([]credit, len(amounts)) - for i := range msgTx.TxOut { - c := wtxmgr.Credit{ - OutPoint: wire.OutPoint{ - Hash: txHash, - Index: uint32(i), - }, - BlockMeta: wtxmgr.BlockMeta{ - Block: wtxmgr.Block{Height: TstInputsBlock}, - }, - Amount: btcutil.Amount(msgTx.TxOut[i].Value), - PkScript: msgTx.TxOut[i].PkScript, - } - credits[i] = newCredit(c, *addr) - } - return credits -} - -// TstCreateSeriesCreditsOnStore inserts a new credit in the given store for -// every item in the amounts slice. These credits are locked to the votingpool -// address composed of the given seriesID, branch==1 and index==0. -func TstCreateSeriesCreditsOnStore(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, seriesID uint32, amounts []int64, - store *wtxmgr.Store) []credit { - branch := Branch(1) - idx := Index(0) - pkScript := TstCreatePkScript(t, dbtx, pool, seriesID, branch, idx) - eligible := make([]credit, len(amounts)) - for i, credit := range TstCreateCreditsOnStore(t, dbtx, store, pkScript, amounts) { - eligible[i] = newCredit(credit, *TstNewWithdrawalAddress(t, dbtx, pool, seriesID, branch, idx)) - } - return eligible -} - -// TstCreateCreditsOnStore inserts a new credit in the given store for -// every item in the amounts slice. -func TstCreateCreditsOnStore(t *testing.T, dbtx walletdb.ReadWriteTx, s *wtxmgr.Store, pkScript []byte, amounts []int64) []wtxmgr.Credit { - msgTx := createMsgTx(pkScript, amounts) - meta := &wtxmgr.BlockMeta{ - Block: wtxmgr.Block{Height: TstInputsBlock}, - } - - rec, err := wtxmgr.NewTxRecordFromMsgTx(msgTx, time.Now()) - if err != nil { - t.Fatal(err) - } - - txmgrNs := dbtx.ReadWriteBucket(txmgrNamespaceKey) - if err := s.InsertTx(txmgrNs, rec, meta); err != nil { - t.Fatal("Failed to create inputs: ", err) - } - - credits := make([]wtxmgr.Credit, len(msgTx.TxOut)) - for i := range msgTx.TxOut { - if err := s.AddCredit(txmgrNs, rec, meta, uint32(i), false); err != nil { - t.Fatal("Failed to create inputs: ", err) - } - credits[i] = wtxmgr.Credit{ - OutPoint: wire.OutPoint{ - Hash: rec.Hash, - Index: uint32(i), - }, - BlockMeta: *meta, - Amount: btcutil.Amount(msgTx.TxOut[i].Value), - PkScript: msgTx.TxOut[i].PkScript, - } - } - return credits -} - -var ( - addrmgrNamespaceKey = []byte("waddrmgr") - votingpoolNamespaceKey = []byte("votingpool") - txmgrNamespaceKey = []byte("testtxstore") -) - -// TstCreatePool creates a Pool on a fresh walletdb and returns it. It also -// returns a teardown function that closes the Manager and removes the directory -// used to store the database. -func TstCreatePool(t *testing.T) (tearDownFunc func(), db walletdb.DB, pool *Pool) { - // This should be moved somewhere else eventually as not all of our tests - // call this function, but right now the only option would be to have the - // t.Parallel() call in each of our tests. - t.Parallel() - - // Create a new wallet DB and addr manager. - dir, err := ioutil.TempDir("", "pool_test") - if err != nil { - t.Fatalf("Failed to create db dir: %v", err) - } - db, err = walletdb.Create("bdb", filepath.Join(dir, "wallet.db")) - if err != nil { - t.Fatalf("Failed to create wallet DB: %v", err) - } - var addrMgr *waddrmgr.Manager - err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { - addrmgrNs, err := tx.CreateTopLevelBucket(addrmgrNamespaceKey) - if err != nil { - return err - } - votingpoolNs, err := tx.CreateTopLevelBucket(votingpoolNamespaceKey) - if err != nil { - return err - } - fastScrypt := &waddrmgr.ScryptOptions{N: 16, R: 8, P: 1} - err = waddrmgr.Create(addrmgrNs, seed, pubPassphrase, privPassphrase, - &chaincfg.MainNetParams, fastScrypt, time.Now()) - if err != nil { - return err - } - addrMgr, err = waddrmgr.Open(addrmgrNs, pubPassphrase, &chaincfg.MainNetParams) - if err != nil { - return err - } - pool, err = Create(votingpoolNs, addrMgr, []byte{0x00}) - return err - }) - if err != nil { - t.Fatalf("Could not set up DB: %v", err) - } - tearDownFunc = func() { - addrMgr.Close() - db.Close() - os.RemoveAll(dir) - } - return tearDownFunc, db, pool -} - -func TstCreateTxStore(t *testing.T, db walletdb.DB) *wtxmgr.Store { - var store *wtxmgr.Store - err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { - txmgrNs, err := tx.CreateTopLevelBucket(txmgrNamespaceKey) - if err != nil { - return err - } - err = wtxmgr.Create(txmgrNs) - if err != nil { - return err - } - store, err = wtxmgr.Open(txmgrNs, &chaincfg.MainNetParams) - return err - }) - if err != nil { - t.Fatalf("Failed to create txmgr: %v", err) - } - return store -} - -func TstNewOutputRequest(t *testing.T, transaction uint32, address string, amount btcutil.Amount, - net *chaincfg.Params) OutputRequest { - addr, err := btcutil.DecodeAddress(address, net) - if err != nil { - t.Fatalf("Unable to decode address %s", address) - } - pkScript, err := txscript.PayToAddrScript(addr) - if err != nil { - t.Fatalf("Unable to generate pkScript for %v", addr) - } - return OutputRequest{ - PkScript: pkScript, - Address: addr, - Amount: amount, - Server: "server", - Transaction: transaction, - } -} - -func TstNewWithdrawalOutput(r OutputRequest, status outputStatus, - outpoints []OutBailmentOutpoint) *WithdrawalOutput { - output := &WithdrawalOutput{ - request: r, - status: status, - outpoints: outpoints, - } - return output -} - -func TstNewWithdrawalAddress(t *testing.T, dbtx walletdb.ReadWriteTx, p *Pool, seriesID uint32, branch Branch, - index Index) (addr *WithdrawalAddress) { - TstEnsureUsedAddr(t, dbtx, p, seriesID, branch, index) - ns, addrmgrNs := TstRNamespaces(dbtx) - var err error - TstRunWithManagerUnlocked(t, p.Manager(), addrmgrNs, func() { - addr, err = p.WithdrawalAddress(ns, addrmgrNs, seriesID, branch, index) - }) - if err != nil { - t.Fatalf("Failed to get WithdrawalAddress: %v", err) - } - return addr -} - -func TstNewChangeAddress(t *testing.T, p *Pool, seriesID uint32, idx Index) (addr *ChangeAddress) { - addr, err := p.ChangeAddress(seriesID, idx) - if err != nil { - t.Fatalf("Failed to get ChangeAddress: %v", err) - } - return addr -} - -func TstConstantFee(fee btcutil.Amount) func() btcutil.Amount { - return func() btcutil.Amount { return fee } -} - -func createAndFulfillWithdrawalRequests(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, roundID uint32) withdrawalInfo { - - params := pool.Manager().ChainParams() - seriesID, eligible := TstCreateCreditsOnNewSeries(t, dbtx, pool, []int64{2e6, 4e6}) - requests := []OutputRequest{ - TstNewOutputRequest(t, 1, "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", 3e6, params), - TstNewOutputRequest(t, 2, "3PbExiaztsSYgh6zeMswC49hLUwhTQ86XG", 2e6, params), - } - changeStart := TstNewChangeAddress(t, pool, seriesID, 0) - dustThreshold := btcutil.Amount(1e4) - startAddr := TstNewWithdrawalAddress(t, dbtx, pool, seriesID, 1, 0) - lastSeriesID := seriesID - w := newWithdrawal(roundID, requests, eligible, *changeStart) - if err := w.fulfillRequests(); err != nil { - t.Fatal(err) - } - return withdrawalInfo{ - requests: requests, - startAddress: *startAddr, - changeStart: *changeStart, - lastSeriesID: lastSeriesID, - dustThreshold: dustThreshold, - status: *w.status, - } -} diff --git a/votingpool/input_selection.go b/votingpool/input_selection.go deleted file mode 100644 index 39cbe34..0000000 --- a/votingpool/input_selection.go +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright (c) 2015-2016 The btcsuite developers - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -package votingpool - -import ( - "bytes" - "fmt" - "sort" - - "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/walletdb" - "github.com/btcsuite/btcwallet/wtxmgr" -) - -const eligibleInputMinConfirmations = 100 - -// credit is an abstraction over wtxmgr.Credit used in the construction of -// voting pool withdrawal transactions. -type credit struct { - wtxmgr.Credit - addr WithdrawalAddress -} - -func newCredit(c wtxmgr.Credit, addr WithdrawalAddress) credit { - return credit{Credit: c, addr: addr} -} - -func (c *credit) String() string { - return fmt.Sprintf("credit of %v locked to %v", c.Amount, c.addr) -} - -// byAddress defines the methods needed to satisify sort.Interface to sort a -// slice of credits by their address. -type byAddress []credit - -func (c byAddress) Len() int { return len(c) } -func (c byAddress) Swap(i, j int) { c[i], c[j] = c[j], c[i] } - -// Less returns true if the element at positions i is smaller than the -// element at position j. The 'smaller-than' relation is defined to be -// the lexicographic ordering defined on the tuple (SeriesID, Index, -// Branch, TxSha, OutputIndex). -func (c byAddress) Less(i, j int) bool { - iAddr := c[i].addr - jAddr := c[j].addr - if iAddr.seriesID < jAddr.seriesID { - return true - } - if iAddr.seriesID > jAddr.seriesID { - return false - } - - // The seriesID are equal, so compare index. - if iAddr.index < jAddr.index { - return true - } - if iAddr.index > jAddr.index { - return false - } - - // The seriesID and index are equal, so compare branch. - if iAddr.branch < jAddr.branch { - return true - } - if iAddr.branch > jAddr.branch { - return false - } - - // The seriesID, index, and branch are equal, so compare hash. - txidComparison := bytes.Compare(c[i].OutPoint.Hash[:], c[j].OutPoint.Hash[:]) - if txidComparison < 0 { - return true - } - if txidComparison > 0 { - return false - } - - // The seriesID, index, branch, and hash are equal, so compare output - // index. - return c[i].OutPoint.Index < c[j].OutPoint.Index -} - -// getEligibleInputs returns eligible inputs with addresses between startAddress -// and the last used address of lastSeriesID. They're reverse ordered based on -// their address. -func (p *Pool) getEligibleInputs(ns, addrmgrNs walletdb.ReadBucket, store *wtxmgr.Store, txmgrNs walletdb.ReadBucket, startAddress WithdrawalAddress, - lastSeriesID uint32, dustThreshold btcutil.Amount, chainHeight int32, - minConf int) ([]credit, error) { - - if p.Series(lastSeriesID) == nil { - str := fmt.Sprintf("lastSeriesID (%d) does not exist", lastSeriesID) - return nil, newError(ErrSeriesNotExists, str, nil) - } - unspents, err := store.UnspentOutputs(txmgrNs) - if err != nil { - return nil, newError(ErrInputSelection, "failed to get unspent outputs", err) - } - addrMap, err := groupCreditsByAddr(unspents, p.manager.ChainParams()) - if err != nil { - return nil, err - } - var inputs []credit - address := startAddress - for { - log.Debugf("Looking for eligible inputs at address %v", address.addrIdentifier()) - if candidates, ok := addrMap[address.addr.EncodeAddress()]; ok { - var eligibles []credit - for _, c := range candidates { - candidate := newCredit(c, address) - if p.isCreditEligible(candidate, minConf, chainHeight, dustThreshold) { - eligibles = append(eligibles, candidate) - } - } - inputs = append(inputs, eligibles...) - } - nAddr, err := nextAddr(p, ns, addrmgrNs, address.seriesID, address.branch, address.index, lastSeriesID+1) - if err != nil { - return nil, newError(ErrInputSelection, "failed to get next withdrawal address", err) - } else if nAddr == nil { - log.Debugf("getEligibleInputs: reached last addr, stopping") - break - } - address = *nAddr - } - sort.Sort(sort.Reverse(byAddress(inputs))) - return inputs, nil -} - -// nextAddr returns the next WithdrawalAddress according to the input selection -// rules: http://opentransactions.org/wiki/index.php/Input_Selection_Algorithm_(voting_pools) -// It returns nil if the new address' seriesID is >= stopSeriesID. -func nextAddr(p *Pool, ns, addrmgrNs walletdb.ReadBucket, seriesID uint32, branch Branch, index Index, stopSeriesID uint32) ( - *WithdrawalAddress, error) { - series := p.Series(seriesID) - if series == nil { - return nil, newError(ErrSeriesNotExists, fmt.Sprintf("unknown seriesID: %d", seriesID), nil) - } - branch++ - if int(branch) > len(series.publicKeys) { - highestIdx, err := p.highestUsedSeriesIndex(ns, seriesID) - if err != nil { - return nil, err - } - if index > highestIdx { - seriesID++ - log.Debugf("nextAddr(): reached last branch (%d) and highest used index (%d), "+ - "moving on to next series (%d)", branch, index, seriesID) - index = 0 - } else { - index++ - } - branch = 0 - } - - if seriesID >= stopSeriesID { - return nil, nil - } - - addr, err := p.WithdrawalAddress(ns, addrmgrNs, seriesID, branch, index) - if err != nil && err.(Error).ErrorCode == ErrWithdrawFromUnusedAddr { - // The used indices will vary between branches so sometimes we'll try to - // get a WithdrawalAddress that hasn't been used before, and in such - // cases we just need to move on to the next one. - log.Debugf("nextAddr(): skipping addr (series #%d, branch #%d, index #%d) as it hasn't "+ - "been used before", seriesID, branch, index) - return nextAddr(p, ns, addrmgrNs, seriesID, branch, index, stopSeriesID) - } - return addr, err -} - -// highestUsedSeriesIndex returns the highest index among all of this Pool's -// used addresses for the given seriesID. It returns 0 if there are no used -// addresses with the given seriesID. -func (p *Pool) highestUsedSeriesIndex(ns walletdb.ReadBucket, seriesID uint32) (Index, error) { - maxIdx := Index(0) - series := p.Series(seriesID) - if series == nil { - return maxIdx, - newError(ErrSeriesNotExists, fmt.Sprintf("unknown seriesID: %d", seriesID), nil) - } - for i := range series.publicKeys { - idx, err := p.highestUsedIndexFor(ns, seriesID, Branch(i)) - if err != nil { - return Index(0), err - } - if idx > maxIdx { - maxIdx = idx - } - } - return maxIdx, nil -} - -// groupCreditsByAddr converts a slice of credits to a map from the string -// representation of an encoded address to the unspent outputs associated with -// that address. -func groupCreditsByAddr(credits []wtxmgr.Credit, chainParams *chaincfg.Params) ( - map[string][]wtxmgr.Credit, error) { - addrMap := make(map[string][]wtxmgr.Credit) - for _, c := range credits { - _, addrs, _, err := txscript.ExtractPkScriptAddrs(c.PkScript, chainParams) - if err != nil { - return nil, newError(ErrInputSelection, "failed to obtain input address", err) - } - // As our credits are all P2SH we should never have more than one - // address per credit, so let's error out if that assumption is - // violated. - if len(addrs) != 1 { - return nil, newError(ErrInputSelection, "input doesn't have exactly one address", nil) - } - encAddr := addrs[0].EncodeAddress() - if v, ok := addrMap[encAddr]; ok { - addrMap[encAddr] = append(v, c) - } else { - addrMap[encAddr] = []wtxmgr.Credit{c} - } - } - - return addrMap, nil -} - -// isCreditEligible tests a given credit for eligibilty with respect -// to number of confirmations, the dust threshold and that it is not -// the charter output. -func (p *Pool) isCreditEligible(c credit, minConf int, chainHeight int32, - dustThreshold btcutil.Amount) bool { - if c.Amount < dustThreshold { - return false - } - if confirms(c.BlockMeta.Block.Height, chainHeight) < int32(minConf) { - return false - } - if p.isCharterOutput(c) { - return false - } - - return true -} - -// isCharterOutput - TODO: In order to determine this, we need the txid -// and the output index of the current charter output, which we don't have yet. -func (p *Pool) isCharterOutput(c credit) bool { - return false -} - -// confirms returns the number of confirmations for a transaction in a block at -// height txHeight (or -1 for an unconfirmed tx) given the chain height -// curHeight. -func confirms(txHeight, curHeight int32) int32 { - switch { - case txHeight == -1, txHeight > curHeight: - return 0 - default: - return curHeight - txHeight + 1 - } -} diff --git a/votingpool/input_selection_wb_test.go b/votingpool/input_selection_wb_test.go deleted file mode 100644 index 8136968..0000000 --- a/votingpool/input_selection_wb_test.go +++ /dev/null @@ -1,400 +0,0 @@ -// Copyright (c) 2015-2017 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package votingpool - -import ( - "bytes" - "reflect" - "sort" - "testing" - - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/walletdb" - "github.com/btcsuite/btcwallet/wtxmgr" -) - -var ( - // random small number of satoshis used as dustThreshold - dustThreshold btcutil.Amount = 1e4 -) - -func TestGetEligibleInputs(t *testing.T) { - tearDown, db, pool, store := TstCreatePoolAndTxStore(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, addrmgrNs := TstRWNamespaces(dbtx) - - series := []TstSeriesDef{ - {ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1}, - {ReqSigs: 2, PubKeys: TstPubKeys[3:6], SeriesID: 2}, - } - TstCreateSeries(t, dbtx, pool, series) - scripts := append( - getPKScriptsForAddressRange(t, dbtx, pool, 1, 0, 2, 0, 4), - getPKScriptsForAddressRange(t, dbtx, pool, 2, 0, 2, 0, 6)...) - - // Create two eligible inputs locked to each of the PKScripts above. - expNoEligibleInputs := 2 * len(scripts) - eligibleAmounts := []int64{int64(dustThreshold + 1), int64(dustThreshold + 1)} - var inputs []wtxmgr.Credit - for i := 0; i < len(scripts); i++ { - created := TstCreateCreditsOnStore(t, dbtx, store, scripts[i], eligibleAmounts) - inputs = append(inputs, created...) - } - - startAddr := TstNewWithdrawalAddress(t, dbtx, pool, 1, 0, 0) - lastSeriesID := uint32(2) - currentBlock := int32(TstInputsBlock + eligibleInputMinConfirmations + 1) - var eligibles []credit - txmgrNs := dbtx.ReadBucket(txmgrNamespaceKey) - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - eligibles, err = pool.getEligibleInputs(ns, addrmgrNs, - store, txmgrNs, *startAddr, lastSeriesID, dustThreshold, int32(currentBlock), - eligibleInputMinConfirmations) - }) - if err != nil { - t.Fatal("InputSelection failed:", err) - } - - // Check we got the expected number of eligible inputs. - if len(eligibles) != expNoEligibleInputs { - t.Fatalf("Wrong number of eligible inputs returned. Got: %d, want: %d.", - len(eligibles), expNoEligibleInputs) - } - - // Check that the returned eligibles are reverse sorted by address. - if !sort.IsSorted(sort.Reverse(byAddress(eligibles))) { - t.Fatal("Eligible inputs are not sorted.") - } - - // Check that all credits are unique - checkUniqueness(t, eligibles) -} - -func TestNextAddrWithVaryingHighestIndices(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, addrmgrNs := TstRWNamespaces(dbtx) - - series := []TstSeriesDef{ - {ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1}, - } - TstCreateSeries(t, dbtx, pool, series) - stopSeriesID := uint32(2) - - // Populate the used addr DB for branch 0 and indices ranging from 0 to 2. - TstEnsureUsedAddr(t, dbtx, pool, 1, Branch(0), 2) - - // Populate the used addr DB for branch 1 and indices ranging from 0 to 1. - TstEnsureUsedAddr(t, dbtx, pool, 1, Branch(1), 1) - - // Start with the address for branch==0, index==1. - addr := TstNewWithdrawalAddress(t, dbtx, pool, 1, 0, 1) - - // The first call to nextAddr() should give us the address for branch==1 - // and index==1. - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - addr, err = nextAddr(pool, ns, addrmgrNs, addr.seriesID, addr.branch, addr.index, stopSeriesID) - }) - if err != nil { - t.Fatalf("Failed to get next address: %v", err) - } - checkWithdrawalAddressMatches(t, addr, 1, Branch(1), 1) - - // The next call should give us the address for branch==0, index==2 since - // there are no used addresses for branch==2. - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - addr, err = nextAddr(pool, ns, addrmgrNs, addr.seriesID, addr.branch, addr.index, stopSeriesID) - }) - if err != nil { - t.Fatalf("Failed to get next address: %v", err) - } - checkWithdrawalAddressMatches(t, addr, 1, Branch(0), 2) - - // Since the last addr for branch==1 was the one with index==1, a subsequent - // call will return nil. - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - addr, err = nextAddr(pool, ns, addrmgrNs, addr.seriesID, addr.branch, addr.index, stopSeriesID) - }) - if err != nil { - t.Fatalf("Failed to get next address: %v", err) - } - if addr != nil { - t.Fatalf("Wrong next addr; got '%s', want 'nil'", addr.addrIdentifier()) - } -} - -func TestNextAddr(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, addrmgrNs := TstRWNamespaces(dbtx) - - series := []TstSeriesDef{ - {ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1}, - {ReqSigs: 2, PubKeys: TstPubKeys[3:6], SeriesID: 2}, - } - TstCreateSeries(t, dbtx, pool, series) - stopSeriesID := uint32(3) - - lastIdx := Index(10) - // Populate used addresses DB with entries for seriesID==1, branch==0..3, - // idx==0..10. - for _, i := range []int{0, 1, 2, 3} { - TstEnsureUsedAddr(t, dbtx, pool, 1, Branch(i), lastIdx) - } - addr := TstNewWithdrawalAddress(t, dbtx, pool, 1, 0, lastIdx-1) - // nextAddr() first increments just the branch, which ranges from 0 to 3 - // here (because our series has 3 public keys). - for _, i := range []int{1, 2, 3} { - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - addr, err = nextAddr(pool, ns, addrmgrNs, addr.seriesID, addr.branch, addr.index, stopSeriesID) - }) - if err != nil { - t.Fatalf("Failed to get next address: %v", err) - } - checkWithdrawalAddressMatches(t, addr, 1, Branch(i), lastIdx-1) - } - - // The last nextAddr() above gave us the addr with branch=3, - // idx=lastIdx-1, so the next 4 calls should give us the addresses with - // branch=[0-3] and idx=lastIdx. - for _, i := range []int{0, 1, 2, 3} { - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - addr, err = nextAddr(pool, ns, addrmgrNs, addr.seriesID, addr.branch, addr.index, stopSeriesID) - }) - if err != nil { - t.Fatalf("Failed to get next address: %v", err) - } - checkWithdrawalAddressMatches(t, addr, 1, Branch(i), lastIdx) - } - - // Populate used addresses DB with entries for seriesID==2, branch==0..3, - // idx==0..10. - for _, i := range []int{0, 1, 2, 3} { - TstEnsureUsedAddr(t, dbtx, pool, 2, Branch(i), lastIdx) - } - // Now we've gone through all the available branch/idx combinations, so - // we should move to the next series and start again with branch=0, idx=0. - for _, i := range []int{0, 1, 2, 3} { - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - addr, err = nextAddr(pool, ns, addrmgrNs, addr.seriesID, addr.branch, addr.index, stopSeriesID) - }) - if err != nil { - t.Fatalf("Failed to get next address: %v", err) - } - checkWithdrawalAddressMatches(t, addr, 2, Branch(i), 0) - } - - // Finally check that nextAddr() returns nil when we've reached the last - // available address before stopSeriesID. - addr = TstNewWithdrawalAddress(t, dbtx, pool, 2, 3, lastIdx) - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - addr, err = nextAddr(pool, ns, addrmgrNs, addr.seriesID, addr.branch, addr.index, stopSeriesID) - }) - if err != nil { - t.Fatalf("Failed to get next address: %v", err) - } - if addr != nil { - t.Fatalf("Wrong WithdrawalAddress; got %s, want nil", addr.addrIdentifier()) - } -} - -func TestEligibleInputsAreEligible(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - var chainHeight int32 = 1000 - _, credits := TstCreateCreditsOnNewSeries(t, dbtx, pool, []int64{int64(dustThreshold)}) - c := credits[0] - // Make sure credit is old enough to pass the minConf check. - c.BlockMeta.Height = int32(eligibleInputMinConfirmations) - - if !pool.isCreditEligible(c, eligibleInputMinConfirmations, chainHeight, dustThreshold) { - t.Errorf("Input is not eligible and it should be.") - } -} - -func TestNonEligibleInputsAreNotEligible(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - var chainHeight int32 = 1000 - _, credits := TstCreateCreditsOnNewSeries(t, dbtx, pool, []int64{int64(dustThreshold - 1)}) - c := credits[0] - // Make sure credit is old enough to pass the minConf check. - c.BlockMeta.Height = int32(eligibleInputMinConfirmations) - - // Check that credit below dustThreshold is rejected. - if pool.isCreditEligible(c, eligibleInputMinConfirmations, chainHeight, dustThreshold) { - t.Errorf("Input is eligible and it should not be.") - } - - // Check that a credit with not enough confirmations is rejected. - _, credits = TstCreateCreditsOnNewSeries(t, dbtx, pool, []int64{int64(dustThreshold)}) - c = credits[0] - // The calculation of if it has been confirmed does this: chainheigt - bh + - // 1 >= target, which is quite weird, but the reason why I need to put 902 - // is *that* makes 1000 - 902 +1 = 99 >= 100 false - c.BlockMeta.Height = int32(902) - if pool.isCreditEligible(c, eligibleInputMinConfirmations, chainHeight, dustThreshold) { - t.Errorf("Input is eligible and it should not be.") - } -} - -func TestCreditSortingByAddress(t *testing.T) { - teardown, db, pool := TstCreatePool(t) - defer teardown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - series := []TstSeriesDef{ - {ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1}, - {ReqSigs: 2, PubKeys: TstPubKeys[3:6], SeriesID: 2}, - } - TstCreateSeries(t, dbtx, pool, series) - - shaHash0 := bytes.Repeat([]byte{0}, 32) - shaHash1 := bytes.Repeat([]byte{1}, 32) - shaHash2 := bytes.Repeat([]byte{2}, 32) - c0 := newDummyCredit(t, dbtx, pool, 1, 0, 0, shaHash0, 0) - c1 := newDummyCredit(t, dbtx, pool, 1, 0, 0, shaHash0, 1) - c2 := newDummyCredit(t, dbtx, pool, 1, 0, 0, shaHash1, 0) - c3 := newDummyCredit(t, dbtx, pool, 1, 0, 0, shaHash2, 0) - c4 := newDummyCredit(t, dbtx, pool, 1, 0, 1, shaHash0, 0) - c5 := newDummyCredit(t, dbtx, pool, 1, 1, 0, shaHash0, 0) - c6 := newDummyCredit(t, dbtx, pool, 2, 0, 0, shaHash0, 0) - - randomCredits := [][]credit{ - {c6, c5, c4, c3, c2, c1, c0}, - {c2, c1, c0, c6, c5, c4, c3}, - {c6, c4, c5, c2, c3, c0, c1}, - } - - want := []credit{c0, c1, c2, c3, c4, c5, c6} - - for _, random := range randomCredits { - sort.Sort(byAddress(random)) - got := random - - if len(got) != len(want) { - t.Fatalf("Sorted credit slice size wrong: Got: %d, want: %d", - len(got), len(want)) - } - - for idx := 0; idx < len(want); idx++ { - if !reflect.DeepEqual(got[idx], want[idx]) { - t.Errorf("Wrong output index. Got: %v, want: %v", - got[idx], want[idx]) - } - } - } -} - -// newDummyCredit creates a new credit with the given hash and outpointIdx, -// locked to the votingpool address identified by the given -// series/index/branch. -func newDummyCredit(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, series uint32, index Index, branch Branch, - txHash []byte, outpointIdx uint32) credit { - var hash chainhash.Hash - if err := hash.SetBytes(txHash); err != nil { - t.Fatal(err) - } - // Ensure the address defined by the given series/branch/index is present on - // the set of used addresses as that's a requirement of WithdrawalAddress. - TstEnsureUsedAddr(t, dbtx, pool, series, branch, index) - addr := TstNewWithdrawalAddress(t, dbtx, pool, series, branch, index) - c := wtxmgr.Credit{ - OutPoint: wire.OutPoint{ - Hash: hash, - Index: outpointIdx, - }, - } - return newCredit(c, *addr) -} - -func checkUniqueness(t *testing.T, credits byAddress) { - type uniq struct { - series uint32 - branch Branch - index Index - hash chainhash.Hash - outputIndex uint32 - } - - uniqMap := make(map[uniq]bool) - for _, c := range credits { - u := uniq{ - series: c.addr.SeriesID(), - branch: c.addr.Branch(), - index: c.addr.Index(), - hash: c.OutPoint.Hash, - outputIndex: c.OutPoint.Index, - } - if _, exists := uniqMap[u]; exists { - t.Fatalf("Duplicate found: %v", u) - } else { - uniqMap[u] = true - } - } -} - -func getPKScriptsForAddressRange(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, seriesID uint32, - startBranch, stopBranch Branch, startIdx, stopIdx Index) [][]byte { - var pkScripts [][]byte - for idx := startIdx; idx <= stopIdx; idx++ { - for branch := startBranch; branch <= stopBranch; branch++ { - pkScripts = append(pkScripts, TstCreatePkScript(t, dbtx, pool, seriesID, branch, idx)) - } - } - return pkScripts -} - -func checkWithdrawalAddressMatches(t *testing.T, addr *WithdrawalAddress, seriesID uint32, - branch Branch, index Index) { - if addr.SeriesID() != seriesID { - t.Fatalf("Wrong seriesID; got %d, want %d", addr.SeriesID(), seriesID) - } - if addr.Branch() != branch { - t.Fatalf("Wrong branch; got %d, want %d", addr.Branch(), branch) - } - if addr.Index() != index { - t.Fatalf("Wrong index; got %d, want %d", addr.Index(), index) - } -} diff --git a/votingpool/internal_test.go b/votingpool/internal_test.go deleted file mode 100644 index 36054d4..0000000 --- a/votingpool/internal_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2014 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package votingpool - -import ( - "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcutil/hdkeychain" - "github.com/btcsuite/btcwallet/waddrmgr" - "github.com/btcsuite/btcwallet/walletdb" -) - -var TstLastErr = lastErr - -const TstEligibleInputMinConfirmations = eligibleInputMinConfirmations - -// TstPutSeries transparently wraps the voting pool putSeries method. -func (vp *Pool) TstPutSeries(ns walletdb.ReadWriteBucket, version, seriesID, reqSigs uint32, inRawPubKeys []string) error { - return vp.putSeries(ns, version, seriesID, reqSigs, inRawPubKeys) -} - -var TstBranchOrder = branchOrder - -// TstExistsSeries checks whether a series is stored in the database. -func (vp *Pool) TstExistsSeries(dbtx walletdb.ReadTx, seriesID uint32) (bool, error) { - ns, _ := TstRNamespaces(dbtx) - poolBucket := ns.NestedReadBucket(vp.ID) - if poolBucket == nil { - return false, nil - } - bucket := poolBucket.NestedReadBucket(seriesBucketName) - if bucket == nil { - return false, nil - } - return bucket.Get(uint32ToBytes(seriesID)) != nil, nil -} - -// TstGetRawPublicKeys gets a series public keys in string format. -func (s *SeriesData) TstGetRawPublicKeys() []string { - rawKeys := make([]string, len(s.publicKeys)) - for i, key := range s.publicKeys { - rawKeys[i] = key.String() - } - return rawKeys -} - -// TstGetRawPrivateKeys gets a series private keys in string format. -func (s *SeriesData) TstGetRawPrivateKeys() []string { - rawKeys := make([]string, len(s.privateKeys)) - for i, key := range s.privateKeys { - if key != nil { - rawKeys[i] = key.String() - } - } - return rawKeys -} - -// TstGetReqSigs expose the series reqSigs attribute. -func (s *SeriesData) TstGetReqSigs() uint32 { - return s.reqSigs -} - -// TstEmptySeriesLookup empties the voting pool seriesLookup attribute. -func (vp *Pool) TstEmptySeriesLookup() { - vp.seriesLookup = make(map[uint32]*SeriesData) -} - -// TstDecryptExtendedKey expose the decryptExtendedKey method. -func (vp *Pool) TstDecryptExtendedKey(keyType waddrmgr.CryptoKeyType, encrypted []byte) (*hdkeychain.ExtendedKey, error) { - return vp.decryptExtendedKey(keyType, encrypted) -} - -// TstGetMsgTx returns a copy of the withdrawal transaction with the given -// ntxid. -func (s *WithdrawalStatus) TstGetMsgTx(ntxid Ntxid) *wire.MsgTx { - return s.transactions[ntxid].MsgTx.Copy() -} diff --git a/votingpool/log.go b/votingpool/log.go deleted file mode 100644 index 83cb964..0000000 --- a/votingpool/log.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2015 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package votingpool - -import "github.com/btcsuite/btclog" - -// 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 btclog.Logger - -// The default amount of logging is none. -func init() { - DisableLog() -} - -// DisableLog disables all library log output. Logging output is disabled -// by default until either UseLogger or SetLogWriter are called. -func DisableLog() { - log = btclog.Disabled -} - -// UseLogger uses a specified Logger to output package logging info. -// This should be used in preference to SetLogWriter if the caller is also -// using btclog. -func UseLogger(logger btclog.Logger) { - log = logger -} diff --git a/votingpool/pool.go b/votingpool/pool.go deleted file mode 100644 index c8dd75d..0000000 --- a/votingpool/pool.go +++ /dev/null @@ -1,841 +0,0 @@ -// Copyright (c) 2014 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package votingpool - -import ( - "fmt" - "sort" - - "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcutil/hdkeychain" - "github.com/btcsuite/btcwallet/internal/zero" - "github.com/btcsuite/btcwallet/waddrmgr" - "github.com/btcsuite/btcwallet/walletdb" -) - -const ( - minSeriesPubKeys = 3 - // CurrentVersion is the version used for newly created Series. - CurrentVersion = 1 -) - -// Branch is the type used to represent a branch number in a series. -type Branch uint32 - -// Index is the type used to represent an index number in a series. -type Index uint32 - -// SeriesData represents a Series for a given Pool. -type SeriesData struct { - version uint32 - // Whether or not a series is active. This is serialized/deserialized but - // for now there's no way to deactivate a series. - active bool - // A.k.a. "m" in "m of n signatures needed". - reqSigs uint32 - publicKeys []*hdkeychain.ExtendedKey - privateKeys []*hdkeychain.ExtendedKey -} - -// Pool represents an arrangement of notary servers to securely -// store and account for customer cryptocurrency deposits and to redeem -// valid withdrawals. For details about how the arrangement works, see -// http://opentransactions.org/wiki/index.php?title=Category:Voting_Pools -type Pool struct { - ID []byte - seriesLookup map[uint32]*SeriesData - manager *waddrmgr.Manager -} - -// PoolAddress represents a voting pool P2SH address, generated by -// deriving public HD keys from the series' master keys using the given -// branch/index and constructing a M-of-N multi-sig script. -type PoolAddress interface { - SeriesID() uint32 - Branch() Branch - Index() Index -} - -type poolAddress struct { - pool *Pool - addr btcutil.Address - script []byte - seriesID uint32 - branch Branch - index Index -} - -// ChangeAddress is a votingpool address meant to be used on transaction change -// outputs. All change addresses have branch==0. -type ChangeAddress struct { - *poolAddress -} - -// WithdrawalAddress is a votingpool address that may contain unspent outputs -// available for use in a withdrawal. -type WithdrawalAddress struct { - *poolAddress -} - -// Create creates a new entry in the database with the given ID -// and returns the Pool representing it. -func Create(ns walletdb.ReadWriteBucket, m *waddrmgr.Manager, poolID []byte) (*Pool, error) { - err := putPool(ns, poolID) - if err != nil { - str := fmt.Sprintf("unable to add voting pool %v to db", poolID) - return nil, newError(ErrPoolAlreadyExists, str, err) - } - return newPool(m, poolID), nil -} - -// Load fetches the entry in the database with the given ID and returns the Pool -// representing it. -func Load(ns walletdb.ReadBucket, m *waddrmgr.Manager, poolID []byte) (*Pool, error) { - if !existsPool(ns, poolID) { - str := fmt.Sprintf("unable to find voting pool %v in db", poolID) - return nil, newError(ErrPoolNotExists, str, nil) - } - p := newPool(m, poolID) - if err := p.LoadAllSeries(ns); err != nil { - return nil, err - } - return p, nil -} - -// newPool creates a new Pool instance. -func newPool(m *waddrmgr.Manager, poolID []byte) *Pool { - return &Pool{ - ID: poolID, - seriesLookup: make(map[uint32]*SeriesData), - manager: m, - } -} - -// LoadAndGetDepositScript generates and returns a deposit script for the given seriesID, -// branch and index of the Pool identified by poolID. -func LoadAndGetDepositScript(ns walletdb.ReadBucket, m *waddrmgr.Manager, poolID string, seriesID uint32, branch Branch, index Index) ([]byte, error) { - pid := []byte(poolID) - p, err := Load(ns, m, pid) - if err != nil { - return nil, err - } - script, err := p.DepositScript(seriesID, branch, index) - if err != nil { - return nil, err - } - return script, nil -} - -// LoadAndCreateSeries loads the Pool with the given ID, creating a new one if it doesn't -// yet exist, and then creates and returns a Series with the given seriesID, rawPubKeys -// and reqSigs. See CreateSeries for the constraints enforced on rawPubKeys and reqSigs. -func LoadAndCreateSeries(ns walletdb.ReadWriteBucket, m *waddrmgr.Manager, version uint32, - poolID string, seriesID, reqSigs uint32, rawPubKeys []string) error { - pid := []byte(poolID) - p, err := Load(ns, m, pid) - if err != nil { - vpErr := err.(Error) - if vpErr.ErrorCode == ErrPoolNotExists { - p, err = Create(ns, m, pid) - if err != nil { - return err - } - } else { - return err - } - } - return p.CreateSeries(ns, version, seriesID, reqSigs, rawPubKeys) -} - -// LoadAndReplaceSeries loads the voting pool with the given ID and calls ReplaceSeries, -// passing the given series ID, public keys and reqSigs to it. -func LoadAndReplaceSeries(ns walletdb.ReadWriteBucket, m *waddrmgr.Manager, version uint32, - poolID string, seriesID, reqSigs uint32, rawPubKeys []string) error { - pid := []byte(poolID) - p, err := Load(ns, m, pid) - if err != nil { - return err - } - return p.ReplaceSeries(ns, version, seriesID, reqSigs, rawPubKeys) -} - -// LoadAndEmpowerSeries loads the voting pool with the given ID and calls EmpowerSeries, -// passing the given series ID and private key to it. -func LoadAndEmpowerSeries(ns walletdb.ReadWriteBucket, m *waddrmgr.Manager, - poolID string, seriesID uint32, rawPrivKey string) error { - pid := []byte(poolID) - pool, err := Load(ns, m, pid) - if err != nil { - return err - } - return pool.EmpowerSeries(ns, seriesID, rawPrivKey) -} - -// Series returns the series with the given ID, or nil if it doesn't -// exist. -func (p *Pool) Series(seriesID uint32) *SeriesData { - series, exists := p.seriesLookup[seriesID] - if !exists { - return nil - } - return series -} - -// Manager returns the waddrmgr.Manager used by this Pool. -func (p *Pool) Manager() *waddrmgr.Manager { - return p.manager -} - -// saveSeriesToDisk stores the given series ID and data in the database, -// first encrypting the public/private extended keys. -// -// This method must be called with the Pool's manager unlocked. -func (p *Pool) saveSeriesToDisk(ns walletdb.ReadWriteBucket, seriesID uint32, data *SeriesData) error { - var err error - encryptedPubKeys := make([][]byte, len(data.publicKeys)) - for i, pubKey := range data.publicKeys { - encryptedPubKeys[i], err = p.manager.Encrypt( - waddrmgr.CKTPublic, []byte(pubKey.String())) - if err != nil { - str := fmt.Sprintf("key %v failed encryption", pubKey) - return newError(ErrCrypto, str, err) - } - } - encryptedPrivKeys := make([][]byte, len(data.privateKeys)) - for i, privKey := range data.privateKeys { - if privKey == nil { - encryptedPrivKeys[i] = nil - } else { - encryptedPrivKeys[i], err = p.manager.Encrypt( - waddrmgr.CKTPrivate, []byte(privKey.String())) - } - if err != nil { - str := fmt.Sprintf("key %v failed encryption", privKey) - return newError(ErrCrypto, str, err) - } - } - - err = putSeries(ns, p.ID, data.version, seriesID, data.active, - data.reqSigs, encryptedPubKeys, encryptedPrivKeys) - if err != nil { - str := fmt.Sprintf("cannot put series #%d into db", seriesID) - return newError(ErrSeriesSerialization, str, err) - } - return nil -} - -// CanonicalKeyOrder will return a copy of the input canonically -// ordered which is defined to be lexicographical. -func CanonicalKeyOrder(keys []string) []string { - orderedKeys := make([]string, len(keys)) - copy(orderedKeys, keys) - sort.Sort(sort.StringSlice(orderedKeys)) - return orderedKeys -} - -// Convert the given slice of strings into a slice of ExtendedKeys, -// checking that all of them are valid public (and not private) keys, -// and that there are no duplicates. -func convertAndValidatePubKeys(rawPubKeys []string) ([]*hdkeychain.ExtendedKey, error) { - seenKeys := make(map[string]bool) - keys := make([]*hdkeychain.ExtendedKey, len(rawPubKeys)) - for i, rawPubKey := range rawPubKeys { - if _, seen := seenKeys[rawPubKey]; seen { - str := fmt.Sprintf("duplicated public key: %v", rawPubKey) - return nil, newError(ErrKeyDuplicate, str, nil) - } - seenKeys[rawPubKey] = true - - key, err := hdkeychain.NewKeyFromString(rawPubKey) - if err != nil { - str := fmt.Sprintf("invalid extended public key %v", rawPubKey) - return nil, newError(ErrKeyChain, str, err) - } - - if key.IsPrivate() { - str := fmt.Sprintf("private keys not accepted: %v", rawPubKey) - return nil, newError(ErrKeyIsPrivate, str, nil) - } - keys[i] = key - } - return keys, nil -} - -// putSeries creates a new seriesData with the given arguments, ordering the -// given public keys (using CanonicalKeyOrder), validating and converting them -// to hdkeychain.ExtendedKeys, saves that to disk and adds it to this voting -// pool's seriesLookup map. It also ensures inRawPubKeys has at least -// minSeriesPubKeys items and reqSigs is not greater than the number of items in -// inRawPubKeys. -// -// This method must be called with the Pool's manager unlocked. -func (p *Pool) putSeries(ns walletdb.ReadWriteBucket, version, seriesID, reqSigs uint32, inRawPubKeys []string) error { - if len(inRawPubKeys) < minSeriesPubKeys { - str := fmt.Sprintf("need at least %d public keys to create a series", minSeriesPubKeys) - return newError(ErrTooFewPublicKeys, str, nil) - } - - if reqSigs > uint32(len(inRawPubKeys)) { - str := fmt.Sprintf( - "the number of required signatures cannot be more than the number of keys") - return newError(ErrTooManyReqSignatures, str, nil) - } - - rawPubKeys := CanonicalKeyOrder(inRawPubKeys) - - keys, err := convertAndValidatePubKeys(rawPubKeys) - if err != nil { - return err - } - - data := &SeriesData{ - version: version, - active: false, - reqSigs: reqSigs, - publicKeys: keys, - privateKeys: make([]*hdkeychain.ExtendedKey, len(keys)), - } - - err = p.saveSeriesToDisk(ns, seriesID, data) - if err != nil { - return err - } - p.seriesLookup[seriesID] = data - return nil -} - -// CreateSeries will create and return a new non-existing series. -// -// - seriesID must be greater than or equal 1; -// - rawPubKeys has to contain three or more public keys; -// - reqSigs has to be less or equal than the number of public keys in rawPubKeys. -func (p *Pool) CreateSeries(ns walletdb.ReadWriteBucket, version, seriesID, reqSigs uint32, rawPubKeys []string) error { - if seriesID == 0 { - return newError(ErrSeriesIDInvalid, "series ID cannot be 0", nil) - } - - if series := p.Series(seriesID); series != nil { - str := fmt.Sprintf("series #%d already exists", seriesID) - return newError(ErrSeriesAlreadyExists, str, nil) - } - - if seriesID != 1 { - if _, ok := p.seriesLookup[seriesID-1]; !ok { - str := fmt.Sprintf("series #%d cannot be created because series #%d does not exist", - seriesID, seriesID-1) - return newError(ErrSeriesIDNotSequential, str, nil) - } - } - - return p.putSeries(ns, version, seriesID, reqSigs, rawPubKeys) -} - -// ActivateSeries marks the series with the given ID as active. -func (p *Pool) ActivateSeries(ns walletdb.ReadWriteBucket, seriesID uint32) error { - series := p.Series(seriesID) - if series == nil { - str := fmt.Sprintf("series #%d does not exist, cannot activate it", seriesID) - return newError(ErrSeriesNotExists, str, nil) - } - series.active = true - err := p.saveSeriesToDisk(ns, seriesID, series) - if err != nil { - return err - } - p.seriesLookup[seriesID] = series - return nil -} - -// ReplaceSeries will replace an already existing series. -// -// - rawPubKeys has to contain three or more public keys -// - reqSigs has to be less or equal than the number of public keys in rawPubKeys. -func (p *Pool) ReplaceSeries(ns walletdb.ReadWriteBucket, version, seriesID, reqSigs uint32, rawPubKeys []string) error { - series := p.Series(seriesID) - if series == nil { - str := fmt.Sprintf("series #%d does not exist, cannot replace it", seriesID) - return newError(ErrSeriesNotExists, str, nil) - } - - if series.IsEmpowered() { - str := fmt.Sprintf("series #%d has private keys and cannot be replaced", seriesID) - return newError(ErrSeriesAlreadyEmpowered, str, nil) - } - - return p.putSeries(ns, version, seriesID, reqSigs, rawPubKeys) -} - -// decryptExtendedKey uses Manager.Decrypt() to decrypt the encrypted byte slice and return -// an extended (public or private) key representing it. -// -// This method must be called with the Pool's manager unlocked. -func (p *Pool) decryptExtendedKey(keyType waddrmgr.CryptoKeyType, encrypted []byte) (*hdkeychain.ExtendedKey, error) { - decrypted, err := p.manager.Decrypt(keyType, encrypted) - if err != nil { - str := fmt.Sprintf("cannot decrypt key %v", encrypted) - return nil, newError(ErrCrypto, str, err) - } - result, err := hdkeychain.NewKeyFromString(string(decrypted)) - zero.Bytes(decrypted) - if err != nil { - str := fmt.Sprintf("cannot get key from string %v", decrypted) - return nil, newError(ErrKeyChain, str, err) - } - return result, nil -} - -// validateAndDecryptSeriesKeys checks that the length of the public and private key -// slices is the same, decrypts them, ensures the non-nil private keys have a matching -// public key and returns them. -// -// This function must be called with the Pool's manager unlocked. -func validateAndDecryptKeys(rawPubKeys, rawPrivKeys [][]byte, p *Pool) (pubKeys, privKeys []*hdkeychain.ExtendedKey, err error) { - pubKeys = make([]*hdkeychain.ExtendedKey, len(rawPubKeys)) - privKeys = make([]*hdkeychain.ExtendedKey, len(rawPrivKeys)) - if len(pubKeys) != len(privKeys) { - return nil, nil, newError(ErrKeysPrivatePublicMismatch, - "the pub key and priv key arrays should have the same number of elements", - nil) - } - - for i, encryptedPub := range rawPubKeys { - pubKey, err := p.decryptExtendedKey(waddrmgr.CKTPublic, encryptedPub) - if err != nil { - return nil, nil, err - } - pubKeys[i] = pubKey - - encryptedPriv := rawPrivKeys[i] - var privKey *hdkeychain.ExtendedKey - if encryptedPriv == nil { - privKey = nil - } else { - privKey, err = p.decryptExtendedKey(waddrmgr.CKTPrivate, encryptedPriv) - if err != nil { - return nil, nil, err - } - } - privKeys[i] = privKey - - if privKey != nil { - checkPubKey, err := privKey.Neuter() - if err != nil { - str := fmt.Sprintf("cannot neuter key %v", privKey) - return nil, nil, newError(ErrKeyNeuter, str, err) - } - if pubKey.String() != checkPubKey.String() { - str := fmt.Sprintf("public key %v different than expected %v", - pubKey, checkPubKey) - return nil, nil, newError(ErrKeyMismatch, str, nil) - } - } - } - return pubKeys, privKeys, nil -} - -// LoadAllSeries fetches all series (decrypting their public and private -// extended keys) for this Pool from the database and populates the -// seriesLookup map with them. If there are any private extended keys for -// a series, it will also ensure they have a matching extended public key -// in that series. -// -// This method must be called with the Pool's manager unlocked. -// FIXME: We should be able to get rid of this (and loadAllSeries/seriesLookup) -// by making Series() load the series data directly from the DB. -func (p *Pool) LoadAllSeries(ns walletdb.ReadBucket) error { - series, err := loadAllSeries(ns, p.ID) - if err != nil { - return err - } - for id, series := range series { - pubKeys, privKeys, err := validateAndDecryptKeys( - series.pubKeysEncrypted, series.privKeysEncrypted, p) - if err != nil { - return err - } - p.seriesLookup[id] = &SeriesData{ - publicKeys: pubKeys, - privateKeys: privKeys, - reqSigs: series.reqSigs, - } - } - return nil -} - -// Change the order of the pubkeys based on branch number. -// Given the three pubkeys ABC, this would mean: -// - branch 0: CBA (reversed) -// - branch 1: ABC (first key priority) -// - branch 2: BAC (second key priority) -// - branch 3: CAB (third key priority) -func branchOrder(pks []*hdkeychain.ExtendedKey, branch Branch) ([]*hdkeychain.ExtendedKey, error) { - if pks == nil { - // This really shouldn't happen, but we want to be good citizens, so we - // return an error instead of crashing. - return nil, newError(ErrInvalidValue, "pks cannot be nil", nil) - } - - if branch > Branch(len(pks)) { - return nil, newError( - ErrInvalidBranch, "branch number is bigger than number of public keys", nil) - } - - if branch == 0 { - numKeys := len(pks) - res := make([]*hdkeychain.ExtendedKey, numKeys) - copy(res, pks) - // reverse pk - for i, j := 0, numKeys-1; i < j; i, j = i+1, j-1 { - res[i], res[j] = res[j], res[i] - } - return res, nil - } - - tmp := make([]*hdkeychain.ExtendedKey, len(pks)) - tmp[0] = pks[branch-1] - j := 1 - for i := 0; i < len(pks); i++ { - if i != int(branch-1) { - tmp[j] = pks[i] - j++ - } - } - return tmp, nil -} - -// DepositScriptAddress calls DepositScript to get a multi-signature -// redemption script and returns the pay-to-script-hash-address for that script. -func (p *Pool) DepositScriptAddress(seriesID uint32, branch Branch, index Index) (btcutil.Address, error) { - script, err := p.DepositScript(seriesID, branch, index) - if err != nil { - return nil, err - } - return p.addressFor(script) -} - -func (p *Pool) addressFor(script []byte) (btcutil.Address, error) { - scriptHash := btcutil.Hash160(script) - return btcutil.NewAddressScriptHashFromHash(scriptHash, p.manager.ChainParams()) -} - -// DepositScript constructs and returns a multi-signature redemption script where -// a certain number (Series.reqSigs) of the public keys belonging to the series -// with the given ID are required to sign the transaction for it to be successful. -func (p *Pool) DepositScript(seriesID uint32, branch Branch, index Index) ([]byte, error) { - series := p.Series(seriesID) - if series == nil { - str := fmt.Sprintf("series #%d does not exist", seriesID) - return nil, newError(ErrSeriesNotExists, str, nil) - } - - pubKeys, err := branchOrder(series.publicKeys, branch) - if err != nil { - return nil, err - } - - pks := make([]*btcutil.AddressPubKey, len(pubKeys)) - for i, key := range pubKeys { - child, err := key.Child(uint32(index)) - // TODO: implement getting the next index until we find a valid one, - // in case there is a hdkeychain.ErrInvalidChild. - if err != nil { - str := fmt.Sprintf("child #%d for this pubkey %d does not exist", index, i) - return nil, newError(ErrKeyChain, str, err) - } - pubkey, err := child.ECPubKey() - if err != nil { - str := fmt.Sprintf("child #%d for this pubkey %d does not exist", index, i) - return nil, newError(ErrKeyChain, str, err) - } - pks[i], err = btcutil.NewAddressPubKey(pubkey.SerializeCompressed(), - p.manager.ChainParams()) - if err != nil { - str := fmt.Sprintf( - "child #%d for this pubkey %d could not be converted to an address", - index, i) - return nil, newError(ErrKeyChain, str, err) - } - } - - script, err := txscript.MultiSigScript(pks, int(series.reqSigs)) - if err != nil { - str := fmt.Sprintf("error while making multisig script hash, %d", len(pks)) - return nil, newError(ErrScriptCreation, str, err) - } - - return script, nil -} - -// ChangeAddress returns a new votingpool address for the given seriesID and -// index, on the 0th branch (which is reserved for change addresses). The series -// with the given ID must be active. -func (p *Pool) ChangeAddress(seriesID uint32, index Index) (*ChangeAddress, error) { - series := p.Series(seriesID) - if series == nil { - return nil, newError(ErrSeriesNotExists, - fmt.Sprintf("series %d does not exist", seriesID), nil) - } - if !series.active { - str := fmt.Sprintf("ChangeAddress must be on active series; series #%d is not", seriesID) - return nil, newError(ErrSeriesNotActive, str, nil) - } - - script, err := p.DepositScript(seriesID, Branch(0), index) - if err != nil { - return nil, err - } - pAddr, err := p.poolAddress(seriesID, Branch(0), index, script) - if err != nil { - return nil, err - } - return &ChangeAddress{poolAddress: pAddr}, nil -} - -// WithdrawalAddress queries the address manager for the P2SH address -// of the redeem script generated with the given series/branch/index and uses -// that to populate the returned WithdrawalAddress. This is done because we -// should only withdraw from previously used addresses but also because when -// processing withdrawals we may iterate over a huge number of addresses and -// it'd be too expensive to re-generate the redeem script for all of them. -// This method must be called with the manager unlocked. -func (p *Pool) WithdrawalAddress(ns, addrmgrNs walletdb.ReadBucket, seriesID uint32, branch Branch, index Index) ( - *WithdrawalAddress, error) { - // TODO: Ensure the given series is hot. - addr, err := p.getUsedAddr(ns, addrmgrNs, seriesID, branch, index) - if err != nil { - return nil, err - } - if addr == nil { - str := fmt.Sprintf("cannot withdraw from unused addr (series: %d, branch: %d, index: %d)", - seriesID, branch, index) - return nil, newError(ErrWithdrawFromUnusedAddr, str, nil) - } - script, err := addr.Script() - if err != nil { - return nil, err - } - pAddr, err := p.poolAddress(seriesID, branch, index, script) - if err != nil { - return nil, err - } - return &WithdrawalAddress{poolAddress: pAddr}, nil -} - -func (p *Pool) poolAddress(seriesID uint32, branch Branch, index Index, script []byte) ( - *poolAddress, error) { - addr, err := p.addressFor(script) - if err != nil { - return nil, err - } - return &poolAddress{ - pool: p, seriesID: seriesID, branch: branch, index: index, addr: addr, - script: script}, - nil -} - -// EmpowerSeries adds the given extended private key (in raw format) to the -// series with the given ID, thus allowing it to sign deposit/withdrawal -// scripts. The series with the given ID must exist, the key must be a valid -// private extended key and must match one of the series' extended public keys. -// -// This method must be called with the Pool's manager unlocked. -func (p *Pool) EmpowerSeries(ns walletdb.ReadWriteBucket, seriesID uint32, rawPrivKey string) error { - // make sure this series exists - series := p.Series(seriesID) - if series == nil { - str := fmt.Sprintf("series %d does not exist for this voting pool", - seriesID) - return newError(ErrSeriesNotExists, str, nil) - } - - // Check that the private key is valid. - privKey, err := hdkeychain.NewKeyFromString(rawPrivKey) - if err != nil { - str := fmt.Sprintf("invalid extended private key %v", rawPrivKey) - return newError(ErrKeyChain, str, err) - } - if !privKey.IsPrivate() { - str := fmt.Sprintf( - "to empower a series you need the extended private key, not an extended public key %v", - privKey) - return newError(ErrKeyIsPublic, str, err) - } - - pubKey, err := privKey.Neuter() - if err != nil { - str := fmt.Sprintf("invalid extended private key %v, can't convert to public key", - rawPrivKey) - return newError(ErrKeyNeuter, str, err) - } - - lookingFor := pubKey.String() - found := false - - // Make sure the private key has the corresponding public key in the series, - // to be able to empower it. - for i, publicKey := range series.publicKeys { - if publicKey.String() == lookingFor { - found = true - series.privateKeys[i] = privKey - } - } - - if !found { - str := fmt.Sprintf( - "private Key does not have a corresponding public key in this series") - return newError(ErrKeysPrivatePublicMismatch, str, nil) - } - - if err = p.saveSeriesToDisk(ns, seriesID, series); err != nil { - return err - } - - return nil -} - -// EnsureUsedAddr ensures we have entries in our used addresses DB for the given -// seriesID, branch and all indices up to the given one. It must be called with -// the manager unlocked. -func (p *Pool) EnsureUsedAddr(ns, addrmgrNs walletdb.ReadWriteBucket, seriesID uint32, branch Branch, index Index) error { - lastIdx, err := p.highestUsedIndexFor(ns, seriesID, branch) - if err != nil { - return err - } - if lastIdx == 0 { - // highestUsedIndexFor() returns 0 when there are no used addresses for a - // given seriesID/branch, so we do this to ensure there is an entry with - // index==0. - if err := p.addUsedAddr(ns, addrmgrNs, seriesID, branch, lastIdx); err != nil { - return err - } - } - lastIdx++ - for lastIdx <= index { - if err := p.addUsedAddr(ns, addrmgrNs, seriesID, branch, lastIdx); err != nil { - return err - } - lastIdx++ - } - return nil -} - -// addUsedAddr creates a deposit script for the given seriesID/branch/index, -// ensures it is imported into the address manager and finaly adds the script -// hash to our used addresses DB. It must be called with the manager unlocked. -func (p *Pool) addUsedAddr(ns, addrmgrNs walletdb.ReadWriteBucket, seriesID uint32, branch Branch, index Index) error { - script, err := p.DepositScript(seriesID, branch, index) - if err != nil { - return err - } - - // First ensure the address manager has our script. That way there's no way - // to have it in the used addresses DB but not in the address manager. - // TODO: Decide how far back we want the addr manager to rescan and set the - // BlockStamp height according to that. - manager, err := p.manager.FetchScopedKeyManager(waddrmgr.KeyScopeBIP0044) - if err != nil { - return err - } - _, err = manager.ImportScript(addrmgrNs, script, &waddrmgr.BlockStamp{}) - if err != nil && err.(waddrmgr.ManagerError).ErrorCode != waddrmgr.ErrDuplicateAddress { - return err - } - - encryptedHash, err := p.manager.Encrypt(waddrmgr.CKTPublic, btcutil.Hash160(script)) - if err != nil { - return newError(ErrCrypto, "failed to encrypt script hash", err) - } - err = putUsedAddrHash(ns, p.ID, seriesID, branch, index, encryptedHash) - if err != nil { - return newError(ErrDatabase, "failed to store used addr script hash", err) - } - - return nil -} - -// getUsedAddr gets the script hash for the given series, branch and index from -// the used addresses DB and uses that to look up the ManagedScriptAddress -// from the address manager. It must be called with the manager unlocked. -func (p *Pool) getUsedAddr(ns, addrmgrNs walletdb.ReadBucket, seriesID uint32, branch Branch, index Index) ( - waddrmgr.ManagedScriptAddress, error) { - - mgr := p.manager - encryptedHash := getUsedAddrHash(ns, p.ID, seriesID, branch, index) - if encryptedHash == nil { - return nil, nil - } - hash, err := p.manager.Decrypt(waddrmgr.CKTPublic, encryptedHash) - if err != nil { - return nil, newError(ErrCrypto, "failed to decrypt stored script hash", err) - } - addr, err := btcutil.NewAddressScriptHashFromHash(hash, mgr.ChainParams()) - if err != nil { - return nil, newError(ErrInvalidScriptHash, "failed to parse script hash", err) - } - mAddr, err := mgr.Address(addrmgrNs, addr) - if err != nil { - return nil, err - } - return mAddr.(waddrmgr.ManagedScriptAddress), nil -} - -// highestUsedIndexFor returns the highest index from this Pool's used addresses -// with the given seriesID and branch. It returns 0 if there are no used -// addresses with the given seriesID and branch. -func (p *Pool) highestUsedIndexFor(ns walletdb.ReadBucket, seriesID uint32, branch Branch) (Index, error) { - return getMaxUsedIdx(ns, p.ID, seriesID, branch) -} - -// String returns a string encoding of the underlying bitcoin payment address. -func (a *poolAddress) String() string { - return a.addr.EncodeAddress() -} - -func (a *poolAddress) addrIdentifier() string { - return fmt.Sprintf("PoolAddress seriesID:%d, branch:%d, index:%d", a.seriesID, a.branch, - a.index) -} - -func (a *poolAddress) redeemScript() []byte { - return a.script -} - -func (a *poolAddress) series() *SeriesData { - return a.pool.Series(a.seriesID) -} - -func (a *poolAddress) SeriesID() uint32 { - return a.seriesID -} - -func (a *poolAddress) Branch() Branch { - return a.branch -} - -func (a *poolAddress) Index() Index { - return a.index -} - -// IsEmpowered returns true if this series is empowered (i.e. if it has -// at least one private key loaded). -func (s *SeriesData) IsEmpowered() bool { - for _, key := range s.privateKeys { - if key != nil { - return true - } - } - return false -} - -func (s *SeriesData) getPrivKeyFor(pubKey *hdkeychain.ExtendedKey) (*hdkeychain.ExtendedKey, error) { - for i, key := range s.publicKeys { - if key.String() == pubKey.String() { - return s.privateKeys[i], nil - } - } - return nil, newError(ErrUnknownPubKey, fmt.Sprintf("unknown public key '%s'", - pubKey.String()), nil) -} diff --git a/votingpool/pool_test.go b/votingpool/pool_test.go deleted file mode 100644 index 4269ea5..0000000 --- a/votingpool/pool_test.go +++ /dev/null @@ -1,1104 +0,0 @@ -// Copyright (c) 2014 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package votingpool_test - -import ( - "bytes" - "encoding/hex" - "fmt" - "reflect" - "testing" - - "github.com/btcsuite/btcutil/hdkeychain" - vp "github.com/btcsuite/btcwallet/votingpool" - "github.com/btcsuite/btcwallet/waddrmgr" - "github.com/btcsuite/btcwallet/walletdb" - _ "github.com/btcsuite/btcwallet/walletdb/bdb" -) - -func TestLoadPoolAndDepositScript(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - // setup - poolID := "test" - pubKeys := vp.TstPubKeys[0:3] - err = vp.LoadAndCreateSeries(ns, pool.Manager(), 1, poolID, 1, 2, pubKeys) - if err != nil { - t.Fatalf("Failed to create voting pool and series: %v", err) - } - - // execute - script, err := vp.LoadAndGetDepositScript(ns, pool.Manager(), poolID, 1, 0, 0) - if err != nil { - t.Fatalf("Failed to get deposit script: %v", err) - } - - // validate - strScript := hex.EncodeToString(script) - want := "5221035e94da75731a2153b20909017f62fcd49474c45f3b46282c0dafa8b40a3a312b2102e983a53dd20b7746dd100dfd2925b777436fc1ab1dd319433798924a5ce143e32102908d52a548ee9ef6b2d0ea67a3781a0381bc3570ad623564451e63757ff9393253ae" - if want != strScript { - t.Fatalf("Failed to get the right deposit script. Got %v, want %v", - strScript, want) - } -} - -func TestLoadPoolAndCreateSeries(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - poolID := "test" - - // first time, the voting pool is created - pubKeys := vp.TstPubKeys[0:3] - err = vp.LoadAndCreateSeries(ns, pool.Manager(), 1, poolID, 1, 2, pubKeys) - if err != nil { - t.Fatalf("Creating voting pool and Creating series failed: %v", err) - } - - // create another series where the voting pool is loaded this time - pubKeys = vp.TstPubKeys[3:6] - err = vp.LoadAndCreateSeries(ns, pool.Manager(), 1, poolID, 2, 2, pubKeys) - - if err != nil { - t.Fatalf("Loading voting pool and Creating series failed: %v", err) - } -} - -func TestLoadPoolAndReplaceSeries(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - // setup - poolID := "test" - pubKeys := vp.TstPubKeys[0:3] - err = vp.LoadAndCreateSeries(ns, pool.Manager(), 1, poolID, 1, 2, pubKeys) - if err != nil { - t.Fatalf("Failed to create voting pool and series: %v", err) - } - - pubKeys = vp.TstPubKeys[3:6] - err = vp.LoadAndReplaceSeries(ns, pool.Manager(), 1, poolID, 1, 2, pubKeys) - if err != nil { - t.Fatalf("Failed to replace series: %v", err) - } -} - -func TestLoadPoolAndEmpowerSeries(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, addrmgrNs := vp.TstRWNamespaces(dbtx) - - // setup - poolID := "test" - pubKeys := vp.TstPubKeys[0:3] - err = vp.LoadAndCreateSeries(ns, pool.Manager(), 1, poolID, 1, 2, pubKeys) - if err != nil { - t.Fatalf("Creating voting pool and Creating series failed: %v", err) - } - - vp.TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - err = vp.LoadAndEmpowerSeries(ns, pool.Manager(), poolID, 1, vp.TstPrivKeys[0]) - }) - if err != nil { - t.Fatalf("Load voting pool and Empower series failed: %v", err) - } -} - -func TestDepositScriptAddress(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - tests := []struct { - version uint32 - series uint32 - reqSigs uint32 - pubKeys []string - // map of branch:address (we only check the branch index at 0) - addresses map[uint32]string - }{ - { - version: 1, - series: 1, - reqSigs: 2, - pubKeys: vp.TstPubKeys[0:3], - addresses: map[uint32]string{ - 0: "3Hb4xcebcKg4DiETJfwjh8sF4uDw9rqtVC", - 1: "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", - 2: "3Qt1EaKRD9g9FeL2DGkLLswhK1AKmmXFSe", - 3: "3PbExiaztsSYgh6zeMswC49hLUwhTQ86XG", - }, - }, - } - - for i, test := range tests { - if err := pool.CreateSeries(ns, test.version, test.series, - test.reqSigs, test.pubKeys); err != nil { - t.Fatalf("Cannot creates series %v", test.series) - } - for branch, expectedAddress := range test.addresses { - addr, err := pool.DepositScriptAddress(test.series, vp.Branch(branch), vp.Index(0)) - if err != nil { - t.Fatalf("Failed to get DepositScriptAddress #%d: %v", i, err) - } - address := addr.EncodeAddress() - if expectedAddress != address { - t.Errorf("DepositScript #%d returned the wrong deposit script. Got %v, want %v", - i, address, expectedAddress) - } - } - } -} - -func TestDepositScriptAddressForNonExistentSeries(t *testing.T) { - tearDown, _, pool := vp.TstCreatePool(t) - defer tearDown() - - _, err := pool.DepositScriptAddress(1, 0, 0) - - vp.TstCheckError(t, "", err, vp.ErrSeriesNotExists) -} - -func TestDepositScriptAddressForHardenedPubKey(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - if err := pool.CreateSeries(ns, 1, 1, 2, vp.TstPubKeys[0:3]); err != nil { - t.Fatalf("Cannot creates series") - } - - // Ask for a DepositScriptAddress using an index for a hardened child, which should - // fail as we use the extended public keys to derive childs. - _, err = pool.DepositScriptAddress(1, 0, vp.Index(hdkeychain.HardenedKeyStart+1)) - - vp.TstCheckError(t, "", err, vp.ErrKeyChain) -} - -func TestLoadPool(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - pool2, err := vp.Load(ns, pool.Manager(), pool.ID) - if err != nil { - t.Errorf("Error loading Pool: %v", err) - } - if !bytes.Equal(pool2.ID, pool.ID) { - t.Errorf("Voting pool obtained from DB does not match the created one") - } -} - -func TestCreatePool(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - pool2, err := vp.Create(ns, pool.Manager(), []byte{0x02}) - if err != nil { - t.Errorf("Error creating Pool: %v", err) - } - if !bytes.Equal(pool2.ID, []byte{0x02}) { - t.Errorf("Pool ID mismatch: got %v, want %v", pool2.ID, []byte{0x02}) - } -} - -func TestCreatePoolWhenAlreadyExists(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - _, err = vp.Create(ns, pool.Manager(), pool.ID) - - vp.TstCheckError(t, "", err, vp.ErrPoolAlreadyExists) -} - -func TestCreateSeries(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - tests := []struct { - version uint32 - series uint32 - reqSigs uint32 - pubKeys []string - }{ - { - version: 1, - series: 1, - reqSigs: 2, - pubKeys: vp.TstPubKeys[0:3], - }, - { - version: 1, - series: 2, - reqSigs: 3, - pubKeys: vp.TstPubKeys[0:5], - }, - { - version: 1, - series: 3, - reqSigs: 4, - pubKeys: vp.TstPubKeys[0:7], - }, - { - version: 1, - series: 4, - reqSigs: 5, - pubKeys: vp.TstPubKeys[0:9], - }, - } - - for testNum, test := range tests { - err := pool.CreateSeries(ns, test.version, test.series, test.reqSigs, test.pubKeys[:]) - if err != nil { - t.Fatalf("%d: Cannot create series %d", testNum, test.series) - } - exists, err := pool.TstExistsSeries(dbtx, test.series) - if err != nil { - t.Fatal(err) - } - if !exists { - t.Errorf("%d: Series %d not in database", testNum, test.series) - } - } -} - -func TestPoolCreateSeriesInvalidID(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - err = pool.CreateSeries(ns, vp.CurrentVersion, 0, 1, vp.TstPubKeys[0:3]) - - vp.TstCheckError(t, "", err, vp.ErrSeriesIDInvalid) -} - -func TestPoolCreateSeriesWhenAlreadyExists(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - pubKeys := vp.TstPubKeys[0:3] - if err := pool.CreateSeries(ns, 1, 1, 1, pubKeys); err != nil { - t.Fatalf("Cannot create series: %v", err) - } - - err = pool.CreateSeries(ns, 1, 1, 1, pubKeys) - - vp.TstCheckError(t, "", err, vp.ErrSeriesAlreadyExists) -} - -func TestPoolCreateSeriesIDNotSequential(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - pubKeys := vp.TstPubKeys[0:4] - if err := pool.CreateSeries(ns, 1, 1, 2, pubKeys); err != nil { - t.Fatalf("Cannot create series: %v", err) - } - - err = pool.CreateSeries(ns, 1, 3, 2, pubKeys) - - vp.TstCheckError(t, "", err, vp.ErrSeriesIDNotSequential) -} - -func TestPutSeriesErrors(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - tests := []struct { - version uint32 - reqSigs uint32 - pubKeys []string - err vp.ErrorCode - msg string - }{ - { - pubKeys: vp.TstPubKeys[0:1], - err: vp.ErrTooFewPublicKeys, - msg: "Should return error when passed too few pubkeys", - }, - { - reqSigs: 5, - pubKeys: vp.TstPubKeys[0:3], - err: vp.ErrTooManyReqSignatures, - msg: "Should return error when reqSigs > len(pubKeys)", - }, - { - pubKeys: []string{vp.TstPubKeys[0], vp.TstPubKeys[1], vp.TstPubKeys[2], vp.TstPubKeys[0]}, - err: vp.ErrKeyDuplicate, - msg: "Should return error when passed duplicate pubkeys", - }, - { - pubKeys: []string{"invalidxpub1", "invalidxpub2", "invalidxpub3"}, - err: vp.ErrKeyChain, - msg: "Should return error when passed invalid pubkey", - }, - { - pubKeys: vp.TstPrivKeys[0:3], - err: vp.ErrKeyIsPrivate, - msg: "Should return error when passed private keys", - }, - } - - for i, test := range tests { - err := pool.TstPutSeries(ns, test.version, uint32(i+1), test.reqSigs, test.pubKeys) - vp.TstCheckError(t, fmt.Sprintf("Create series #%d", i), err, test.err) - } -} - -func TestCannotReplaceEmpoweredSeries(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, addrmgrNs := vp.TstRWNamespaces(dbtx) - - seriesID := uint32(1) - - if err := pool.CreateSeries(ns, 1, seriesID, 3, vp.TstPubKeys[0:4]); err != nil { - t.Fatalf("Failed to create series: %v", err) - } - - vp.TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - if err := pool.EmpowerSeries(ns, seriesID, vp.TstPrivKeys[1]); err != nil { - t.Fatalf("Failed to empower series: %v", err) - } - }) - - err = pool.ReplaceSeries(ns, 1, seriesID, 2, []string{vp.TstPubKeys[0], vp.TstPubKeys[2], - vp.TstPubKeys[3]}) - - vp.TstCheckError(t, "", err, vp.ErrSeriesAlreadyEmpowered) -} - -func TestReplaceNonExistingSeries(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - pubKeys := vp.TstPubKeys[0:3] - - err = pool.ReplaceSeries(ns, 1, 1, 3, pubKeys) - - vp.TstCheckError(t, "", err, vp.ErrSeriesNotExists) -} - -type replaceSeriesTestEntry struct { - testID int - orig seriesRaw - replaceWith seriesRaw -} - -var replaceSeriesTestData = []replaceSeriesTestEntry{ - { - testID: 0, - orig: seriesRaw{ - id: 1, - version: 1, - reqSigs: 2, - pubKeys: vp.CanonicalKeyOrder([]string{vp.TstPubKeys[0], vp.TstPubKeys[1], - vp.TstPubKeys[2], vp.TstPubKeys[4]}), - }, - replaceWith: seriesRaw{ - id: 1, - version: 1, - reqSigs: 1, - pubKeys: vp.CanonicalKeyOrder(vp.TstPubKeys[3:6]), - }, - }, - { - testID: 1, - orig: seriesRaw{ - id: 2, - version: 1, - reqSigs: 2, - pubKeys: vp.CanonicalKeyOrder(vp.TstPubKeys[0:3]), - }, - replaceWith: seriesRaw{ - id: 2, - version: 1, - reqSigs: 2, - pubKeys: vp.CanonicalKeyOrder(vp.TstPubKeys[3:7]), - }, - }, - { - testID: 2, - orig: seriesRaw{ - id: 3, - version: 1, - reqSigs: 8, - pubKeys: vp.CanonicalKeyOrder(vp.TstPubKeys[0:9]), - }, - replaceWith: seriesRaw{ - id: 3, - version: 1, - reqSigs: 7, - pubKeys: vp.CanonicalKeyOrder(vp.TstPubKeys[0:8]), - }, - }, -} - -func TestReplaceExistingSeries(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - for _, data := range replaceSeriesTestData { - seriesID := data.orig.id - testID := data.testID - - if err := pool.CreateSeries(ns, data.orig.version, seriesID, data.orig.reqSigs, data.orig.pubKeys); err != nil { - t.Fatalf("Test #%d: failed to create series in replace series setup: %v", - testID, err) - } - - if err := pool.ReplaceSeries(ns, data.replaceWith.version, seriesID, - data.replaceWith.reqSigs, data.replaceWith.pubKeys); err != nil { - t.Errorf("Test #%d: replaceSeries failed: %v", testID, err) - } - - validateReplaceSeries(t, pool, testID, data.replaceWith) - } -} - -// validateReplaceSeries validate the created series stored in the system -// corresponds to the series we replaced the original with. -func validateReplaceSeries(t *testing.T, pool *vp.Pool, testID int, replacedWith seriesRaw) { - seriesID := replacedWith.id - series := pool.Series(seriesID) - if series == nil { - t.Fatalf("Test #%d Series #%d: series not found", testID, seriesID) - } - - pubKeys := series.TstGetRawPublicKeys() - // Check that the public keys match what we expect. - if !reflect.DeepEqual(replacedWith.pubKeys, pubKeys) { - t.Errorf("Test #%d, series #%d: pubkeys mismatch. Got %v, want %v", - testID, seriesID, pubKeys, replacedWith.pubKeys) - } - - // Check number of required sigs. - if replacedWith.reqSigs != series.TstGetReqSigs() { - t.Errorf("Test #%d, series #%d: required signatures mismatch. Got %d, want %d", - testID, seriesID, series.TstGetReqSigs(), replacedWith.reqSigs) - } - - // Check that the series is not empowered. - if series.IsEmpowered() { - t.Errorf("Test #%d, series #%d: series is empowered but should not be", - testID, seriesID) - } -} - -func TestEmpowerSeries(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, addrmgrNs := vp.TstRWNamespaces(dbtx) - - seriesID := uint32(1) - if err := pool.CreateSeries(ns, 1, seriesID, 2, vp.TstPubKeys[0:3]); err != nil { - t.Fatalf("Failed to create series: %v", err) - } - - vp.TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - if err := pool.EmpowerSeries(ns, seriesID, vp.TstPrivKeys[0]); err != nil { - t.Errorf("Failed to empower series: %v", err) - } - }) -} - -func TestEmpowerSeriesErrors(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - seriesID := uint32(1) - if err := pool.CreateSeries(ns, 1, seriesID, 2, vp.TstPubKeys[0:3]); err != nil { - t.Fatalf("Failed to create series: %v", err) - } - - tests := []struct { - seriesID uint32 - key string - err vp.ErrorCode - }{ - { - seriesID: 2, - key: vp.TstPrivKeys[0], - // Invalid series. - err: vp.ErrSeriesNotExists, - }, - { - seriesID: seriesID, - key: "NONSENSE", - // Invalid private key. - err: vp.ErrKeyChain, - }, - { - seriesID: seriesID, - key: vp.TstPubKeys[5], - // Wrong type of key. - err: vp.ErrKeyIsPublic, - }, - { - seriesID: seriesID, - key: vp.TstPrivKeys[5], - // Key not corresponding to public key. - err: vp.ErrKeysPrivatePublicMismatch, - }, - } - - for i, test := range tests { - err := pool.EmpowerSeries(ns, test.seriesID, test.key) - vp.TstCheckError(t, fmt.Sprintf("EmpowerSeries #%d", i), err, test.err) - } - -} - -func TestPoolSeries(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - expectedPubKeys := vp.CanonicalKeyOrder(vp.TstPubKeys[0:3]) - if err := pool.CreateSeries(ns, vp.CurrentVersion, 1, 2, expectedPubKeys); err != nil { - t.Fatalf("Failed to create series: %v", err) - } - - series := pool.Series(1) - - if series == nil { - t.Fatal("Series() returned nil") - } - pubKeys := series.TstGetRawPublicKeys() - if !reflect.DeepEqual(pubKeys, expectedPubKeys) { - t.Errorf("Series pubKeys mismatch. Got %v, want %v", pubKeys, expectedPubKeys) - } -} - -type seriesRaw struct { - id uint32 - version uint32 - reqSigs uint32 - pubKeys []string - privKeys []string -} - -type testLoadAllSeriesTest struct { - id int - series []seriesRaw -} - -var testLoadAllSeriesTests = []testLoadAllSeriesTest{ - { - id: 1, - series: []seriesRaw{ - { - id: 1, - version: 1, - reqSigs: 2, - pubKeys: vp.TstPubKeys[0:3], - }, - { - id: 2, - version: 1, - reqSigs: 2, - pubKeys: vp.TstPubKeys[3:6], - privKeys: vp.TstPrivKeys[4:5], - }, - { - id: 3, - version: 1, - reqSigs: 3, - pubKeys: vp.TstPubKeys[0:5], - privKeys: []string{vp.TstPrivKeys[0], vp.TstPrivKeys[2]}, - }, - }, - }, - { - id: 2, - series: []seriesRaw{ - { - id: 1, - version: 1, - reqSigs: 2, - pubKeys: vp.TstPubKeys[0:3], - }, - }, - }, -} - -func setUpLoadAllSeries(t *testing.T, dbtx walletdb.ReadWriteTx, mgr *waddrmgr.Manager, - test testLoadAllSeriesTest) *vp.Pool { - ns, addrmgrNs := vp.TstRWNamespaces(dbtx) - pool, err := vp.Create(ns, mgr, []byte{byte(test.id + 1)}) - if err != nil { - t.Fatalf("Voting Pool creation failed: %v", err) - } - - for _, series := range test.series { - err := pool.CreateSeries(ns, series.version, series.id, - series.reqSigs, series.pubKeys) - if err != nil { - t.Fatalf("Test #%d Series #%d: failed to create series: %v", - test.id, series.id, err) - } - - for _, privKey := range series.privKeys { - vp.TstRunWithManagerUnlocked(t, mgr, addrmgrNs, func() { - if err := pool.EmpowerSeries(ns, series.id, privKey); err != nil { - t.Fatalf("Test #%d Series #%d: empower with privKey %v failed: %v", - test.id, series.id, privKey, err) - } - }) - } - } - return pool -} - -func TestLoadAllSeries(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, addrmgrNs := vp.TstRWNamespaces(dbtx) - - for _, test := range testLoadAllSeriesTests { - pool := setUpLoadAllSeries(t, dbtx, pool.Manager(), test) - pool.TstEmptySeriesLookup() - vp.TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - if err := pool.LoadAllSeries(ns); err != nil { - t.Fatalf("Test #%d: failed to load voting pool: %v", test.id, err) - } - }) - for _, seriesData := range test.series { - validateLoadAllSeries(t, pool, test.id, seriesData) - } - } -} - -func validateLoadAllSeries(t *testing.T, pool *vp.Pool, testID int, seriesData seriesRaw) { - series := pool.Series(seriesData.id) - - // Check that the series exists. - if series == nil { - t.Errorf("Test #%d, series #%d: series not found", testID, seriesData.id) - } - - // Check that reqSigs is what we inserted. - if seriesData.reqSigs != series.TstGetReqSigs() { - t.Errorf("Test #%d, series #%d: required sigs are different. Got %d, want %d", - testID, seriesData.id, series.TstGetReqSigs(), seriesData.reqSigs) - } - - // Check that pubkeys and privkeys have the same length. - publicKeys := series.TstGetRawPublicKeys() - privateKeys := series.TstGetRawPrivateKeys() - if len(privateKeys) != len(publicKeys) { - t.Errorf("Test #%d, series #%d: wrong number of private keys. Got %d, want %d", - testID, seriesData.id, len(privateKeys), len(publicKeys)) - } - - sortedKeys := vp.CanonicalKeyOrder(seriesData.pubKeys) - if !reflect.DeepEqual(publicKeys, sortedKeys) { - t.Errorf("Test #%d, series #%d: public keys mismatch. Got %v, want %v", - testID, seriesData.id, sortedKeys, publicKeys) - } - - // Check that privkeys are what we inserted (length and content). - foundPrivKeys := make([]string, 0, len(seriesData.pubKeys)) - for _, privateKey := range privateKeys { - if privateKey != "" { - foundPrivKeys = append(foundPrivKeys, privateKey) - } - } - foundPrivKeys = vp.CanonicalKeyOrder(foundPrivKeys) - privKeys := vp.CanonicalKeyOrder(seriesData.privKeys) - if !reflect.DeepEqual(privKeys, foundPrivKeys) { - t.Errorf("Test #%d, series #%d: private keys mismatch. Got %v, want %v", - testID, seriesData.id, foundPrivKeys, privKeys) - } -} - -func reverse(inKeys []*hdkeychain.ExtendedKey) []*hdkeychain.ExtendedKey { - revKeys := make([]*hdkeychain.ExtendedKey, len(inKeys)) - max := len(inKeys) - for i := range inKeys { - revKeys[i] = inKeys[max-i-1] - } - return revKeys -} - -func TestBranchOrderZero(t *testing.T) { - // test change address branch (0) for 0-10 keys - for i := 0; i < 10; i++ { - inKeys := createTestPubKeys(t, i, 0) - wantKeys := reverse(inKeys) - resKeys, err := vp.TstBranchOrder(inKeys, 0) - if err != nil { - t.Fatalf("Error ordering keys: %v", err) - } - - if len(resKeys) != len(wantKeys) { - t.Errorf("BranchOrder: wrong no. of keys. Got: %d, want %d", - len(resKeys), len(inKeys)) - return - } - - for keyIdx := 0; i < len(inKeys); i++ { - if resKeys[keyIdx] != wantKeys[keyIdx] { - t.Errorf("BranchOrder(keys, 0): got %v, want %v", - resKeys[i], wantKeys[i]) - } - } - } -} - -func TestBranchOrderNonZero(t *testing.T) { - maxBranch := 5 - maxTail := 4 - // Test branch reordering for branch no. > 0. We test all branch values - // within [1, 5] in a slice of up to 9 (maxBranch-1 + branch-pivot + - // maxTail) keys. Hopefully that covers all combinations and edge-cases. - // We test the case where branch no. is 0 elsewhere. - for branch := 1; branch <= maxBranch; branch++ { - for j := 0; j <= maxTail; j++ { - first := createTestPubKeys(t, branch-1, 0) - pivot := createTestPubKeys(t, 1, branch) - last := createTestPubKeys(t, j, branch+1) - - inKeys := append(append(first, pivot...), last...) - wantKeys := append(append(pivot, first...), last...) - resKeys, err := vp.TstBranchOrder(inKeys, vp.Branch(branch)) - if err != nil { - t.Fatalf("Error ordering keys: %v", err) - } - - if len(resKeys) != len(inKeys) { - t.Errorf("BranchOrder: wrong no. of keys. Got: %d, want %d", - len(resKeys), len(inKeys)) - } - - for idx := 0; idx < len(inKeys); idx++ { - if resKeys[idx] != wantKeys[idx] { - o, w, g := branchErrorFormat(inKeys, wantKeys, resKeys) - t.Errorf("Branch: %d\nOrig: %v\nGot: %v\nWant: %v", branch, o, g, w) - } - } - } - } -} - -func TestBranchOrderNilKeys(t *testing.T) { - _, err := vp.TstBranchOrder(nil, 1) - - vp.TstCheckError(t, "", err, vp.ErrInvalidValue) -} - -func TestBranchOrderInvalidBranch(t *testing.T) { - _, err := vp.TstBranchOrder(createTestPubKeys(t, 3, 0), 4) - - vp.TstCheckError(t, "", err, vp.ErrInvalidBranch) -} - -func branchErrorFormat(orig, want, got []*hdkeychain.ExtendedKey) (origOrder, wantOrder, gotOrder []int) { - origOrder = []int{} - origMap := make(map[*hdkeychain.ExtendedKey]int) - for i, key := range orig { - origMap[key] = i + 1 - origOrder = append(origOrder, i+1) - } - - wantOrder = []int{} - for _, key := range want { - wantOrder = append(wantOrder, origMap[key]) - } - - gotOrder = []int{} - for _, key := range got { - gotOrder = append(gotOrder, origMap[key]) - } - - return origOrder, wantOrder, gotOrder -} - -func createTestPubKeys(t *testing.T, number, offset int) []*hdkeychain.ExtendedKey { - xpubRaw := "xpub661MyMwAqRbcFwdnYF5mvCBY54vaLdJf8c5ugJTp5p7PqF9J1USgBx12qYMnZ9yUiswV7smbQ1DSweMqu8wn7Jociz4PWkuJ6EPvoVEgMw7" - xpubKey, err := hdkeychain.NewKeyFromString(xpubRaw) - if err != nil { - t.Fatalf("Failed to generate new key: %v", err) - } - - keys := make([]*hdkeychain.ExtendedKey, number) - for i := uint32(0); i < uint32(len(keys)); i++ { - chPubKey, err := xpubKey.Child(i + uint32(offset)) - if err != nil { - t.Fatalf("Failed to generate child key: %v", err) - } - keys[i] = chPubKey - } - return keys -} - -func TestReverse(t *testing.T) { - // Test the utility function that reverses a list of public keys. - // 11 is arbitrary. - for numKeys := 0; numKeys < 11; numKeys++ { - keys := createTestPubKeys(t, numKeys, 0) - revRevKeys := reverse(reverse(keys)) - if len(keys) != len(revRevKeys) { - t.Errorf("Reverse(Reverse(x)): the no. pubkeys changed. Got %d, want %d", - len(revRevKeys), len(keys)) - } - - for i := 0; i < len(keys); i++ { - if keys[i] != revRevKeys[i] { - t.Errorf("Reverse(Reverse(x)) != x. Got %v, want %v", - revRevKeys[i], keys[i]) - } - } - } -} - -func TestEmpowerSeriesNeuterFailed(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, _ := vp.TstRWNamespaces(dbtx) - - seriesID := uint32(1) - err = pool.CreateSeries(ns, 1, seriesID, 2, vp.TstPubKeys[0:3]) - if err != nil { - t.Fatalf("Failed to create series: %v", err) - } - - // A private key with bad version (0xffffffff) will trigger an - // error in (k *ExtendedKey).Neuter and the associated error path - // in EmpowerSeries. - badKey := "wM5uZBNTYmaYGiK8VaGi7zPGbZGLuQgDiR2Zk4nGfbRFLXwHGcMUdVdazRpNHFSR7X7WLmzzbAq8dA1ViN6eWKgKqPye1rJTDQTvBiXvZ7E3nmdx" - err = pool.EmpowerSeries(ns, seriesID, badKey) - - vp.TstCheckError(t, "", err, vp.ErrKeyNeuter) -} - -func TestDecryptExtendedKeyCannotCreateResultKey(t *testing.T) { - tearDown, _, pool := vp.TstCreatePool(t) - defer tearDown() - - // the plaintext not being base58 encoded triggers the error - cipherText, err := pool.Manager().Encrypt(waddrmgr.CKTPublic, []byte("not-base58-encoded")) - if err != nil { - t.Fatalf("Failed to encrypt plaintext: %v", err) - } - - _, err = pool.TstDecryptExtendedKey(waddrmgr.CKTPublic, cipherText) - - vp.TstCheckError(t, "", err, vp.ErrKeyChain) -} - -func TestDecryptExtendedKeyCannotDecrypt(t *testing.T) { - tearDown, _, pool := vp.TstCreatePool(t) - defer tearDown() - - _, err := pool.TstDecryptExtendedKey(waddrmgr.CKTPublic, []byte{}) - - vp.TstCheckError(t, "", err, vp.ErrCrypto) -} - -func TestPoolChangeAddress(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - pubKeys := vp.TstPubKeys[1:4] - vp.TstCreateSeries(t, dbtx, pool, []vp.TstSeriesDef{{ReqSigs: 2, PubKeys: pubKeys, SeriesID: 1}}) - - addr := vp.TstNewChangeAddress(t, pool, 1, 0) - checkPoolAddress(t, addr, 1, 0, 0) - - // When the series is not active, we should get an error. - pubKeys = vp.TstPubKeys[3:6] - vp.TstCreateSeries(t, dbtx, pool, - []vp.TstSeriesDef{{ReqSigs: 2, PubKeys: pubKeys, SeriesID: 2, Inactive: true}}) - _, err = pool.ChangeAddress(2, 0) - vp.TstCheckError(t, "", err, vp.ErrSeriesNotActive) -} - -func TestPoolWithdrawalAddress(t *testing.T) { - tearDown, db, pool := vp.TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, addrmgrNs := vp.TstRWNamespaces(dbtx) - - pubKeys := vp.TstPubKeys[1:4] - vp.TstCreateSeries(t, dbtx, pool, []vp.TstSeriesDef{{ReqSigs: 2, PubKeys: pubKeys, SeriesID: 1}}) - addr := vp.TstNewWithdrawalAddress(t, dbtx, pool, 1, 0, 0) - checkPoolAddress(t, addr, 1, 0, 0) - - // When the requested address is not present in the set of used addresses - // for that Pool, we should get an error. - _, err = pool.WithdrawalAddress(ns, addrmgrNs, 1, 2, 3) - vp.TstCheckError(t, "", err, vp.ErrWithdrawFromUnusedAddr) -} - -func checkPoolAddress(t *testing.T, addr vp.PoolAddress, seriesID uint32, branch vp.Branch, - index vp.Index) { - - if addr.SeriesID() != seriesID { - t.Fatalf("Wrong SeriesID; got %d, want %d", addr.SeriesID(), seriesID) - } - if addr.Branch() != branch { - t.Fatalf("Wrong Branch; got %d, want %d", addr.Branch(), branch) - } - if addr.Index() != index { - t.Fatalf("Wrong Index; got %d, want %d", addr.Index(), index) - } -} diff --git a/votingpool/pool_wb_test.go b/votingpool/pool_wb_test.go deleted file mode 100644 index 1dd64a2..0000000 --- a/votingpool/pool_wb_test.go +++ /dev/null @@ -1,491 +0,0 @@ -// Copyright (c) 2015 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package votingpool - -import ( - "bytes" - "fmt" - "testing" - - "github.com/btcsuite/btcutil/hdkeychain" - "github.com/btcsuite/btcwallet/waddrmgr" -) - -func TestPoolEnsureUsedAddr(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, addrmgrNs := TstRWNamespaces(dbtx) - - var script []byte - var addr waddrmgr.ManagedScriptAddress - TstCreateSeries(t, dbtx, pool, []TstSeriesDef{{ReqSigs: 2, PubKeys: TstPubKeys[0:3], SeriesID: 1}}) - - idx := Index(0) - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - err = pool.EnsureUsedAddr(ns, addrmgrNs, 1, 0, idx) - }) - if err != nil { - t.Fatalf("Failed to ensure used addresses: %v", err) - } - addr, err = pool.getUsedAddr(ns, addrmgrNs, 1, 0, 0) - if err != nil { - t.Fatalf("Failed to get addr from used addresses set: %v", err) - } - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - script, err = addr.Script() - }) - if err != nil { - t.Fatalf("Failed to get script: %v", err) - } - wantScript, _ := pool.DepositScript(1, 0, 0) - if !bytes.Equal(script, wantScript) { - t.Fatalf("Script from looked up addr is not what we expect") - } - - idx = Index(3) - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - err = pool.EnsureUsedAddr(ns, addrmgrNs, 1, 0, idx) - }) - if err != nil { - t.Fatalf("Failed to ensure used addresses: %v", err) - } - for _, i := range []int{0, 1, 2, 3} { - addr, err = pool.getUsedAddr(ns, addrmgrNs, 1, 0, Index(i)) - if err != nil { - t.Fatalf("Failed to get addr from used addresses set: %v", err) - } - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - script, err = addr.Script() - }) - if err != nil { - t.Fatalf("Failed to get script: %v", err) - } - wantScript, _ := pool.DepositScript(1, 0, Index(i)) - if !bytes.Equal(script, wantScript) { - t.Fatalf("Script from looked up addr is not what we expect") - } - } -} - -func TestPoolGetUsedAddr(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, addrmgrNs := TstRWNamespaces(dbtx) - - TstCreateSeries(t, dbtx, pool, []TstSeriesDef{{ReqSigs: 2, PubKeys: TstPubKeys[0:3], SeriesID: 1}}) - - // Addr with series=1, branch=0, index=10 has never been used, so it should - // return nil. - addr, err := pool.getUsedAddr(ns, addrmgrNs, 1, 0, 10) - if err != nil { - t.Fatalf("Error when looking up used addr: %v", err) - } - if addr != nil { - t.Fatalf("Unused address found in used addresses DB: %v", addr) - } - - // Now we add that addr to the used addresses DB and check that the value - // returned by getUsedAddr() is what we expect. - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - err = pool.addUsedAddr(ns, addrmgrNs, 1, 0, 10) - }) - if err != nil { - t.Fatalf("Error when storing addr in used addresses DB: %v", err) - } - var script []byte - addr, err = pool.getUsedAddr(ns, addrmgrNs, 1, 0, 10) - if err != nil { - t.Fatalf("Error when looking up used addr: %v", err) - } - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - script, err = addr.Script() - }) - if err != nil { - t.Fatalf("Failed to get script: %v", err) - } - wantScript, _ := pool.DepositScript(1, 0, 10) - if !bytes.Equal(script, wantScript) { - t.Fatalf("Script from looked up addr is not what we expect") - } -} - -func TestSerializationErrors(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - _, addrmgrNs := TstRWNamespaces(dbtx) - - tests := []struct { - version uint32 - pubKeys []string - privKeys []string - reqSigs uint32 - err ErrorCode - }{ - { - version: 2, - pubKeys: TstPubKeys[0:3], - err: ErrSeriesVersion, - }, - { - pubKeys: []string{"NONSENSE"}, - // Not a valid length public key. - err: ErrSeriesSerialization, - }, - { - pubKeys: TstPubKeys[0:3], - privKeys: TstPrivKeys[0:1], - // The number of public and private keys should be the same. - err: ErrSeriesSerialization, - }, - { - pubKeys: TstPubKeys[0:1], - privKeys: []string{"NONSENSE"}, - // Not a valid length private key. - err: ErrSeriesSerialization, - }, - } - - active := true - for testNum, test := range tests { - encryptedPubs, err := encryptKeys(test.pubKeys, pool.Manager(), waddrmgr.CKTPublic) - if err != nil { - t.Fatalf("Test #%d - Error encrypting pubkeys: %v", testNum, err) - } - var encryptedPrivs [][]byte - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - encryptedPrivs, err = encryptKeys(test.privKeys, pool.Manager(), waddrmgr.CKTPrivate) - }) - if err != nil { - t.Fatalf("Test #%d - Error encrypting privkeys: %v", testNum, err) - } - - row := &dbSeriesRow{ - version: test.version, - active: active, - reqSigs: test.reqSigs, - pubKeysEncrypted: encryptedPubs, - privKeysEncrypted: encryptedPrivs} - _, err = serializeSeriesRow(row) - - TstCheckError(t, fmt.Sprintf("Test #%d", testNum), err, test.err) - } -} - -func TestSerialization(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - _, addrmgrNs := TstRWNamespaces(dbtx) - - tests := []struct { - version uint32 - active bool - pubKeys []string - privKeys []string - reqSigs uint32 - }{ - { - version: 1, - active: true, - pubKeys: TstPubKeys[0:1], - reqSigs: 1, - }, - { - version: 0, - active: false, - pubKeys: TstPubKeys[0:1], - privKeys: TstPrivKeys[0:1], - reqSigs: 1, - }, - { - pubKeys: TstPubKeys[0:3], - privKeys: []string{TstPrivKeys[0], "", ""}, - reqSigs: 2, - }, - { - pubKeys: TstPubKeys[0:5], - reqSigs: 3, - }, - { - pubKeys: TstPubKeys[0:7], - privKeys: []string{"", TstPrivKeys[1], "", TstPrivKeys[3], "", "", ""}, - reqSigs: 4, - }, - } - - var encryptedPrivs [][]byte - for testNum, test := range tests { - encryptedPubs, err := encryptKeys(test.pubKeys, pool.Manager(), waddrmgr.CKTPublic) - if err != nil { - t.Fatalf("Test #%d - Error encrypting pubkeys: %v", testNum, err) - } - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - encryptedPrivs, err = encryptKeys(test.privKeys, pool.Manager(), waddrmgr.CKTPrivate) - }) - if err != nil { - t.Fatalf("Test #%d - Error encrypting privkeys: %v", testNum, err) - } - - row := &dbSeriesRow{ - version: test.version, - active: test.active, - reqSigs: test.reqSigs, - pubKeysEncrypted: encryptedPubs, - privKeysEncrypted: encryptedPrivs, - } - serialized, err := serializeSeriesRow(row) - if err != nil { - t.Fatalf("Test #%d - Error in serialization %v", testNum, err) - } - - row, err = deserializeSeriesRow(serialized) - if err != nil { - t.Fatalf("Test #%d - Failed to deserialize %v %v", testNum, serialized, err) - } - - if row.version != test.version { - t.Errorf("Serialization #%d - version mismatch: got %d want %d", - testNum, row.version, test.version) - } - - if row.active != test.active { - t.Errorf("Serialization #%d - active mismatch: got %v want %v", - testNum, row.active, test.active) - } - - if row.reqSigs != test.reqSigs { - t.Errorf("Serialization #%d - row reqSigs off. Got %d, want %d", - testNum, row.reqSigs, test.reqSigs) - } - - if len(row.pubKeysEncrypted) != len(test.pubKeys) { - t.Errorf("Serialization #%d - Wrong no. of pubkeys. Got %d, want %d", - testNum, len(row.pubKeysEncrypted), len(test.pubKeys)) - } - - for i, encryptedPub := range encryptedPubs { - got := string(row.pubKeysEncrypted[i]) - - if got != string(encryptedPub) { - t.Errorf("Serialization #%d - Pubkey deserialization. Got %v, want %v", - testNum, got, string(encryptedPub)) - } - } - - if len(row.privKeysEncrypted) != len(row.pubKeysEncrypted) { - t.Errorf("Serialization #%d - no. privkeys (%d) != no. pubkeys (%d)", - testNum, len(row.privKeysEncrypted), len(row.pubKeysEncrypted)) - } - - for i, encryptedPriv := range encryptedPrivs { - got := string(row.privKeysEncrypted[i]) - - if got != string(encryptedPriv) { - t.Errorf("Serialization #%d - Privkey deserialization. Got %v, want %v", - testNum, got, string(encryptedPriv)) - } - } - } -} - -func TestDeserializationErrors(t *testing.T) { - t.Parallel() - - tests := []struct { - serialized []byte - err ErrorCode - }{ - { - serialized: make([]byte, seriesMaxSerial+1), - // Too many bytes (over seriesMaxSerial). - err: ErrSeriesSerialization, - }, - { - serialized: make([]byte, seriesMinSerial-1), - // Not enough bytes (under seriesMinSerial). - err: ErrSeriesSerialization, - }, - { - serialized: []byte{ - 1, 0, 0, 0, // 4 bytes (version) - 0, // 1 byte (active) - 2, 0, 0, 0, // 4 bytes (reqSigs) - 3, 0, 0, 0, // 4 bytes (nKeys) - }, - // Here we have the constant data but are missing any public/private keys. - err: ErrSeriesSerialization, - }, - { - serialized: []byte{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - // Unsupported version. - err: ErrSeriesVersion, - }, - } - - for testNum, test := range tests { - _, err := deserializeSeriesRow(test.serialized) - - TstCheckError(t, fmt.Sprintf("Test #%d", testNum), err, test.err) - } -} - -func TestValidateAndDecryptKeys(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - _, addrmgrNs := TstRWNamespaces(dbtx) - - rawPubKeys, err := encryptKeys(TstPubKeys[0:2], pool.Manager(), waddrmgr.CKTPublic) - if err != nil { - t.Fatalf("Failed to encrypt public keys: %v", err) - } - - var rawPrivKeys [][]byte - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - rawPrivKeys, err = encryptKeys([]string{TstPrivKeys[0], ""}, pool.Manager(), waddrmgr.CKTPrivate) - }) - if err != nil { - t.Fatalf("Failed to encrypt private keys: %v", err) - } - - var pubKeys, privKeys []*hdkeychain.ExtendedKey - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - pubKeys, privKeys, err = validateAndDecryptKeys(rawPubKeys, rawPrivKeys, pool) - }) - if err != nil { - t.Fatalf("Error when validating/decrypting keys: %v", err) - } - - if len(pubKeys) != 2 { - t.Fatalf("Unexpected number of decrypted public keys: got %d, want 2", len(pubKeys)) - } - if len(privKeys) != 2 { - t.Fatalf("Unexpected number of decrypted private keys: got %d, want 2", len(privKeys)) - } - - if pubKeys[0].String() != TstPubKeys[0] || pubKeys[1].String() != TstPubKeys[1] { - t.Fatalf("Public keys don't match: %v!=%v ", TstPubKeys[0:2], pubKeys) - } - - if privKeys[0].String() != TstPrivKeys[0] || privKeys[1] != nil { - t.Fatalf("Private keys don't match: %v, %v", []string{TstPrivKeys[0], ""}, privKeys) - } - - neuteredKey, err := privKeys[0].Neuter() - if err != nil { - t.Fatalf("Unable to neuter private key: %v", err) - } - if pubKeys[0].String() != neuteredKey.String() { - t.Errorf("Public key (%v) does not match neutered private key (%v)", - pubKeys[0].String(), neuteredKey.String()) - } -} - -func TestValidateAndDecryptKeysErrors(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - _, addrmgrNs := TstRWNamespaces(dbtx) - - encryptedPubKeys, err := encryptKeys(TstPubKeys[0:1], pool.Manager(), waddrmgr.CKTPublic) - if err != nil { - t.Fatalf("Failed to encrypt public key: %v", err) - } - - var encryptedPrivKeys [][]byte - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - encryptedPrivKeys, err = encryptKeys(TstPrivKeys[1:2], pool.Manager(), waddrmgr.CKTPrivate) - }) - if err != nil { - t.Fatalf("Failed to encrypt private key: %v", err) - } - - tests := []struct { - rawPubKeys [][]byte - rawPrivKeys [][]byte - err ErrorCode - }{ - { - // Number of public keys does not match number of private keys. - rawPubKeys: [][]byte{[]byte(TstPubKeys[0])}, - rawPrivKeys: [][]byte{}, - err: ErrKeysPrivatePublicMismatch, - }, - { - // Failure to decrypt public key. - rawPubKeys: [][]byte{[]byte(TstPubKeys[0])}, - rawPrivKeys: [][]byte{[]byte(TstPrivKeys[0])}, - err: ErrCrypto, - }, - { - // Failure to decrypt private key. - rawPubKeys: encryptedPubKeys, - rawPrivKeys: [][]byte{[]byte(TstPrivKeys[0])}, - err: ErrCrypto, - }, - { - // One public and one private key, but they don't match. - rawPubKeys: encryptedPubKeys, - rawPrivKeys: encryptedPrivKeys, - err: ErrKeyMismatch, - }, - } - - for i, test := range tests { - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - _, _, err = validateAndDecryptKeys(test.rawPubKeys, test.rawPrivKeys, pool) - }) - TstCheckError(t, fmt.Sprintf("Test #%d", i), err, test.err) - } -} - -func encryptKeys(keys []string, mgr *waddrmgr.Manager, keyType waddrmgr.CryptoKeyType) ([][]byte, error) { - encryptedKeys := make([][]byte, len(keys)) - var err error - for i, key := range keys { - if key == "" { - encryptedKeys[i] = nil - } else { - encryptedKeys[i], err = mgr.Encrypt(keyType, []byte(key)) - } - if err != nil { - return nil, err - } - } - return encryptedKeys, nil -} diff --git a/votingpool/test_coverage.txt b/votingpool/test_coverage.txt deleted file mode 100644 index 0d02016..0000000 --- a/votingpool/test_coverage.txt +++ /dev/null @@ -1,142 +0,0 @@ - -github.com/btcsuite/btcwallet/votingpool/db.go serializeSeriesRow 100.00% (31/31) -github.com/btcsuite/btcwallet/votingpool/pool.go branchOrder 100.00% (19/19) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.splitLastOutput 100.00% (16/16) -github.com/btcsuite/btcwallet/votingpool/pool.go convertAndValidatePubKeys 100.00% (16/16) -github.com/btcsuite/btcwallet/votingpool/input_selection.go byAddress.Less 100.00% (12/12) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.maybeDropRequests 100.00% (12/12) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.rollBackLastOutput 100.00% (10/10) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go calculateSize 100.00% (10/10) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.decryptExtendedKey 100.00% (10/10) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go OutputRequest.outBailmentIDHash 100.00% (8/8) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.ReplaceSeries 100.00% (8/8) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.toMsgTx 100.00% (8/8) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.CreateSeries 100.00% (8/8) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.addChange 100.00% (7/7) -github.com/btcsuite/btcwallet/votingpool/db.go putPool 100.00% (5/5) -github.com/btcsuite/btcwallet/votingpool/pool.go Create 100.00% (5/5) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go newWithdrawalTx 100.00% (5/5) -github.com/btcsuite/btcwallet/votingpool/db.go getUsedAddrHash 100.00% (4/4) -github.com/btcsuite/btcwallet/votingpool/pool.go CanonicalKeyOrder 100.00% (4/4) -github.com/btcsuite/btcwallet/votingpool/pool.go SeriesData.IsEmpowered 100.00% (4/4) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.DepositScriptAddress 100.00% (4/4) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go getRedeemScript 100.00% (4/4) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go newWithdrawal 100.00% (4/4) -github.com/btcsuite/btcwallet/votingpool/db.go @102:3 100.00% (4/4) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.removeInput 100.00% (4/4) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.removeOutput 100.00% (4/4) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.GetSeries 100.00% (4/4) -github.com/btcsuite/btcwallet/votingpool/pool.go @119:3 100.00% (4/4) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.outputTotal 100.00% (3/3) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.inputTotal 100.00% (3/3) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.highestUsedIndexFor 100.00% (3/3) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.popRequest 100.00% (3/3) -github.com/btcsuite/btcwallet/votingpool/pool.go @458:26 100.00% (3/3) -github.com/btcsuite/btcwallet/votingpool/db.go uint32ToBytes 100.00% (3/3) -github.com/btcsuite/btcwallet/votingpool/pool.go @811:3 100.00% (3/3) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.popInput 100.00% (3/3) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.updateStatusFor 100.00% (3/3) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.addInput 100.00% (2/2) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.addOutput 100.00% (2/2) -github.com/btcsuite/btcwallet/votingpool/db.go putSeries 100.00% (2/2) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go Ntxid 100.00% (2/2) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.addressFor 100.00% (2/2) -github.com/btcsuite/btcwallet/votingpool/pool.go zero 100.00% (2/2) -github.com/btcsuite/btcwallet/votingpool/pool.go @780:3 100.00% (2/2) -github.com/btcsuite/btcwallet/votingpool/db.go existsPool 100.00% (2/2) -github.com/btcsuite/btcwallet/votingpool/db.go getUsedAddrBucketID 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/db.go bytesToUint32 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/error.go newError 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/input_selection.go newCredit 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/input_selection.go credit.TxSha 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/input_selection.go credit.OutputIndex 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/input_selection.go credit.Address 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/input_selection.go byAddress.Len 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/input_selection.go byAddress.Swap 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/input_selection.go Pool.isCharterOutput 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/log.go init 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/log.go DisableLog 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/log.go UseLogger 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/pool.go @105:3 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/pool.go newPool 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.Manager 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/pool.go @250:27 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/pool.go @761:3 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.Addr 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.AddrIdentifier 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.RedeemScript 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.Series 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.SeriesID 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.Branch 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.Index 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go byAmount.Len 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go byAmount.Less 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go byAmount.Swap 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go byOutBailmentID.Len 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go byOutBailmentID.Swap 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go byOutBailmentID.Less 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalStatus.Outputs 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go OutputRequest.String 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go OutputRequest.outBailmentID 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalOutput.addOutpoint 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Status 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Address 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Outpoints 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go OutBailmentOutpoint.Amount 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTxOut.pkScript 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go @237:20 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go @240:16 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.hasChange 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.pushRequest 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.pushInput 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go @246:21 100.00% (1/1) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.EmpowerSeries 96.30% (26/27) -github.com/btcsuite/btcwallet/votingpool/db.go deserializeSeriesRow 94.59% (35/37) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.fulfillNextRequest 94.44% (17/18) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.putSeries 93.75% (15/16) -github.com/btcsuite/btcwallet/votingpool/pool.go validateAndDecryptKeys 92.31% (24/26) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go getRawSigs 91.43% (32/35) -github.com/btcsuite/btcwallet/votingpool/input_selection.go nextAddr 90.48% (19/21) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.fulfillRequests 87.50% (14/16) -github.com/btcsuite/btcwallet/votingpool/db.go getMaxUsedIdx 87.50% (7/8) -github.com/btcsuite/btcwallet/votingpool/input_selection.go Pool.getEligibleInputs 85.71% (30/35) -github.com/btcsuite/btcwallet/votingpool/input_selection.go Pool.isCreditEligible 85.71% (6/7) -github.com/btcsuite/btcwallet/votingpool/pool.go Load 85.71% (6/7) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go signMultiSigUTXO 85.19% (23/27) -github.com/btcsuite/btcwallet/votingpool/input_selection.go groupCreditsByAddr 83.33% (10/12) -github.com/btcsuite/btcwallet/votingpool/db.go loadAllSeries 83.33% (5/6) -github.com/btcsuite/btcwallet/votingpool/input_selection.go Pool.highestUsedSeriesIndex 81.82% (9/11) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.handleOversizeTx 80.00% (12/15) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.LoadAllSeries 80.00% (8/10) -github.com/btcsuite/btcwallet/votingpool/pool.go LoadAndCreateSeries 80.00% (8/10) -github.com/btcsuite/btcwallet/votingpool/pool.go LoadAndEmpowerSeries 80.00% (4/5) -github.com/btcsuite/btcwallet/votingpool/pool.go LoadAndReplaceSeries 80.00% (4/5) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.finalizeCurrentTx 79.31% (23/29) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.ChangeAddress 76.92% (10/13) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.WithdrawalAddress 76.92% (10/13) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.getUsedAddr 76.47% (13/17) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go Pool.StartWithdrawal 75.00% (12/16) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.ensureUsedAddr 75.00% (9/12) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go validateSigScript 75.00% (6/8) -github.com/btcsuite/btcwallet/votingpool/pool.go LoadAndGetDepositScript 75.00% (6/8) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go getTxOutIndex 75.00% (3/4) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.poolAddress 75.00% (3/4) -github.com/btcsuite/btcwallet/votingpool/db.go putUsedAddrHash 75.00% (3/4) -github.com/btcsuite/btcwallet/votingpool/pool.go SeriesData.getPrivKeyFor 75.00% (3/4) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.DepositScript 73.08% (19/26) -github.com/btcsuite/btcwallet/votingpool/db.go @132:3 71.43% (5/7) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go nextChangeAddr 71.43% (5/7) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go storeTransactions 70.59% (12/17) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.saveSeriesToDisk 70.00% (14/20) -github.com/btcsuite/btcwallet/votingpool/pool.go Pool.addUsedAddr 69.23% (9/13) -github.com/btcsuite/btcwallet/votingpool/error.go ErrorCode.String 66.67% (2/3) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go SignTx 62.50% (5/8) -github.com/btcsuite/btcwallet/votingpool/db.go putSeriesRow 53.85% (7/13) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go nBytesToSerialize 40.00% (2/5) -github.com/btcsuite/btcwallet/votingpool/error.go Error.Error 0.00% (0/3) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalOutput.String 0.00% (0/1) -github.com/btcsuite/btcwallet/votingpool/input_selection.go credit.String 0.00% (0/1) -github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTxOut.String 0.00% (0/1) -github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.String 0.00% (0/1) -github.com/btcsuite/btcwallet/votingpool ------------------------------- 87.49% (818/935) - diff --git a/votingpool/test_data_test.go b/votingpool/test_data_test.go deleted file mode 100644 index f888ebb..0000000 --- a/votingpool/test_data_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2015 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package votingpool - -// Sample data used across our tests. - -var TstPrivKeys = []string{ - "xprv9s21ZrQH143K2j9PK4CXkCu8sgxkpUxCF7p1KVwiV5tdnkeYzJXReUkxz5iB2FUzTXC1L15abCDG4RMxSYT5zhm67uvsnLYxuDhZfoFcB6a", - "xprv9s21ZrQH143K4PtW77ATQAKAGk7KAFFCzxFuAcWduoMEeQhCgWpuYWQvMGZknqdispUbgLZV1YPqFCbpzMJij8tSZ5xPSaZqPbchojeNuq7", - "xprv9s21ZrQH143K27XboWxXZGU5j7VZ9SqVBnmMQPKTbddiWAhuNzeLynKHaZTAti6N454tVUUcvy6u15DfuW68NCBUxry6ZsHHzqoA8UtzdMn", - "xprv9s21ZrQH143K2vb4DGQymRejLcZSksBHTYLxB7Stg1c7Lk9JxgEUGZTozwUKxoEWJPoGSdGnJY1TW7LNFQCWrpZjDdEXJeqJuDde6BmdD4P", - "xprv9s21ZrQH143K4JNmRvWeLc1PggzusKcDYV1y8fAMNDdb9Rm5X1AvGHizxEdhTVR3sc62XvifC6dLAXMuQesX1y6999xnDwQ3aVno8KviU9d", - "xprv9s21ZrQH143K3dxrqESqeHZ7pSwM6Uq77ssQADSBs7qdFs6dyRWmRcPyLUTQRpgB3EduNhJuWkCGG2LHjuUisw8KKfXJpPqYJ1MSPrZpe1z", - "xprv9s21ZrQH143K2nE8ENAMNksTTVxPrMxFNWUuwThMy2bcH9LHTtQDXSNq2pTNcbuq36n5A3J9pbXVqnq5LDXvqniFRLN299kW7Svnxsx9tQv", - "xprv9s21ZrQH143K3p93xF1oFeB6ey5ruUesWjuPxA9Z2R5wf6BLYfGXz7fg7NavWkQ2cx3Vm8w2HV9uKpSprNNHnenGeW9XhYDPSjwS9hyCs33", - "xprv9s21ZrQH143K3WxnnvPZ8SDGXndASvLTFwMLBVzNCVgs9rzP6rXgW92DLvozdyBm8T9bSQvrFm1jMpTJrRE6w1KY5tshFeDk9Nn3K6V5FYX", -} - -var TstPubKeys = []string{ - "xpub661MyMwAqRbcFDDrR5jY7LqsRioFDwg3cLjc7tML3RRcfYyhXqqgCH5SqMSQdpQ1Xh8EtVwcfm8psD8zXKPcRaCVSY4GCqbb3aMEs27GitE", - "xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9", - "xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh", - "xpub661MyMwAqRbcFQfXKHwz8ZbTtePwAKu8pmGYyVrWEM96DYUTWDYipMnHrFcemZHn13jcRMfsNU3UWQUudiaE7mhkWCHGFRMavF167DQM4Va", - "xpub661MyMwAqRbcGnTEXx3ehjx8EiqQGnL4uhwZw3ZxvZAa2E6E4YVAp63UoVtvm2vMDDF8BdPpcarcf7PWcEKvzHhxzAYw1zG23C2egeh82AR", - "xpub661MyMwAqRbcG83KwFyr1RVrNUmqVwYxV6nzxbqoRTNc8fRnWxq1yQiTBifTHhevcEM9ucZ1TqFS7Kv17Gd81cesv6RDrrvYS9SLPjPXhV5", - "xpub661MyMwAqRbcFGJbLPhMjtpC1XntFpg6jjQWjr6yXN8b9wfS1RiU5EhJt5L7qoFuidYawc3XJoLjT2PcjVpXryS3hn1WmSPCyvQDNuKsfgM", - "xpub661MyMwAqRbcGJDX4GYocn7qCzvMJwNisxpzkYZAakcvXtWV6CanXuz9xdfe5kTptFMJ4hDt2iTiT11zyN14u8R5zLvoZ1gnEVqNLxp1r3v", - "xpub661MyMwAqRbcG13FtwvZVaA15pTerP4JdAGvytPykqDr2fKXePqw3wLhCALPAixsE176jFkc2ac9K3tnF4KwaTRKUqFF5apWD6XL9LHCu7E", -} diff --git a/votingpool/withdrawal.go b/votingpool/withdrawal.go deleted file mode 100644 index b148c39..0000000 --- a/votingpool/withdrawal.go +++ /dev/null @@ -1,1046 +0,0 @@ -// Copyright (c) 2015-2017 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package votingpool - -import ( - "bytes" - "crypto/sha256" - "fmt" - "math" - "reflect" - "sort" - "strconv" - "time" - - "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/waddrmgr" - "github.com/btcsuite/btcwallet/walletdb" - "github.com/btcsuite/btcwallet/wtxmgr" -) - -// Maximum tx size (in bytes). This should be the same as bitcoind's -// MAX_STANDARD_TX_SIZE. -const txMaxSize = 100000 - -// feeIncrement is the minimum transation fee (0.00001 BTC, measured in satoshis) -// added to transactions requiring a fee. -const feeIncrement = 1e3 - -type outputStatus byte - -const ( - statusSuccess outputStatus = iota - statusPartial - statusSplit -) - -// OutBailmentID is the unique ID of a user's outbailment, comprising the -// name of the server the user connected to, and the transaction number, -// internal to that server. -type OutBailmentID string - -// Ntxid is the normalized ID of a given bitcoin transaction, which is generated -// by hashing the serialized tx with blank sig scripts on all inputs. -type Ntxid string - -// OutputRequest represents one of the outputs (address/amount) requested by a -// withdrawal, and includes information about the user's outbailment request. -type OutputRequest struct { - Address btcutil.Address - Amount btcutil.Amount - PkScript []byte - - // The notary server that received the outbailment request. - Server string - - // The server-specific transaction number for the outbailment request. - Transaction uint32 - - // cachedHash is used to cache the hash of the outBailmentID so it - // only has to be calculated once. - cachedHash []byte -} - -// WithdrawalOutput represents a possibly fulfilled OutputRequest. -type WithdrawalOutput struct { - request OutputRequest - status outputStatus - // The outpoints that fulfill the OutputRequest. There will be more than one in case we - // need to split the request across multiple transactions. - outpoints []OutBailmentOutpoint -} - -// OutBailmentOutpoint represents one of the outpoints created to fulfill an OutputRequest. -type OutBailmentOutpoint struct { - ntxid Ntxid - index uint32 - amount btcutil.Amount -} - -// changeAwareTx is just a wrapper around wire.MsgTx that knows about its change -// output, if any. -type changeAwareTx struct { - *wire.MsgTx - changeIdx int32 // -1 if there's no change output. -} - -// WithdrawalStatus contains the details of a processed withdrawal, including -// the status of each requested output, the total amount of network fees and the -// next input and change addresses to use in a subsequent withdrawal request. -type WithdrawalStatus struct { - nextInputAddr WithdrawalAddress - nextChangeAddr ChangeAddress - fees btcutil.Amount - outputs map[OutBailmentID]*WithdrawalOutput - sigs map[Ntxid]TxSigs - transactions map[Ntxid]changeAwareTx -} - -// withdrawalInfo contains all the details of an existing withdrawal, including -// the original request parameters and the WithdrawalStatus returned by -// StartWithdrawal. -type withdrawalInfo struct { - requests []OutputRequest - startAddress WithdrawalAddress - changeStart ChangeAddress - lastSeriesID uint32 - dustThreshold btcutil.Amount - status WithdrawalStatus -} - -// TxSigs is list of raw signatures (one for every pubkey in the multi-sig -// script) for a given transaction input. They should match the order of pubkeys -// in the script and an empty RawSig should be used when the private key for a -// pubkey is not known. -type TxSigs [][]RawSig - -// RawSig represents one of the signatures included in the unlocking script of -// inputs spending from P2SH UTXOs. -type RawSig []byte - -// byAmount defines the methods needed to satisify sort.Interface to -// sort a slice of OutputRequests by their amount. -type byAmount []OutputRequest - -func (u byAmount) Len() int { return len(u) } -func (u byAmount) Less(i, j int) bool { return u[i].Amount < u[j].Amount } -func (u byAmount) Swap(i, j int) { u[i], u[j] = u[j], u[i] } - -// byOutBailmentID defines the methods needed to satisify sort.Interface to sort -// a slice of OutputRequests by their outBailmentIDHash. -type byOutBailmentID []OutputRequest - -func (s byOutBailmentID) Len() int { return len(s) } -func (s byOutBailmentID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -func (s byOutBailmentID) Less(i, j int) bool { - return bytes.Compare(s[i].outBailmentIDHash(), s[j].outBailmentIDHash()) < 0 -} - -func (s outputStatus) String() string { - strings := map[outputStatus]string{ - statusSuccess: "success", - statusPartial: "partial-", - statusSplit: "split", - } - return strings[s] -} - -func (tx *changeAwareTx) addSelfToStore(store *wtxmgr.Store, txmgrNs walletdb.ReadWriteBucket) error { - rec, err := wtxmgr.NewTxRecordFromMsgTx(tx.MsgTx, time.Now()) - if err != nil { - return newError(ErrWithdrawalTxStorage, "error constructing TxRecord for storing", err) - } - - if err := store.InsertTx(txmgrNs, rec, nil); err != nil { - return newError(ErrWithdrawalTxStorage, "error adding tx to store", err) - } - if tx.changeIdx != -1 { - if err = store.AddCredit(txmgrNs, rec, nil, uint32(tx.changeIdx), true); err != nil { - return newError(ErrWithdrawalTxStorage, "error adding tx credits to store", err) - } - } - return nil -} - -// Outputs returns a map of outbailment IDs to WithdrawalOutputs for all outputs -// requested in this withdrawal. -func (s *WithdrawalStatus) Outputs() map[OutBailmentID]*WithdrawalOutput { - return s.outputs -} - -// Sigs returns a map of ntxids to signature lists for every input in the tx -// with that ntxid. -func (s *WithdrawalStatus) Sigs() map[Ntxid]TxSigs { - return s.sigs -} - -// Fees returns the total amount of network fees included in all transactions -// generated as part of a withdrawal. -func (s *WithdrawalStatus) Fees() btcutil.Amount { - return s.fees -} - -// NextInputAddr returns the votingpool address that should be used as the -// startAddress of subsequent withdrawals. -func (s *WithdrawalStatus) NextInputAddr() WithdrawalAddress { - return s.nextInputAddr -} - -// NextChangeAddr returns the votingpool address that should be used as the -// changeStart of subsequent withdrawals. -func (s *WithdrawalStatus) NextChangeAddr() ChangeAddress { - return s.nextChangeAddr -} - -// String makes OutputRequest satisfy the Stringer interface. -func (r OutputRequest) String() string { - return fmt.Sprintf("OutputRequest %s to send %v to %s", r.outBailmentID(), r.Amount, r.Address) -} - -func (r OutputRequest) outBailmentID() OutBailmentID { - return OutBailmentID(fmt.Sprintf("%s:%d", r.Server, r.Transaction)) -} - -// outBailmentIDHash returns a byte slice which is used when sorting -// OutputRequests. -func (r OutputRequest) outBailmentIDHash() []byte { - if r.cachedHash != nil { - return r.cachedHash - } - str := r.Server + strconv.Itoa(int(r.Transaction)) - hasher := sha256.New() - // hasher.Write() always returns nil as the error, so it's safe to ignore it here. - _, _ = hasher.Write([]byte(str)) - id := hasher.Sum(nil) - r.cachedHash = id - return id -} - -func (o *WithdrawalOutput) String() string { - return fmt.Sprintf("WithdrawalOutput for %s", o.request) -} - -func (o *WithdrawalOutput) addOutpoint(outpoint OutBailmentOutpoint) { - o.outpoints = append(o.outpoints, outpoint) -} - -// Status returns the status of this WithdrawalOutput. -func (o *WithdrawalOutput) Status() string { - return o.status.String() -} - -// Address returns the string representation of this WithdrawalOutput's address. -func (o *WithdrawalOutput) Address() string { - return o.request.Address.String() -} - -// Outpoints returns a slice containing the OutBailmentOutpoints created to -// fulfill this output. -func (o *WithdrawalOutput) Outpoints() []OutBailmentOutpoint { - return o.outpoints -} - -// Amount returns the amount (in satoshis) in this OutBailmentOutpoint. -func (o OutBailmentOutpoint) Amount() btcutil.Amount { - return o.amount -} - -// withdrawal holds all the state needed for Pool.Withdrawal() to do its job. -type withdrawal struct { - roundID uint32 - status *WithdrawalStatus - transactions []*withdrawalTx - pendingRequests []OutputRequest - eligibleInputs []credit - current *withdrawalTx - // txOptions is a function called for every new withdrawalTx created as - // part of this withdrawal. It is defined as a function field because it - // exists mainly so that tests can mock withdrawalTx fields. - txOptions func(tx *withdrawalTx) -} - -// withdrawalTxOut wraps an OutputRequest and provides a separate amount field. -// It is necessary because some requests may be partially fulfilled or split -// across transactions. -type withdrawalTxOut struct { - // Notice that in the case of a split output, the OutputRequest here will - // be a copy of the original one with the amount being the remainder of the - // originally requested amount minus the amounts fulfilled by other - // withdrawalTxOut. The original OutputRequest, if needed, can be obtained - // from WithdrawalStatus.outputs. - request OutputRequest - amount btcutil.Amount -} - -// String makes withdrawalTxOut satisfy the Stringer interface. -func (o *withdrawalTxOut) String() string { - return fmt.Sprintf("withdrawalTxOut fulfilling %v of %s", o.amount, o.request) -} - -func (o *withdrawalTxOut) pkScript() []byte { - return o.request.PkScript -} - -// withdrawalTx represents a transaction constructed by the withdrawal process. -type withdrawalTx struct { - inputs []credit - outputs []*withdrawalTxOut - fee btcutil.Amount - - // changeOutput holds information about the change for this transaction. - changeOutput *wire.TxOut - - // calculateSize returns the estimated serialized size (in bytes) of this - // tx. See calculateTxSize() for details on how that's done. We use a - // struct field instead of a method so that it can be replaced in tests. - calculateSize func() int - // calculateFee calculates the expected network fees for this tx. We use a - // struct field instead of a method so that it can be replaced in tests. - calculateFee func() btcutil.Amount -} - -// newWithdrawalTx creates a new withdrawalTx and calls setOptions() -// passing the newly created tx. -func newWithdrawalTx(setOptions func(tx *withdrawalTx)) *withdrawalTx { - tx := &withdrawalTx{} - tx.calculateSize = func() int { return calculateTxSize(tx) } - tx.calculateFee = func() btcutil.Amount { - return btcutil.Amount(1+tx.calculateSize()/1000) * feeIncrement - } - setOptions(tx) - return tx -} - -// ntxid returns the unique ID for this transaction. -func (tx *withdrawalTx) ntxid() Ntxid { - msgtx := tx.toMsgTx() - var empty []byte - for _, txin := range msgtx.TxIn { - txin.SignatureScript = empty - } - return Ntxid(msgtx.TxHash().String()) -} - -// isTooBig returns true if the size (in bytes) of the given tx is greater -// than or equal to txMaxSize. -func (tx *withdrawalTx) isTooBig() bool { - // In bitcoind a tx is considered standard only if smaller than - // MAX_STANDARD_TX_SIZE; that's why we consider anything >= txMaxSize to - // be too big. - return tx.calculateSize() >= txMaxSize -} - -// inputTotal returns the sum amount of all inputs in this tx. -func (tx *withdrawalTx) inputTotal() (total btcutil.Amount) { - for _, input := range tx.inputs { - total += input.Amount - } - return total -} - -// outputTotal returns the sum amount of all outputs in this tx. It does not -// include the amount for the change output, in case the tx has one. -func (tx *withdrawalTx) outputTotal() (total btcutil.Amount) { - for _, output := range tx.outputs { - total += output.amount - } - return total -} - -// hasChange returns true if this transaction has a change output. -func (tx *withdrawalTx) hasChange() bool { - return tx.changeOutput != nil -} - -// toMsgTx generates a btcwire.MsgTx with this tx's inputs and outputs. -func (tx *withdrawalTx) toMsgTx() *wire.MsgTx { - msgtx := wire.NewMsgTx(wire.TxVersion) - for _, o := range tx.outputs { - msgtx.AddTxOut(wire.NewTxOut(int64(o.amount), o.pkScript())) - } - - if tx.hasChange() { - msgtx.AddTxOut(tx.changeOutput) - } - - for _, i := range tx.inputs { - msgtx.AddTxIn(wire.NewTxIn(&i.OutPoint, []byte{}, nil)) - } - return msgtx -} - -// addOutput adds a new output to this transaction. -func (tx *withdrawalTx) addOutput(request OutputRequest) { - log.Debugf("Added tx output sending %s to %s", request.Amount, request.Address) - tx.outputs = append(tx.outputs, &withdrawalTxOut{request: request, amount: request.Amount}) -} - -// removeOutput removes the last added output and returns it. -func (tx *withdrawalTx) removeOutput() *withdrawalTxOut { - removed := tx.outputs[len(tx.outputs)-1] - tx.outputs = tx.outputs[:len(tx.outputs)-1] - log.Debugf("Removed tx output sending %s to %s", removed.amount, removed.request.Address) - return removed -} - -// addInput adds a new input to this transaction. -func (tx *withdrawalTx) addInput(input credit) { - log.Debugf("Added tx input with amount %v", input.Amount) - tx.inputs = append(tx.inputs, input) -} - -// removeInput removes the last added input and returns it. -func (tx *withdrawalTx) removeInput() credit { - removed := tx.inputs[len(tx.inputs)-1] - tx.inputs = tx.inputs[:len(tx.inputs)-1] - log.Debugf("Removed tx input with amount %v", removed.Amount) - return removed -} - -// addChange adds a change output if there are any satoshis left after paying -// all the outputs and network fees. It returns true if a change output was -// added. -// -// This method must be called only once, and no extra inputs/outputs should be -// added after it's called. Also, callsites must make sure adding a change -// output won't cause the tx to exceed the size limit. -func (tx *withdrawalTx) addChange(pkScript []byte) bool { - tx.fee = tx.calculateFee() - change := tx.inputTotal() - tx.outputTotal() - tx.fee - log.Debugf("addChange: input total %v, output total %v, fee %v", tx.inputTotal(), - tx.outputTotal(), tx.fee) - if change > 0 { - tx.changeOutput = wire.NewTxOut(int64(change), pkScript) - log.Debugf("Added change output with amount %v", change) - } - return tx.hasChange() -} - -// rollBackLastOutput will roll back the last added output and possibly remove -// inputs that are no longer needed to cover the remaining outputs. The method -// returns the removed output and the removed inputs, in the reverse order they -// were added, if any. -// -// The tx needs to have two or more outputs. The case with only one output must -// be handled separately (by the split output procedure). -func (tx *withdrawalTx) rollBackLastOutput() ([]credit, *withdrawalTxOut, error) { - // Check precondition: At least two outputs are required in the transaction. - if len(tx.outputs) < 2 { - str := fmt.Sprintf("at least two outputs expected; got %d", len(tx.outputs)) - return nil, nil, newError(ErrPreconditionNotMet, str, nil) - } - - removedOutput := tx.removeOutput() - - var removedInputs []credit - // Continue until sum(in) < sum(out) + fee - for tx.inputTotal() >= tx.outputTotal()+tx.calculateFee() { - removedInputs = append(removedInputs, tx.removeInput()) - } - - // Re-add the last item from removedInputs, which is the last popped input. - tx.addInput(removedInputs[len(removedInputs)-1]) - removedInputs = removedInputs[:len(removedInputs)-1] - return removedInputs, removedOutput, nil -} - -func defaultTxOptions(tx *withdrawalTx) {} - -func newWithdrawal(roundID uint32, requests []OutputRequest, inputs []credit, - changeStart ChangeAddress) *withdrawal { - outputs := make(map[OutBailmentID]*WithdrawalOutput, len(requests)) - for _, request := range requests { - outputs[request.outBailmentID()] = &WithdrawalOutput{request: request} - } - status := &WithdrawalStatus{ - outputs: outputs, - nextChangeAddr: changeStart, - } - return &withdrawal{ - roundID: roundID, - pendingRequests: requests, - eligibleInputs: inputs, - status: status, - txOptions: defaultTxOptions, - } -} - -// StartWithdrawal uses a fully deterministic algorithm to construct -// transactions fulfilling as many of the given output requests as possible. -// It returns a WithdrawalStatus containing the outpoints fulfilling the -// requested outputs and a map of normalized transaction IDs (ntxid) to -// signature lists (one for every private key available to this wallet) for each -// of those transaction's inputs. More details about the actual algorithm can be -// found at http://opentransactions.org/wiki/index.php/Startwithdrawal -// This method must be called with the address manager unlocked. -func (p *Pool) StartWithdrawal(ns walletdb.ReadWriteBucket, addrmgrNs walletdb.ReadBucket, roundID uint32, requests []OutputRequest, - startAddress WithdrawalAddress, lastSeriesID uint32, changeStart ChangeAddress, - txStore *wtxmgr.Store, txmgrNs walletdb.ReadBucket, chainHeight int32, dustThreshold btcutil.Amount) ( - *WithdrawalStatus, error) { - - status, err := getWithdrawalStatus(p, ns, addrmgrNs, roundID, requests, startAddress, lastSeriesID, - changeStart, dustThreshold) - if err != nil { - return nil, err - } - if status != nil { - return status, nil - } - - eligible, err := p.getEligibleInputs(ns, addrmgrNs, txStore, txmgrNs, startAddress, lastSeriesID, dustThreshold, - chainHeight, eligibleInputMinConfirmations) - if err != nil { - return nil, err - } - - w := newWithdrawal(roundID, requests, eligible, changeStart) - if err := w.fulfillRequests(); err != nil { - return nil, err - } - w.status.sigs, err = getRawSigs(w.transactions) - if err != nil { - return nil, err - } - - serialized, err := serializeWithdrawal(requests, startAddress, lastSeriesID, changeStart, - dustThreshold, *w.status) - if err != nil { - return nil, err - } - err = putWithdrawal(ns, p.ID, roundID, serialized) - if err != nil { - return nil, err - } - - return w.status, nil -} - -// popRequest removes and returns the first request from the stack of pending -// requests. -func (w *withdrawal) popRequest() OutputRequest { - request := w.pendingRequests[0] - w.pendingRequests = w.pendingRequests[1:] - return request -} - -// pushRequest adds a new request to the top of the stack of pending requests. -func (w *withdrawal) pushRequest(request OutputRequest) { - w.pendingRequests = append([]OutputRequest{request}, w.pendingRequests...) -} - -// popInput removes and returns the first input from the stack of eligible -// inputs. -func (w *withdrawal) popInput() credit { - input := w.eligibleInputs[len(w.eligibleInputs)-1] - w.eligibleInputs = w.eligibleInputs[:len(w.eligibleInputs)-1] - return input -} - -// pushInput adds a new input to the top of the stack of eligible inputs. -func (w *withdrawal) pushInput(input credit) { - w.eligibleInputs = append(w.eligibleInputs, input) -} - -// If this returns it means we have added an output and the necessary inputs to fulfil that -// output plus the required fees. It also means the tx won't reach the size limit even -// after we add a change output and sign all inputs. -func (w *withdrawal) fulfillNextRequest() error { - request := w.popRequest() - output := w.status.outputs[request.outBailmentID()] - // We start with an output status of success and let the methods that deal - // with special cases change it when appropriate. - output.status = statusSuccess - w.current.addOutput(request) - - if w.current.isTooBig() { - return w.handleOversizeTx() - } - - fee := w.current.calculateFee() - for w.current.inputTotal() < w.current.outputTotal()+fee { - if len(w.eligibleInputs) == 0 { - log.Debug("Splitting last output because we don't have enough inputs") - if err := w.splitLastOutput(); err != nil { - return err - } - break - } - w.current.addInput(w.popInput()) - fee = w.current.calculateFee() - - if w.current.isTooBig() { - return w.handleOversizeTx() - } - } - return nil -} - -// handleOversizeTx handles the case when a transaction has become too -// big by either rolling back an output or splitting it. -func (w *withdrawal) handleOversizeTx() error { - if len(w.current.outputs) > 1 { - log.Debug("Rolling back last output because tx got too big") - inputs, output, err := w.current.rollBackLastOutput() - if err != nil { - return newError(ErrWithdrawalProcessing, "failed to rollback last output", err) - } - for _, input := range inputs { - w.pushInput(input) - } - w.pushRequest(output.request) - } else if len(w.current.outputs) == 1 { - log.Debug("Splitting last output because tx got too big...") - w.pushInput(w.current.removeInput()) - if err := w.splitLastOutput(); err != nil { - return err - } - } else { - return newError(ErrPreconditionNotMet, "Oversize tx must have at least one output", nil) - } - return w.finalizeCurrentTx() -} - -// finalizeCurrentTx finalizes the transaction in w.current, moves it to the -// list of finalized transactions and replaces w.current with a new empty -// transaction. -func (w *withdrawal) finalizeCurrentTx() error { - log.Debug("Finalizing current transaction") - tx := w.current - if len(tx.outputs) == 0 { - log.Debug("Current transaction has no outputs, doing nothing") - return nil - } - - pkScript, err := txscript.PayToAddrScript(w.status.nextChangeAddr.addr) - if err != nil { - return newError(ErrWithdrawalProcessing, "failed to generate pkScript for change address", err) - } - if tx.addChange(pkScript) { - var err error - w.status.nextChangeAddr, err = nextChangeAddress(w.status.nextChangeAddr) - if err != nil { - return newError(ErrWithdrawalProcessing, "failed to get next change address", err) - } - } - - ntxid := tx.ntxid() - for i, txOut := range tx.outputs { - outputStatus := w.status.outputs[txOut.request.outBailmentID()] - outputStatus.addOutpoint( - OutBailmentOutpoint{ntxid: ntxid, index: uint32(i), amount: txOut.amount}) - } - - // Check that WithdrawalOutput entries with status==success have the sum of - // their outpoint amounts matching the requested amount. - for _, txOut := range tx.outputs { - // Look up the original request we received because txOut.request may - // represent a split request and thus have a different amount from the - // original one. - outputStatus := w.status.outputs[txOut.request.outBailmentID()] - origRequest := outputStatus.request - amtFulfilled := btcutil.Amount(0) - for _, outpoint := range outputStatus.outpoints { - amtFulfilled += outpoint.amount - } - if outputStatus.status == statusSuccess && amtFulfilled != origRequest.Amount { - msg := fmt.Sprintf("%s was not completely fulfilled; only %v fulfilled", origRequest, - amtFulfilled) - return newError(ErrWithdrawalProcessing, msg, nil) - } - } - - w.transactions = append(w.transactions, tx) - w.current = newWithdrawalTx(w.txOptions) - return nil -} - -// maybeDropRequests will check the total amount we have in eligible inputs and drop -// requested outputs (in descending amount order) if we don't have enough to -// fulfill them all. For every dropped output request we update its entry in -// w.status.outputs with the status string set to statusPartial. -func (w *withdrawal) maybeDropRequests() { - inputAmount := btcutil.Amount(0) - for _, input := range w.eligibleInputs { - inputAmount += input.Amount - } - outputAmount := btcutil.Amount(0) - for _, request := range w.pendingRequests { - outputAmount += request.Amount - } - sort.Sort(sort.Reverse(byAmount(w.pendingRequests))) - for inputAmount < outputAmount { - request := w.popRequest() - log.Infof("Not fulfilling request to send %v to %v; not enough credits.", - request.Amount, request.Address) - outputAmount -= request.Amount - w.status.outputs[request.outBailmentID()].status = statusPartial - } -} - -func (w *withdrawal) fulfillRequests() error { - w.maybeDropRequests() - if len(w.pendingRequests) == 0 { - return nil - } - - // Sort outputs by outBailmentID (hash(server ID, tx #)) - sort.Sort(byOutBailmentID(w.pendingRequests)) - - w.current = newWithdrawalTx(w.txOptions) - for len(w.pendingRequests) > 0 { - if err := w.fulfillNextRequest(); err != nil { - return err - } - tx := w.current - if len(w.eligibleInputs) == 0 && tx.inputTotal() <= tx.outputTotal()+tx.calculateFee() { - // We don't have more eligible inputs and all the inputs in the - // current tx have been spent. - break - } - } - - if err := w.finalizeCurrentTx(); err != nil { - return err - } - - // TODO: Update w.status.nextInputAddr. Not yet implemented as in some - // conditions we need to know about un-thawed series. - - w.status.transactions = make(map[Ntxid]changeAwareTx, len(w.transactions)) - for _, tx := range w.transactions { - w.status.updateStatusFor(tx) - w.status.fees += tx.fee - msgtx := tx.toMsgTx() - changeIdx := -1 - if tx.hasChange() { - // When withdrawalTx has a change, we know it will be the last entry - // in the generated MsgTx. - changeIdx = len(msgtx.TxOut) - 1 - } - w.status.transactions[tx.ntxid()] = changeAwareTx{ - MsgTx: msgtx, - changeIdx: int32(changeIdx), - } - } - return nil -} - -func (w *withdrawal) splitLastOutput() error { - if len(w.current.outputs) == 0 { - return newError(ErrPreconditionNotMet, - "splitLastOutput requires current tx to have at least 1 output", nil) - } - - tx := w.current - output := tx.outputs[len(tx.outputs)-1] - log.Debugf("Splitting tx output for %s", output.request) - origAmount := output.amount - spentAmount := tx.outputTotal() + tx.calculateFee() - output.amount - // This is how much we have left after satisfying all outputs except the last - // one. IOW, all we have left for the last output, so we set that as the - // amount of the tx's last output. - unspentAmount := tx.inputTotal() - spentAmount - output.amount = unspentAmount - log.Debugf("Updated output amount to %v", output.amount) - - // Create a new OutputRequest with the amount being the difference between - // the original amount and what was left in the tx output above. - request := output.request - newRequest := OutputRequest{ - Server: request.Server, - Transaction: request.Transaction, - Address: request.Address, - PkScript: request.PkScript, - Amount: origAmount - output.amount} - w.pushRequest(newRequest) - log.Debugf("Created a new pending output request with amount %v", newRequest.Amount) - - w.status.outputs[request.outBailmentID()].status = statusPartial - return nil -} - -func (s *WithdrawalStatus) updateStatusFor(tx *withdrawalTx) { - for _, output := range s.outputs { - if len(output.outpoints) > 1 { - output.status = statusSplit - } - // TODO: Update outputs with status=='partial-'. For this we need an API - // that gives us the amount of credits in a given series. - // http://opentransactions.org/wiki/index.php/Update_Status - } -} - -// match returns true if the given arguments match the fields in this -// withdrawalInfo. For the requests slice, the order of the items does not -// matter. -func (wi *withdrawalInfo) match(requests []OutputRequest, startAddress WithdrawalAddress, - lastSeriesID uint32, changeStart ChangeAddress, dustThreshold btcutil.Amount) bool { - // Use reflect.DeepEqual to compare changeStart and startAddress as they're - // structs that contain pointers and we want to compare their content and - // not their address. - if !reflect.DeepEqual(changeStart, wi.changeStart) { - log.Debugf("withdrawal changeStart does not match: %v != %v", changeStart, wi.changeStart) - return false - } - if !reflect.DeepEqual(startAddress, wi.startAddress) { - log.Debugf("withdrawal startAddr does not match: %v != %v", startAddress, wi.startAddress) - return false - } - if lastSeriesID != wi.lastSeriesID { - log.Debugf("withdrawal lastSeriesID does not match: %v != %v", lastSeriesID, - wi.lastSeriesID) - return false - } - if dustThreshold != wi.dustThreshold { - log.Debugf("withdrawal dustThreshold does not match: %v != %v", dustThreshold, - wi.dustThreshold) - return false - } - r1 := make([]OutputRequest, len(requests)) - copy(r1, requests) - r2 := make([]OutputRequest, len(wi.requests)) - copy(r2, wi.requests) - sort.Sort(byOutBailmentID(r1)) - sort.Sort(byOutBailmentID(r2)) - if !reflect.DeepEqual(r1, r2) { - log.Debugf("withdrawal requests does not match: %v != %v", requests, wi.requests) - return false - } - return true -} - -// getWithdrawalStatus returns the existing WithdrawalStatus for the given -// withdrawal parameters, if one exists. This function must be called with the -// address manager unlocked. -func getWithdrawalStatus(p *Pool, ns, addrmgrNs walletdb.ReadBucket, roundID uint32, requests []OutputRequest, - startAddress WithdrawalAddress, lastSeriesID uint32, changeStart ChangeAddress, - dustThreshold btcutil.Amount) (*WithdrawalStatus, error) { - - serialized := getWithdrawal(ns, p.ID, roundID) - if bytes.Equal(serialized, []byte{}) { - return nil, nil - } - wInfo, err := deserializeWithdrawal(p, ns, addrmgrNs, serialized) - if err != nil { - return nil, err - } - if wInfo.match(requests, startAddress, lastSeriesID, changeStart, dustThreshold) { - return &wInfo.status, nil - } - return nil, nil -} - -// getRawSigs iterates over the inputs of each transaction given, constructing the -// raw signatures for them using the private keys available to us. -// It returns a map of ntxids to signature lists. -func getRawSigs(transactions []*withdrawalTx) (map[Ntxid]TxSigs, error) { - sigs := make(map[Ntxid]TxSigs) - for _, tx := range transactions { - txSigs := make(TxSigs, len(tx.inputs)) - msgtx := tx.toMsgTx() - ntxid := tx.ntxid() - for inputIdx, input := range tx.inputs { - creditAddr := input.addr - redeemScript := creditAddr.redeemScript() - series := creditAddr.series() - // The order of the raw signatures in the signature script must match the - // order of the public keys in the redeem script, so we sort the public keys - // here using the same API used to sort them in the redeem script and use - // series.getPrivKeyFor() to lookup the corresponding private keys. - pubKeys, err := branchOrder(series.publicKeys, creditAddr.Branch()) - if err != nil { - return nil, err - } - txInSigs := make([]RawSig, len(pubKeys)) - for i, pubKey := range pubKeys { - var sig RawSig - privKey, err := series.getPrivKeyFor(pubKey) - if err != nil { - return nil, err - } - if privKey != nil { - childKey, err := privKey.Child(uint32(creditAddr.Index())) - if err != nil { - return nil, newError(ErrKeyChain, "failed to derive private key", err) - } - ecPrivKey, err := childKey.ECPrivKey() - if err != nil { - return nil, newError(ErrKeyChain, "failed to obtain ECPrivKey", err) - } - log.Debugf("Generating raw sig for input %d of tx %s with privkey of %s", - inputIdx, ntxid, pubKey.String()) - sig, err = txscript.RawTxInSignature( - msgtx, inputIdx, redeemScript, txscript.SigHashAll, ecPrivKey) - if err != nil { - return nil, newError(ErrRawSigning, "failed to generate raw signature", err) - } - } else { - log.Debugf("Not generating raw sig for input %d of %s because private key "+ - "for %s is not available: %v", inputIdx, ntxid, pubKey.String(), err) - sig = []byte{} - } - txInSigs[i] = sig - } - txSigs[inputIdx] = txInSigs - } - sigs[ntxid] = txSigs - } - return sigs, nil -} - -// SignTx signs every input of the given MsgTx by looking up (on the addr -// manager) the redeem script for each of them and constructing the signature -// script using that and the given raw signatures. -// This function must be called with the manager unlocked. -func SignTx(msgtx *wire.MsgTx, sigs TxSigs, mgr *waddrmgr.Manager, addrmgrNs walletdb.ReadBucket, store *wtxmgr.Store, txmgrNs walletdb.ReadBucket) error { - // We use time.Now() here as we're not going to store the new TxRecord - // anywhere -- we just need it to pass to store.PreviousPkScripts(). - rec, err := wtxmgr.NewTxRecordFromMsgTx(msgtx, time.Now()) - if err != nil { - return newError(ErrTxSigning, "failed to construct TxRecord for signing", err) - } - pkScripts, err := store.PreviousPkScripts(txmgrNs, rec, nil) - if err != nil { - return newError(ErrTxSigning, "failed to obtain pkScripts for signing", err) - } - for i, pkScript := range pkScripts { - if err = signMultiSigUTXO(mgr, addrmgrNs, msgtx, i, pkScript, sigs[i]); err != nil { - return err - } - } - return nil -} - -// getRedeemScript returns the redeem script for the given P2SH address. It must -// be called with the manager unlocked. -func getRedeemScript(mgr *waddrmgr.Manager, addrmgrNs walletdb.ReadBucket, addr *btcutil.AddressScriptHash) ([]byte, error) { - address, err := mgr.Address(addrmgrNs, addr) - if err != nil { - return nil, err - } - return address.(waddrmgr.ManagedScriptAddress).Script() -} - -// signMultiSigUTXO signs the P2SH UTXO with the given index by constructing a -// script containing all given signatures plus the redeem (multi-sig) script. The -// redeem script is obtained by looking up the address of the given P2SH pkScript -// on the address manager. -// The order of the signatures must match that of the public keys in the multi-sig -// script as OP_CHECKMULTISIG expects that. -// This function must be called with the manager unlocked. -func signMultiSigUTXO(mgr *waddrmgr.Manager, addrmgrNs walletdb.ReadBucket, tx *wire.MsgTx, idx int, pkScript []byte, sigs []RawSig) error { - class, addresses, _, err := txscript.ExtractPkScriptAddrs(pkScript, mgr.ChainParams()) - if err != nil { - return newError(ErrTxSigning, "unparseable pkScript", err) - } - if class != txscript.ScriptHashTy { - return newError(ErrTxSigning, fmt.Sprintf("pkScript is not P2SH: %s", class), nil) - } - redeemScript, err := getRedeemScript(mgr, addrmgrNs, addresses[0].(*btcutil.AddressScriptHash)) - if err != nil { - return newError(ErrTxSigning, "unable to retrieve redeem script", err) - } - - class, _, nRequired, err := txscript.ExtractPkScriptAddrs(redeemScript, mgr.ChainParams()) - if err != nil { - return newError(ErrTxSigning, "unparseable redeem script", err) - } - if class != txscript.MultiSigTy { - return newError(ErrTxSigning, fmt.Sprintf("redeem script is not multi-sig: %v", class), nil) - } - if len(sigs) < nRequired { - errStr := fmt.Sprintf("not enough signatures; need %d but got only %d", nRequired, - len(sigs)) - return newError(ErrTxSigning, errStr, nil) - } - - // Construct the unlocking script. - // Start with an OP_0 because of the bug in bitcoind, then add nRequired signatures. - unlockingScript := txscript.NewScriptBuilder().AddOp(txscript.OP_FALSE) - for _, sig := range sigs[:nRequired] { - unlockingScript.AddData(sig) - } - - // Combine the redeem script and the unlocking script to get the actual signature script. - sigScript := unlockingScript.AddData(redeemScript) - script, err := sigScript.Script() - if err != nil { - return newError(ErrTxSigning, "error building sigscript", err) - } - tx.TxIn[idx].SignatureScript = script - - if err := validateSigScript(tx, idx, pkScript); err != nil { - return err - } - return nil -} - -// validateSigScripts executes the signature script of the tx input with the -// given index, returning an error if it fails. -func validateSigScript(msgtx *wire.MsgTx, idx int, pkScript []byte) error { - vm, err := txscript.NewEngine(pkScript, msgtx, idx, - txscript.StandardVerifyFlags, nil, nil, 0) - if err != nil { - return newError(ErrTxSigning, "cannot create script engine", err) - } - if err = vm.Execute(); err != nil { - return newError(ErrTxSigning, "cannot validate tx signature", err) - } - return nil -} - -// calculateTxSize returns an estimate of the serialized size (in bytes) of the -// given transaction. It assumes all tx inputs are P2SH multi-sig. -func calculateTxSize(tx *withdrawalTx) int { - msgtx := tx.toMsgTx() - // Assume that there will always be a change output, for simplicity. We - // simulate that by simply copying the first output as all we care about is - // the size of its serialized form, which should be the same for all of them - // as they're either P2PKH or P2SH.. - if !tx.hasChange() { - msgtx.AddTxOut(msgtx.TxOut[0]) - } - // Craft a SignatureScript with dummy signatures for every input in this tx - // so that we can use msgtx.SerializeSize() to get its size and don't need - // to rely on estimations. - for i, txin := range msgtx.TxIn { - // 1 byte for the OP_FALSE opcode, then 73+1 bytes for each signature - // with their OP_DATA opcode and finally the redeem script + 1 byte - // for its OP_PUSHDATA opcode and N bytes for the redeem script's size. - // Notice that we use 73 as the signature length as that's the maximum - // length they may have: - // https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm - addr := tx.inputs[i].addr - redeemScriptLen := len(addr.redeemScript()) - n := wire.VarIntSerializeSize(uint64(redeemScriptLen)) - sigScriptLen := 1 + (74 * int(addr.series().reqSigs)) + redeemScriptLen + 1 + n - txin.SignatureScript = bytes.Repeat([]byte{1}, sigScriptLen) - } - return msgtx.SerializeSize() -} - -func nextChangeAddress(a ChangeAddress) (ChangeAddress, error) { - index := a.index - seriesID := a.seriesID - if index == math.MaxUint32 { - index = 0 - seriesID++ - } else { - index++ - } - addr, err := a.pool.ChangeAddress(seriesID, index) - return *addr, err -} - -func storeTransactions(store *wtxmgr.Store, txmgrNs walletdb.ReadWriteBucket, transactions []*changeAwareTx) error { - for _, tx := range transactions { - if err := tx.addSelfToStore(store, txmgrNs); err != nil { - return err - } - } - return nil -} diff --git a/votingpool/withdrawal_test.go b/votingpool/withdrawal_test.go deleted file mode 100644 index b3a3b49..0000000 --- a/votingpool/withdrawal_test.go +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) 2014 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package votingpool_test - -import ( - "bytes" - "testing" - - "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcutil/hdkeychain" - vp "github.com/btcsuite/btcwallet/votingpool" -) - -func TestStartWithdrawal(t *testing.T) { - tearDown, db, pool, store := vp.TstCreatePoolAndTxStore(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, addrmgrNs := vp.TstRWNamespaces(dbtx) - txmgrNs := vp.TstTxStoreRWNamespace(dbtx) - - mgr := pool.Manager() - - masters := []*hdkeychain.ExtendedKey{ - vp.TstCreateMasterKey(t, bytes.Repeat([]byte{0x00, 0x01}, 16)), - vp.TstCreateMasterKey(t, bytes.Repeat([]byte{0x02, 0x01}, 16)), - vp.TstCreateMasterKey(t, bytes.Repeat([]byte{0x03, 0x01}, 16))} - def := vp.TstCreateSeriesDef(t, pool, 2, masters) - vp.TstCreateSeries(t, dbtx, pool, []vp.TstSeriesDef{def}) - // Create eligible inputs and the list of outputs we need to fulfil. - vp.TstCreateSeriesCreditsOnStore(t, dbtx, pool, def.SeriesID, []int64{5e6, 4e6}, store) - address1 := "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6" - address2 := "3PbExiaztsSYgh6zeMswC49hLUwhTQ86XG" - requests := []vp.OutputRequest{ - vp.TstNewOutputRequest(t, 1, address1, 4e6, mgr.ChainParams()), - vp.TstNewOutputRequest(t, 2, address2, 1e6, mgr.ChainParams()), - } - changeStart := vp.TstNewChangeAddress(t, pool, def.SeriesID, 0) - - startAddr := vp.TstNewWithdrawalAddress(t, dbtx, pool, def.SeriesID, 0, 0) - lastSeriesID := def.SeriesID - dustThreshold := btcutil.Amount(1e4) - currentBlock := int32(vp.TstInputsBlock + vp.TstEligibleInputMinConfirmations + 1) - var status *vp.WithdrawalStatus - vp.TstRunWithManagerUnlocked(t, mgr, addrmgrNs, func() { - status, err = pool.StartWithdrawal(ns, addrmgrNs, 0, requests, *startAddr, lastSeriesID, *changeStart, - store, txmgrNs, currentBlock, dustThreshold) - }) - if err != nil { - t.Fatal(err) - } - - // Check that all outputs were successfully fulfilled. - checkWithdrawalOutputs(t, status, map[string]btcutil.Amount{address1: 4e6, address2: 1e6}) - - if status.Fees() != btcutil.Amount(1e3) { - t.Fatalf("Wrong amount for fees; got %v, want %v", status.Fees(), btcutil.Amount(1e3)) - } - - // This withdrawal generated a single transaction with just one change - // output, so the next change address will be on the same series with the - // index incremented by 1. - nextChangeAddr := status.NextChangeAddr() - if nextChangeAddr.SeriesID() != changeStart.SeriesID() { - t.Fatalf("Wrong nextChangeStart series; got %d, want %d", nextChangeAddr.SeriesID(), - changeStart.SeriesID()) - } - if nextChangeAddr.Index() != changeStart.Index()+1 { - t.Fatalf("Wrong nextChangeStart index; got %d, want %d", nextChangeAddr.Index(), - changeStart.Index()+1) - } - - // NOTE: The ntxid is deterministic so we hardcode it here, but if the test - // or the code is changed in a way that causes the generated transaction to - // change (e.g. different inputs/outputs), the ntxid will change too and - // this will have to be updated. - ntxid := vp.Ntxid("eb753083db55bd0ad2eb184bfd196a7ea8b90eaa000d9293e892999695af2519") - txSigs := status.Sigs()[ntxid] - - // Finally we use SignTx() to construct the SignatureScripts (using the raw - // signatures). Must unlock the manager as signing involves looking up the - // redeem script, which is stored encrypted. - msgtx := status.TstGetMsgTx(ntxid) - vp.TstRunWithManagerUnlocked(t, mgr, addrmgrNs, func() { - if err = vp.SignTx(msgtx, txSigs, mgr, addrmgrNs, store, txmgrNs); err != nil { - t.Fatal(err) - } - }) - - // Any subsequent StartWithdrawal() calls with the same parameters will - // return the previously stored WithdrawalStatus. - var status2 *vp.WithdrawalStatus - vp.TstRunWithManagerUnlocked(t, mgr, addrmgrNs, func() { - status2, err = pool.StartWithdrawal(ns, addrmgrNs, 0, requests, *startAddr, lastSeriesID, *changeStart, - store, txmgrNs, currentBlock, dustThreshold) - }) - if err != nil { - t.Fatal(err) - } - vp.TstCheckWithdrawalStatusMatches(t, *status, *status2) -} - -func checkWithdrawalOutputs( - t *testing.T, wStatus *vp.WithdrawalStatus, amounts map[string]btcutil.Amount) { - fulfilled := wStatus.Outputs() - if len(fulfilled) != 2 { - t.Fatalf("Unexpected number of outputs in WithdrawalStatus; got %d, want %d", - len(fulfilled), 2) - } - for _, output := range fulfilled { - addr := output.Address() - amount, ok := amounts[addr] - if !ok { - t.Fatalf("Unexpected output addr: %s", addr) - } - - status := output.Status() - if status != "success" { - t.Fatalf( - "Unexpected status for output %v; got '%s', want 'success'", output, status) - } - - outpoints := output.Outpoints() - if len(outpoints) != 1 { - t.Fatalf( - "Unexpected number of outpoints for output %v; got %d, want 1", output, - len(outpoints)) - } - - gotAmount := outpoints[0].Amount() - if gotAmount != amount { - t.Fatalf("Unexpected amount for output %v; got %v, want %v", output, gotAmount, amount) - } - } -} diff --git a/votingpool/withdrawal_wb_test.go b/votingpool/withdrawal_wb_test.go deleted file mode 100644 index 4cb009a..0000000 --- a/votingpool/withdrawal_wb_test.go +++ /dev/null @@ -1,1594 +0,0 @@ -// Copyright (c) 2015-2017 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package votingpool - -import ( - "bytes" - "reflect" - "sort" - "testing" - - "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcutil/hdkeychain" - "github.com/btcsuite/btcwallet/waddrmgr" - "github.com/btcsuite/btcwallet/walletdb" - "github.com/btcsuite/btcwallet/wtxmgr" -) - -// TestOutputSplittingNotEnoughInputs checks that an output will get split if we -// don't have enough inputs to fulfil it. -func TestOutputSplittingNotEnoughInputs(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - net := pool.Manager().ChainParams() - output1Amount := btcutil.Amount(2) - output2Amount := btcutil.Amount(3) - requests := []OutputRequest{ - // These output requests will have the same server ID, so we know - // they'll be fulfilled in the order they're defined here, which is - // important for this test. - TstNewOutputRequest(t, 1, "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", output1Amount, net), - TstNewOutputRequest(t, 2, "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", output2Amount, net), - } - seriesID, eligible := TstCreateCreditsOnNewSeries(t, dbtx, pool, []int64{7}) - w := newWithdrawal(0, requests, eligible, *TstNewChangeAddress(t, pool, seriesID, 0)) - w.txOptions = func(tx *withdrawalTx) { - // Trigger an output split because of lack of inputs by forcing a high fee. - // If we just started with not enough inputs for the requested outputs, - // fulfillRequests() would drop outputs until we had enough. - tx.calculateFee = TstConstantFee(3) - } - - if err := w.fulfillRequests(); err != nil { - t.Fatal(err) - } - - if len(w.transactions) != 1 { - t.Fatalf("Wrong number of finalized transactions; got %d, want 1", len(w.transactions)) - } - - tx := w.transactions[0] - if len(tx.outputs) != 2 { - t.Fatalf("Wrong number of outputs; got %d, want 2", len(tx.outputs)) - } - - // The first output should've been left untouched. - if tx.outputs[0].amount != output1Amount { - t.Fatalf("Wrong amount for first tx output; got %v, want %v", - tx.outputs[0].amount, output1Amount) - } - - // The last output should have had its amount updated to whatever we had - // left after satisfying all previous outputs. - newAmount := tx.inputTotal() - output1Amount - tx.calculateFee() - checkLastOutputWasSplit(t, w, tx, output2Amount, newAmount) -} - -func TestOutputSplittingOversizeTx(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - requestAmount := btcutil.Amount(5) - bigInput := int64(3) - smallInput := int64(2) - request := TstNewOutputRequest( - t, 1, "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", requestAmount, pool.Manager().ChainParams()) - seriesID, eligible := TstCreateCreditsOnNewSeries(t, dbtx, pool, []int64{smallInput, bigInput}) - changeStart := TstNewChangeAddress(t, pool, seriesID, 0) - w := newWithdrawal(0, []OutputRequest{request}, eligible, *changeStart) - w.txOptions = func(tx *withdrawalTx) { - tx.calculateFee = TstConstantFee(0) - tx.calculateSize = func() int { - // Trigger an output split right after the second input is added. - if len(tx.inputs) == 2 { - return txMaxSize + 1 - } - return txMaxSize - 1 - } - } - - if err := w.fulfillRequests(); err != nil { - t.Fatal(err) - } - - if len(w.transactions) != 2 { - t.Fatalf("Wrong number of finalized transactions; got %d, want 2", len(w.transactions)) - } - - tx1 := w.transactions[0] - if len(tx1.outputs) != 1 { - t.Fatalf("Wrong number of outputs on tx1; got %d, want 1", len(tx1.outputs)) - } - if tx1.outputs[0].amount != btcutil.Amount(bigInput) { - t.Fatalf("Wrong amount for output in tx1; got %d, want %d", tx1.outputs[0].amount, - bigInput) - } - - tx2 := w.transactions[1] - if len(tx2.outputs) != 1 { - t.Fatalf("Wrong number of outputs on tx2; got %d, want 1", len(tx2.outputs)) - } - if tx2.outputs[0].amount != btcutil.Amount(smallInput) { - t.Fatalf("Wrong amount for output in tx2; got %d, want %d", tx2.outputs[0].amount, - smallInput) - } - - if len(w.status.outputs) != 1 { - t.Fatalf("Wrong number of output statuses; got %d, want 1", len(w.status.outputs)) - } - status := w.status.outputs[request.outBailmentID()].status - if status != statusSplit { - t.Fatalf("Wrong output status; got '%s', want '%s'", status, statusSplit) - } -} - -func TestSplitLastOutputNoOutputs(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - w := newWithdrawal(0, []OutputRequest{}, []credit{}, ChangeAddress{}) - w.current = createWithdrawalTx(t, dbtx, pool, []int64{}, []int64{}) - - err = w.splitLastOutput() - - TstCheckError(t, "", err, ErrPreconditionNotMet) -} - -// Check that all outputs requested in a withdrawal match the outputs of the generated -// transaction(s). -func TestWithdrawalTxOutputs(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - net := pool.Manager().ChainParams() - - // Create eligible inputs and the list of outputs we need to fulfil. - seriesID, eligible := TstCreateCreditsOnNewSeries(t, dbtx, pool, []int64{2e6, 4e6}) - outputs := []OutputRequest{ - TstNewOutputRequest(t, 1, "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", 3e6, net), - TstNewOutputRequest(t, 2, "3PbExiaztsSYgh6zeMswC49hLUwhTQ86XG", 2e6, net), - } - changeStart := TstNewChangeAddress(t, pool, seriesID, 0) - - w := newWithdrawal(0, outputs, eligible, *changeStart) - if err := w.fulfillRequests(); err != nil { - t.Fatal(err) - } - - if len(w.transactions) != 1 { - t.Fatalf("Unexpected number of transactions; got %d, want 1", len(w.transactions)) - } - - tx := w.transactions[0] - // The created tx should include both eligible credits, so we expect it to have - // an input amount of 2e6+4e6 satoshis. - inputAmount := eligible[0].Amount + eligible[1].Amount - change := inputAmount - (outputs[0].Amount + outputs[1].Amount + tx.calculateFee()) - expectedOutputs := append( - outputs, TstNewOutputRequest(t, 3, changeStart.addr.String(), change, net)) - msgtx := tx.toMsgTx() - checkMsgTxOutputs(t, msgtx, expectedOutputs) -} - -// Check that withdrawal.status correctly states that no outputs were fulfilled when we -// don't have enough eligible credits for any of them. -func TestFulfillRequestsNoSatisfiableOutputs(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - seriesID, eligible := TstCreateCreditsOnNewSeries(t, dbtx, pool, []int64{1e6}) - request := TstNewOutputRequest( - t, 1, "3Qt1EaKRD9g9FeL2DGkLLswhK1AKmmXFSe", btcutil.Amount(3e6), pool.Manager().ChainParams()) - changeStart := TstNewChangeAddress(t, pool, seriesID, 0) - - w := newWithdrawal(0, []OutputRequest{request}, eligible, *changeStart) - if err := w.fulfillRequests(); err != nil { - t.Fatal(err) - } - - if len(w.transactions) != 0 { - t.Fatalf("Unexpected number of transactions; got %d, want 0", len(w.transactions)) - } - - if len(w.status.outputs) != 1 { - t.Fatalf("Unexpected number of outputs in WithdrawalStatus; got %d, want 1", - len(w.status.outputs)) - } - - status := w.status.outputs[request.outBailmentID()].status - if status != statusPartial { - t.Fatalf("Unexpected status for requested outputs; got '%s', want '%s'", - status, statusPartial) - } -} - -// Check that some requested outputs are not fulfilled when we don't have credits for all -// of them. -func TestFulfillRequestsNotEnoughCreditsForAllRequests(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - net := pool.Manager().ChainParams() - - // Create eligible inputs and the list of outputs we need to fulfil. - seriesID, eligible := TstCreateCreditsOnNewSeries(t, dbtx, pool, []int64{2e6, 4e6}) - out1 := TstNewOutputRequest( - t, 1, "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", btcutil.Amount(3e6), net) - out2 := TstNewOutputRequest( - t, 2, "3PbExiaztsSYgh6zeMswC49hLUwhTQ86XG", btcutil.Amount(2e6), net) - out3 := TstNewOutputRequest( - t, 3, "3Qt1EaKRD9g9FeL2DGkLLswhK1AKmmXFSe", btcutil.Amount(5e6), net) - outputs := []OutputRequest{out1, out2, out3} - changeStart := TstNewChangeAddress(t, pool, seriesID, 0) - - w := newWithdrawal(0, outputs, eligible, *changeStart) - if err := w.fulfillRequests(); err != nil { - t.Fatal(err) - } - - tx := w.transactions[0] - // The created tx should spend both eligible credits, so we expect it to have - // an input amount of 2e6+4e6 satoshis. - inputAmount := eligible[0].Amount + eligible[1].Amount - // We expect it to include outputs for requests 1 and 2, plus a change output, but - // output request #3 should not be there because we don't have enough credits. - change := inputAmount - (out1.Amount + out2.Amount + tx.calculateFee()) - expectedOutputs := []OutputRequest{out1, out2} - sort.Sort(byOutBailmentID(expectedOutputs)) - expectedOutputs = append( - expectedOutputs, TstNewOutputRequest(t, 4, changeStart.addr.String(), change, net)) - msgtx := tx.toMsgTx() - checkMsgTxOutputs(t, msgtx, expectedOutputs) - - // withdrawal.status should state that outputs 1 and 2 were successfully fulfilled, - // and that output 3 was not. - expectedStatuses := map[OutBailmentID]outputStatus{ - out1.outBailmentID(): statusSuccess, - out2.outBailmentID(): statusSuccess, - out3.outBailmentID(): statusPartial} - for _, wOutput := range w.status.outputs { - if wOutput.status != expectedStatuses[wOutput.request.outBailmentID()] { - t.Fatalf("Unexpected status for %v; got '%s', want '%s'", wOutput.request, - wOutput.status, expectedStatuses[wOutput.request.outBailmentID()]) - } - } -} - -// TestRollbackLastOutput tests the case where we rollback one output -// and one input, such that sum(in) >= sum(out) + fee. -func TestRollbackLastOutput(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - tx := createWithdrawalTx(t, dbtx, pool, []int64{3, 3, 2, 1, 3}, []int64{3, 3, 2, 2}) - initialInputs := tx.inputs - initialOutputs := tx.outputs - - tx.calculateFee = TstConstantFee(1) - removedInputs, removedOutput, err := tx.rollBackLastOutput() - if err != nil { - t.Fatal("Unexpected error:", err) - } - - // The above rollBackLastOutput() call should have removed the last output - // and the last input. - lastOutput := initialOutputs[len(initialOutputs)-1] - if removedOutput != lastOutput { - t.Fatalf("Wrong rolled back output; got %s want %s", removedOutput, lastOutput) - } - if len(removedInputs) != 1 { - t.Fatalf("Unexpected number of inputs removed; got %d, want 1", len(removedInputs)) - } - lastInput := initialInputs[len(initialInputs)-1] - if !reflect.DeepEqual(removedInputs[0], lastInput) { - t.Fatalf("Wrong rolled back input; got %v want %v", removedInputs[0], lastInput) - } - - // Now check that the inputs and outputs left in the tx match what we - // expect. - checkTxOutputs(t, tx, initialOutputs[:len(initialOutputs)-1]) - checkTxInputs(t, tx, initialInputs[:len(initialInputs)-1]) -} - -func TestRollbackLastOutputMultipleInputsRolledBack(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - // This tx will need the 3 last inputs to fulfill the second output, so they - // should all be rolled back and returned in the reverse order they were added. - tx := createWithdrawalTx(t, dbtx, pool, []int64{1, 2, 3, 4}, []int64{1, 8}) - initialInputs := tx.inputs - initialOutputs := tx.outputs - - tx.calculateFee = TstConstantFee(0) - removedInputs, _, err := tx.rollBackLastOutput() - if err != nil { - t.Fatal("Unexpected error:", err) - } - - if len(removedInputs) != 3 { - t.Fatalf("Unexpected number of inputs removed; got %d, want 3", len(removedInputs)) - } - for i, amount := range []btcutil.Amount{4, 3, 2} { - if removedInputs[i].Amount != amount { - t.Fatalf("Unexpected input amount; got %v, want %v", removedInputs[i].Amount, amount) - } - } - - // Now check that the inputs and outputs left in the tx match what we - // expect. - checkTxOutputs(t, tx, initialOutputs[:len(initialOutputs)-1]) - checkTxInputs(t, tx, initialInputs[:len(initialInputs)-len(removedInputs)]) -} - -// TestRollbackLastOutputNoInputsRolledBack tests the case where we roll back -// one output but don't need to roll back any inputs. -func TestRollbackLastOutputNoInputsRolledBack(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - tx := createWithdrawalTx(t, dbtx, pool, []int64{4}, []int64{2, 3}) - initialInputs := tx.inputs - initialOutputs := tx.outputs - - tx.calculateFee = TstConstantFee(1) - removedInputs, removedOutput, err := tx.rollBackLastOutput() - if err != nil { - t.Fatal("Unexpected error:", err) - } - - // The above rollBackLastOutput() call should have removed the - // last output but no inputs. - lastOutput := initialOutputs[len(initialOutputs)-1] - if removedOutput != lastOutput { - t.Fatalf("Wrong output; got %s want %s", removedOutput, lastOutput) - } - if len(removedInputs) != 0 { - t.Fatalf("Expected no removed inputs, but got %d inputs", len(removedInputs)) - } - - // Now check that the inputs and outputs left in the tx match what we - // expect. - checkTxOutputs(t, tx, initialOutputs[:len(initialOutputs)-1]) - checkTxInputs(t, tx, initialInputs) -} - -// TestRollBackLastOutputInsufficientOutputs checks that -// rollBackLastOutput returns an error if there are less than two -// outputs in the transaction. -func TestRollBackLastOutputInsufficientOutputs(t *testing.T) { - tx := newWithdrawalTx(defaultTxOptions) - _, _, err := tx.rollBackLastOutput() - TstCheckError(t, "", err, ErrPreconditionNotMet) - - output := &WithdrawalOutput{request: TstNewOutputRequest( - t, 1, "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", btcutil.Amount(3), &chaincfg.MainNetParams)} - tx.addOutput(output.request) - _, _, err = tx.rollBackLastOutput() - TstCheckError(t, "", err, ErrPreconditionNotMet) -} - -// TestRollbackLastOutputWhenNewOutputAdded checks that we roll back the last -// output if a tx becomes too big right after we add a new output to it. -func TestRollbackLastOutputWhenNewOutputAdded(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - net := pool.Manager().ChainParams() - series, eligible := TstCreateCreditsOnNewSeries(t, dbtx, pool, []int64{5, 5}) - requests := []OutputRequest{ - // This is ordered by bailment ID - TstNewOutputRequest(t, 1, "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", 1, net), - TstNewOutputRequest(t, 2, "3PbExiaztsSYgh6zeMswC49hLUwhTQ86XG", 2, net), - } - changeStart := TstNewChangeAddress(t, pool, series, 0) - - w := newWithdrawal(0, requests, eligible, *changeStart) - w.txOptions = func(tx *withdrawalTx) { - tx.calculateFee = TstConstantFee(0) - tx.calculateSize = func() int { - // Trigger an output split right after the second output is added. - if len(tx.outputs) > 1 { - return txMaxSize + 1 - } - return txMaxSize - 1 - } - } - - if err := w.fulfillRequests(); err != nil { - t.Fatal("Unexpected error:", err) - } - - // At this point we should have two finalized transactions. - if len(w.transactions) != 2 { - t.Fatalf("Wrong number of finalized transactions; got %d, want 2", len(w.transactions)) - } - - // First tx should have one output with 1 and one change output with 4 - // satoshis. - firstTx := w.transactions[0] - req1 := requests[0] - checkTxOutputs(t, firstTx, - []*withdrawalTxOut{{request: req1, amount: req1.Amount}}) - checkTxChangeAmount(t, firstTx, btcutil.Amount(4)) - - // Second tx should have one output with 2 and one changeoutput with 3 satoshis. - secondTx := w.transactions[1] - req2 := requests[1] - checkTxOutputs(t, secondTx, - []*withdrawalTxOut{{request: req2, amount: req2.Amount}}) - checkTxChangeAmount(t, secondTx, btcutil.Amount(3)) -} - -// TestRollbackLastOutputWhenNewInputAdded checks that we roll back the last -// output if a tx becomes too big right after we add a new input to it. -func TestRollbackLastOutputWhenNewInputAdded(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - net := pool.Manager().ChainParams() - series, eligible := TstCreateCreditsOnNewSeries(t, dbtx, pool, []int64{6, 5, 4, 3, 2, 1}) - requests := []OutputRequest{ - // This is manually ordered by outBailmentIDHash, which is the order in - // which they're going to be fulfilled by w.fulfillRequests(). - TstNewOutputRequest(t, 1, "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", 1, net), - TstNewOutputRequest(t, 3, "3Qt1EaKRD9g9FeL2DGkLLswhK1AKmmXFSe", 6, net), - TstNewOutputRequest(t, 2, "3PbExiaztsSYgh6zeMswC49hLUwhTQ86XG", 3, net), - } - changeStart := TstNewChangeAddress(t, pool, series, 0) - - w := newWithdrawal(0, requests, eligible, *changeStart) - w.txOptions = func(tx *withdrawalTx) { - tx.calculateFee = TstConstantFee(0) - tx.calculateSize = func() int { - // Make a transaction too big as soon as a fourth input is added to it. - if len(tx.inputs) > 3 { - return txMaxSize + 1 - } - return txMaxSize - 1 - } - } - - // The rollback should be triggered right after the 4th input is added in - // order to fulfill the second request. - if err := w.fulfillRequests(); err != nil { - t.Fatal("Unexpected error:", err) - } - - // At this point we should have two finalized transactions. - if len(w.transactions) != 2 { - t.Fatalf("Wrong number of finalized transactions; got %d, want 2", len(w.transactions)) - } - - // First tx should have one output with amount of 1, the first input from - // the stack of eligible inputs (last slice item), and no change output. - firstTx := w.transactions[0] - req1 := requests[0] - checkTxOutputs(t, firstTx, - []*withdrawalTxOut{{request: req1, amount: req1.Amount}}) - checkTxInputs(t, firstTx, eligible[5:6]) - - // Second tx should have outputs for the two last requests (in the same - // order they were passed to newWithdrawal), and the 3 inputs needed to - // fulfill that (in reverse order as they were passed to newWithdrawal, as - // that's how fulfillRequests() consumes them) and no change output. - secondTx := w.transactions[1] - wantOutputs := []*withdrawalTxOut{ - {request: requests[1], amount: requests[1].Amount}, - {request: requests[2], amount: requests[2].Amount}} - checkTxOutputs(t, secondTx, wantOutputs) - wantInputs := []credit{eligible[4], eligible[3], eligible[2]} - checkTxInputs(t, secondTx, wantInputs) -} - -func TestWithdrawalTxRemoveOutput(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - tx := createWithdrawalTx(t, dbtx, pool, []int64{}, []int64{1, 2}) - outputs := tx.outputs - // Make sure we have created the transaction with the expected - // outputs. - checkTxOutputs(t, tx, outputs) - - remainingOutput := tx.outputs[0] - wantRemovedOutput := tx.outputs[1] - - gotRemovedOutput := tx.removeOutput() - - // Check the popped output looks correct. - if gotRemovedOutput != wantRemovedOutput { - t.Fatalf("Removed output wrong; got %v, want %v", gotRemovedOutput, wantRemovedOutput) - } - // And that the remaining output is correct. - checkTxOutputs(t, tx, []*withdrawalTxOut{remainingOutput}) - - // Make sure that the remaining output is really the right one. - if tx.outputs[0] != remainingOutput { - t.Fatalf("Wrong output: got %v, want %v", tx.outputs[0], remainingOutput) - } -} - -func TestWithdrawalTxRemoveInput(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - tx := createWithdrawalTx(t, dbtx, pool, []int64{1, 2}, []int64{}) - inputs := tx.inputs - // Make sure we have created the transaction with the expected inputs - checkTxInputs(t, tx, inputs) - - remainingInput := tx.inputs[0] - wantRemovedInput := tx.inputs[1] - - gotRemovedInput := tx.removeInput() - - // Check the popped input looks correct. - if !reflect.DeepEqual(gotRemovedInput, wantRemovedInput) { - t.Fatalf("Popped input wrong; got %v, want %v", gotRemovedInput, wantRemovedInput) - } - checkTxInputs(t, tx, inputs[0:1]) - - // Make sure that the remaining input is really the right one. - if !reflect.DeepEqual(tx.inputs[0], remainingInput) { - t.Fatalf("Wrong input: got %v, want %v", tx.inputs[0], remainingInput) - } -} - -func TestWithdrawalTxAddChange(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - input, output, fee := int64(4e6), int64(3e6), int64(10) - tx := createWithdrawalTx(t, dbtx, pool, []int64{input}, []int64{output}) - tx.calculateFee = TstConstantFee(btcutil.Amount(fee)) - - if !tx.addChange([]byte{}) { - t.Fatal("tx.addChange() returned false, meaning it did not add a change output") - } - - msgtx := tx.toMsgTx() - if len(msgtx.TxOut) != 2 { - t.Fatalf("Unexpected number of txouts; got %d, want 2", len(msgtx.TxOut)) - } - gotChange := msgtx.TxOut[1].Value - wantChange := input - output - fee - if gotChange != wantChange { - t.Fatalf("Unexpected change amount; got %v, want %v", gotChange, wantChange) - } -} - -// TestWithdrawalTxAddChangeNoChange checks that withdrawalTx.addChange() does not -// add a change output when there's no satoshis left after paying all -// outputs+fees. -func TestWithdrawalTxAddChangeNoChange(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - input, output, fee := int64(4e6), int64(4e6), int64(0) - tx := createWithdrawalTx(t, dbtx, pool, []int64{input}, []int64{output}) - tx.calculateFee = TstConstantFee(btcutil.Amount(fee)) - - if tx.addChange([]byte{}) { - t.Fatal("tx.addChange() returned true, meaning it added a change output") - } - msgtx := tx.toMsgTx() - if len(msgtx.TxOut) != 1 { - t.Fatalf("Unexpected number of txouts; got %d, want 1", len(msgtx.TxOut)) - } -} - -func TestWithdrawalTxToMsgTxNoInputsOrOutputsOrChange(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - tx := createWithdrawalTx(t, dbtx, pool, []int64{}, []int64{}) - msgtx := tx.toMsgTx() - compareMsgTxAndWithdrawalTxOutputs(t, msgtx, tx) - compareMsgTxAndWithdrawalTxInputs(t, msgtx, tx) -} - -func TestWithdrawalTxToMsgTxNoInputsOrOutputsWithChange(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - tx := createWithdrawalTx(t, dbtx, pool, []int64{}, []int64{}) - tx.changeOutput = wire.NewTxOut(int64(1), []byte{}) - - msgtx := tx.toMsgTx() - - compareMsgTxAndWithdrawalTxOutputs(t, msgtx, tx) - compareMsgTxAndWithdrawalTxInputs(t, msgtx, tx) -} - -func TestWithdrawalTxToMsgTxWithInputButNoOutputsWithChange(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - tx := createWithdrawalTx(t, dbtx, pool, []int64{1}, []int64{}) - tx.changeOutput = wire.NewTxOut(int64(1), []byte{}) - - msgtx := tx.toMsgTx() - - compareMsgTxAndWithdrawalTxOutputs(t, msgtx, tx) - compareMsgTxAndWithdrawalTxInputs(t, msgtx, tx) -} - -func TestWithdrawalTxToMsgTxWithInputOutputsAndChange(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - tx := createWithdrawalTx(t, dbtx, pool, []int64{1, 2, 3}, []int64{4, 5, 6}) - tx.changeOutput = wire.NewTxOut(int64(7), []byte{}) - - msgtx := tx.toMsgTx() - - compareMsgTxAndWithdrawalTxOutputs(t, msgtx, tx) - compareMsgTxAndWithdrawalTxInputs(t, msgtx, tx) -} - -func TestWithdrawalTxInputTotal(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - tx := createWithdrawalTx(t, dbtx, pool, []int64{5}, []int64{}) - - if tx.inputTotal() != btcutil.Amount(5) { - t.Fatalf("Wrong total output; got %v, want %v", tx.outputTotal(), btcutil.Amount(5)) - } -} - -func TestWithdrawalTxOutputTotal(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - tx := createWithdrawalTx(t, dbtx, pool, []int64{}, []int64{4}) - tx.changeOutput = wire.NewTxOut(int64(1), []byte{}) - - if tx.outputTotal() != btcutil.Amount(4) { - t.Fatalf("Wrong total output; got %v, want %v", tx.outputTotal(), btcutil.Amount(4)) - } -} - -func TestWithdrawalInfoMatch(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - roundID := uint32(0) - wi := createAndFulfillWithdrawalRequests(t, dbtx, pool, roundID) - - // Use freshly created values for requests, startAddress and changeStart - // to simulate what would happen if we had recreated them from the - // serialized data in the DB. - requestsCopy := make([]OutputRequest, len(wi.requests)) - copy(requestsCopy, wi.requests) - startAddr := TstNewWithdrawalAddress(t, dbtx, pool, wi.startAddress.seriesID, wi.startAddress.branch, - wi.startAddress.index) - changeStart := TstNewChangeAddress(t, pool, wi.changeStart.seriesID, wi.changeStart.index) - - // First check that it matches when all fields are identical. - matches := wi.match(requestsCopy, *startAddr, wi.lastSeriesID, *changeStart, wi.dustThreshold) - if !matches { - t.Fatal("Should match when everything is identical.") - } - - // It also matches if the OutputRequests are not in the same order. - diffOrderRequests := make([]OutputRequest, len(requestsCopy)) - copy(diffOrderRequests, requestsCopy) - diffOrderRequests[0], diffOrderRequests[1] = requestsCopy[1], requestsCopy[0] - matches = wi.match(diffOrderRequests, *startAddr, wi.lastSeriesID, *changeStart, - wi.dustThreshold) - if !matches { - t.Fatal("Should match when requests are in different order.") - } - - // It should not match when the OutputRequests are not the same. - diffRequests := diffOrderRequests - diffRequests[0] = OutputRequest{} - matches = wi.match(diffRequests, *startAddr, wi.lastSeriesID, *changeStart, wi.dustThreshold) - if matches { - t.Fatal("Should not match as requests is not equal.") - } - - // It should not match when lastSeriesID is not equal. - matches = wi.match(requestsCopy, *startAddr, wi.lastSeriesID+1, *changeStart, wi.dustThreshold) - if matches { - t.Fatal("Should not match as lastSeriesID is not equal.") - } - - // It should not match when dustThreshold is not equal. - matches = wi.match(requestsCopy, *startAddr, wi.lastSeriesID, *changeStart, wi.dustThreshold+1) - if matches { - t.Fatal("Should not match as dustThreshold is not equal.") - } - - // It should not match when startAddress is not equal. - diffStartAddr := TstNewWithdrawalAddress(t, dbtx, pool, startAddr.seriesID, startAddr.branch+1, - startAddr.index) - matches = wi.match(requestsCopy, *diffStartAddr, wi.lastSeriesID, *changeStart, - wi.dustThreshold) - if matches { - t.Fatal("Should not match as startAddress is not equal.") - } - - // It should not match when changeStart is not equal. - diffChangeStart := TstNewChangeAddress(t, pool, changeStart.seriesID, changeStart.index+1) - matches = wi.match(requestsCopy, *startAddr, wi.lastSeriesID, *diffChangeStart, - wi.dustThreshold) - if matches { - t.Fatal("Should not match as changeStart is not equal.") - } -} - -func TestGetWithdrawalStatus(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - ns, addrmgrNs := TstRWNamespaces(dbtx) - - roundID := uint32(0) - wi := createAndFulfillWithdrawalRequests(t, dbtx, pool, roundID) - - serialized, err := serializeWithdrawal(wi.requests, wi.startAddress, wi.lastSeriesID, - wi.changeStart, wi.dustThreshold, wi.status) - if err != nil { - t.Fatal(err) - } - err = putWithdrawal(ns, pool.ID, roundID, serialized) - if err != nil { - t.Fatal(err) - } - - // Here we should get a WithdrawalStatus that matches wi.status. - var status *WithdrawalStatus - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - status, err = getWithdrawalStatus(pool, ns, addrmgrNs, roundID, wi.requests, wi.startAddress, - wi.lastSeriesID, wi.changeStart, wi.dustThreshold) - }) - if err != nil { - t.Fatal(err) - } - TstCheckWithdrawalStatusMatches(t, wi.status, *status) - - // Here we should get a nil WithdrawalStatus because the parameters are not - // identical to those of the stored WithdrawalStatus with this roundID. - dustThreshold := wi.dustThreshold + 1 - TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() { - status, err = getWithdrawalStatus(pool, ns, addrmgrNs, roundID, wi.requests, wi.startAddress, - wi.lastSeriesID, wi.changeStart, dustThreshold) - }) - if err != nil { - t.Fatal(err) - } - if status != nil { - t.Fatalf("Expected a nil status, got %v", status) - } -} - -func TestSignMultiSigUTXO(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - _, addrmgrNs := TstRWNamespaces(dbtx) - - // Create a new tx with a single input that we're going to sign. - mgr := pool.Manager() - tx := createWithdrawalTx(t, dbtx, pool, []int64{4e6}, []int64{4e6}) - sigs, err := getRawSigs([]*withdrawalTx{tx}) - if err != nil { - t.Fatal(err) - } - - msgtx := tx.toMsgTx() - txSigs := sigs[tx.ntxid()] - - idx := 0 // The index of the tx input we're going to sign. - pkScript := tx.inputs[idx].PkScript - TstRunWithManagerUnlocked(t, mgr, addrmgrNs, func() { - if err = signMultiSigUTXO(mgr, addrmgrNs, msgtx, idx, pkScript, txSigs[idx]); err != nil { - t.Fatal(err) - } - }) -} - -func TestSignMultiSigUTXOUnparseablePkScript(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - _, addrmgrNs := TstRWNamespaces(dbtx) - - mgr := pool.Manager() - tx := createWithdrawalTx(t, dbtx, pool, []int64{4e6}, []int64{}) - msgtx := tx.toMsgTx() - - unparseablePkScript := []byte{0x01} - err = signMultiSigUTXO(mgr, addrmgrNs, msgtx, 0, unparseablePkScript, []RawSig{{}}) - - TstCheckError(t, "", err, ErrTxSigning) -} - -func TestSignMultiSigUTXOPkScriptNotP2SH(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - _, addrmgrNs := TstRWNamespaces(dbtx) - - mgr := pool.Manager() - tx := createWithdrawalTx(t, dbtx, pool, []int64{4e6}, []int64{}) - addr, _ := btcutil.DecodeAddress("1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX", mgr.ChainParams()) - pubKeyHashPkScript, _ := txscript.PayToAddrScript(addr.(*btcutil.AddressPubKeyHash)) - msgtx := tx.toMsgTx() - - err = signMultiSigUTXO(mgr, addrmgrNs, msgtx, 0, pubKeyHashPkScript, []RawSig{{}}) - - TstCheckError(t, "", err, ErrTxSigning) -} - -func TestSignMultiSigUTXORedeemScriptNotFound(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - _, addrmgrNs := TstRWNamespaces(dbtx) - - mgr := pool.Manager() - tx := createWithdrawalTx(t, dbtx, pool, []int64{4e6}, []int64{}) - // This is a P2SH address for which the addr manager doesn't have the redeem - // script. - addr, _ := btcutil.DecodeAddress("3Hb4xcebcKg4DiETJfwjh8sF4uDw9rqtVC", mgr.ChainParams()) - if _, err := mgr.Address(addrmgrNs, addr); err == nil { - t.Fatalf("Address %s found in manager when it shouldn't", addr) - } - msgtx := tx.toMsgTx() - - pkScript, _ := txscript.PayToAddrScript(addr.(*btcutil.AddressScriptHash)) - err = signMultiSigUTXO(mgr, addrmgrNs, msgtx, 0, pkScript, []RawSig{{}}) - - TstCheckError(t, "", err, ErrTxSigning) -} - -func TestSignMultiSigUTXONotEnoughSigs(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - _, addrmgrNs := TstRWNamespaces(dbtx) - - mgr := pool.Manager() - tx := createWithdrawalTx(t, dbtx, pool, []int64{4e6}, []int64{}) - sigs, err := getRawSigs([]*withdrawalTx{tx}) - if err != nil { - t.Fatal(err) - } - msgtx := tx.toMsgTx() - txSigs := sigs[tx.ntxid()] - - idx := 0 // The index of the tx input we're going to sign. - // Here we provide reqSigs-1 signatures to SignMultiSigUTXO() - reqSigs := tx.inputs[idx].addr.series().TstGetReqSigs() - txInSigs := txSigs[idx][:reqSigs-1] - pkScript := tx.inputs[idx].PkScript - TstRunWithManagerUnlocked(t, mgr, addrmgrNs, func() { - err = signMultiSigUTXO(mgr, addrmgrNs, msgtx, idx, pkScript, txInSigs) - }) - - TstCheckError(t, "", err, ErrTxSigning) -} - -func TestSignMultiSigUTXOWrongRawSigs(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - _, addrmgrNs := TstRWNamespaces(dbtx) - - mgr := pool.Manager() - tx := createWithdrawalTx(t, dbtx, pool, []int64{4e6}, []int64{}) - sigs := []RawSig{{0x00}, {0x01}} - - idx := 0 // The index of the tx input we're going to sign. - pkScript := tx.inputs[idx].PkScript - TstRunWithManagerUnlocked(t, mgr, addrmgrNs, func() { - err = signMultiSigUTXO(mgr, addrmgrNs, tx.toMsgTx(), idx, pkScript, sigs) - }) - - TstCheckError(t, "", err, ErrTxSigning) -} - -func TestGetRawSigs(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - _, addrmgrNs := TstRWNamespaces(dbtx) - - tx := createWithdrawalTx(t, dbtx, pool, []int64{5e6, 4e6}, []int64{}) - - sigs, err := getRawSigs([]*withdrawalTx{tx}) - if err != nil { - t.Fatal(err) - } - msgtx := tx.toMsgTx() - txSigs := sigs[tx.ntxid()] - if len(txSigs) != len(tx.inputs) { - t.Fatalf("Unexpected number of sig lists; got %d, want %d", len(txSigs), len(tx.inputs)) - } - - checkNonEmptySigsForPrivKeys(t, txSigs, tx.inputs[0].addr.series().privateKeys) - - // Since we have all the necessary signatures (m-of-n), we construct the - // sigsnature scripts and execute them to make sure the raw signatures are - // valid. - signTxAndValidate(t, pool.Manager(), addrmgrNs, msgtx, txSigs, tx.inputs) -} - -func TestGetRawSigsOnlyOnePrivKeyAvailable(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - tx := createWithdrawalTx(t, dbtx, pool, []int64{5e6, 4e6}, []int64{}) - // Remove all private keys but the first one from the credit's series. - series := tx.inputs[0].addr.series() - for i := range series.privateKeys[1:] { - series.privateKeys[i] = nil - } - - sigs, err := getRawSigs([]*withdrawalTx{tx}) - if err != nil { - t.Fatal(err) - } - - txSigs := sigs[tx.ntxid()] - if len(txSigs) != len(tx.inputs) { - t.Fatalf("Unexpected number of sig lists; got %d, want %d", len(txSigs), len(tx.inputs)) - } - - checkNonEmptySigsForPrivKeys(t, txSigs, series.privateKeys) -} - -func TestGetRawSigsUnparseableRedeemScript(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - tx := createWithdrawalTx(t, dbtx, pool, []int64{5e6, 4e6}, []int64{}) - // Change the redeem script for one of our tx inputs, to force an error in - // getRawSigs(). - tx.inputs[0].addr.script = []byte{0x01} - - _, err = getRawSigs([]*withdrawalTx{tx}) - - TstCheckError(t, "", err, ErrRawSigning) -} - -func TestGetRawSigsInvalidAddrBranch(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - tx := createWithdrawalTx(t, dbtx, pool, []int64{5e6, 4e6}, []int64{}) - // Change the branch of our input's address to an invalid value, to force - // an error in getRawSigs(). - tx.inputs[0].addr.branch = Branch(999) - - _, err = getRawSigs([]*withdrawalTx{tx}) - - TstCheckError(t, "", err, ErrInvalidBranch) -} - -// TestOutBailmentIDSort tests that we can correctly sort a slice -// of output requests by the hash of the outbailmentID. -func TestOutBailmentIDSort(t *testing.T) { - or00 := OutputRequest{cachedHash: []byte{0, 0}} - or01 := OutputRequest{cachedHash: []byte{0, 1}} - or10 := OutputRequest{cachedHash: []byte{1, 0}} - or11 := OutputRequest{cachedHash: []byte{1, 1}} - - want := []OutputRequest{or00, or01, or10, or11} - random := []OutputRequest{or11, or00, or10, or01} - - sort.Sort(byOutBailmentID(random)) - - if !reflect.DeepEqual(random, want) { - t.Fatalf("Sort failed; got %v, want %v", random, want) - } -} - -func TestTxTooBig(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - - tx := createWithdrawalTx(t, dbtx, pool, []int64{5}, []int64{1}) - - tx.calculateSize = func() int { return txMaxSize - 1 } - if tx.isTooBig() { - t.Fatalf("Tx is smaller than max size (%d < %d) but was considered too big", - tx.calculateSize(), txMaxSize) - } - - // A tx whose size is equal to txMaxSize should be considered too big. - tx.calculateSize = func() int { return txMaxSize } - if !tx.isTooBig() { - t.Fatalf("Tx size is equal to the max size (%d == %d) but was not considered too big", - tx.calculateSize(), txMaxSize) - } - - tx.calculateSize = func() int { return txMaxSize + 1 } - if !tx.isTooBig() { - t.Fatalf("Tx size is bigger than max size (%d > %d) but was not considered too big", - tx.calculateSize(), txMaxSize) - } -} - -func TestTxSizeCalculation(t *testing.T) { - tearDown, db, pool := TstCreatePool(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - _, addrmgrNs := TstRWNamespaces(dbtx) - - tx := createWithdrawalTx(t, dbtx, pool, []int64{1, 5}, []int64{2}) - - size := tx.calculateSize() - - // Now add a change output, get a msgtx, sign it and get its SerializedSize - // to compare with the value above. We need to replace the calculateFee - // method so that the tx.addChange() call below always adds a change - // output. - tx.calculateFee = TstConstantFee(1) - seriesID := tx.inputs[0].addr.SeriesID() - tx.addChange(TstNewChangeAddress(t, pool, seriesID, 0).addr.ScriptAddress()) - msgtx := tx.toMsgTx() - sigs, err := getRawSigs([]*withdrawalTx{tx}) - if err != nil { - t.Fatal(err) - } - signTxAndValidate(t, pool.Manager(), addrmgrNs, msgtx, sigs[tx.ntxid()], tx.inputs) - - // ECDSA signatures have variable length (71-73 bytes) but in - // calculateSize() we use a dummy signature for the worst-case scenario (73 - // bytes) so the estimate here can be up to 2 bytes bigger for every - // signature in every input's SigScript. - maxDiff := 2 * len(msgtx.TxIn) * int(tx.inputs[0].addr.series().reqSigs) - // To make things worse, there's a possibility that the length of the - // actual SignatureScript is at the upper boundary of one of the uint* - // types, and when that happens our dummy SignatureScript is likely to have - // a length that cannot be represented in the same uint* type as that of the - // actual one, so we need to account for that here too. As per - // wire.VarIntSerializeSize(), the biggest difference would be of 4 - // bytes, when the actual SigScript size fits in a uint32 but the dummy one - // needs a uint64. - maxDiff += 4 * len(msgtx.TxIn) - if size-msgtx.SerializeSize() > maxDiff { - t.Fatalf("Size difference bigger than maximum expected: %d - %d > %d", - size, msgtx.SerializeSize(), maxDiff) - } else if size-msgtx.SerializeSize() < 0 { - t.Fatalf("Tx size (%d) bigger than estimated size (%d)", msgtx.SerializeSize(), size) - } -} - -func TestTxFeeEstimationForSmallTx(t *testing.T) { - tx := newWithdrawalTx(defaultTxOptions) - - // A tx that is smaller than 1000 bytes in size should have a fee of 10000 - // satoshis. - tx.calculateSize = func() int { return 999 } - fee := tx.calculateFee() - - wantFee := btcutil.Amount(1e3) - if fee != wantFee { - t.Fatalf("Unexpected tx fee; got %v, want %v", fee, wantFee) - } -} - -func TestTxFeeEstimationForLargeTx(t *testing.T) { - tx := newWithdrawalTx(defaultTxOptions) - - // A tx that is larger than 1000 bytes in size should have a fee of 1e3 - // satoshis plus 1e3 for every 1000 bytes. - tx.calculateSize = func() int { return 3000 } - fee := tx.calculateFee() - - wantFee := btcutil.Amount(4e3) - if fee != wantFee { - t.Fatalf("Unexpected tx fee; got %v, want %v", fee, wantFee) - } -} - -func TestStoreTransactionsWithoutChangeOutput(t *testing.T) { - tearDown, db, pool, store := TstCreatePoolAndTxStore(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - txmgrNs := dbtx.ReadWriteBucket(txmgrNamespaceKey) - - wtx := createWithdrawalTxWithStoreCredits(t, dbtx, store, pool, []int64{4e6}, []int64{3e6}) - tx := &changeAwareTx{MsgTx: wtx.toMsgTx(), changeIdx: int32(-1)} - if err := storeTransactions(store, txmgrNs, []*changeAwareTx{tx}); err != nil { - t.Fatal(err) - } - - credits, err := store.UnspentOutputs(txmgrNs) - if err != nil { - t.Fatal(err) - } - if len(credits) != 0 { - t.Fatalf("Unexpected number of credits in txstore; got %d, want 0", len(credits)) - } -} - -func TestStoreTransactionsWithChangeOutput(t *testing.T) { - tearDown, db, pool, store := TstCreatePoolAndTxStore(t) - defer tearDown() - - dbtx, err := db.BeginReadWriteTx() - if err != nil { - t.Fatal(err) - } - defer dbtx.Commit() - txmgrNs := dbtx.ReadWriteBucket(txmgrNamespaceKey) - - wtx := createWithdrawalTxWithStoreCredits(t, dbtx, store, pool, []int64{5e6}, []int64{1e6, 1e6}) - wtx.changeOutput = wire.NewTxOut(int64(3e6), []byte{}) - msgtx := wtx.toMsgTx() - tx := &changeAwareTx{MsgTx: msgtx, changeIdx: int32(len(msgtx.TxOut) - 1)} - - if err := storeTransactions(store, txmgrNs, []*changeAwareTx{tx}); err != nil { - t.Fatal(err) - } - - hash := msgtx.TxHash() - txDetails, err := store.TxDetails(txmgrNs, &hash) - if err != nil { - t.Fatal(err) - } - if txDetails == nil { - t.Fatal("The new tx doesn't seem to have been stored") - } - - storedTx := txDetails.TxRecord.MsgTx - outputTotal := int64(0) - for i, txOut := range storedTx.TxOut { - if int32(i) != tx.changeIdx { - outputTotal += txOut.Value - } - } - if outputTotal != int64(2e6) { - t.Fatalf("Unexpected output amount; got %v, want %v", outputTotal, int64(2e6)) - } - - inputTotal := btcutil.Amount(0) - for _, debit := range txDetails.Debits { - inputTotal += debit.Amount - } - if inputTotal != btcutil.Amount(5e6) { - t.Fatalf("Unexpected input amount; got %v, want %v", inputTotal, btcutil.Amount(5e6)) - } - - credits, err := store.UnspentOutputs(txmgrNs) - if err != nil { - t.Fatal(err) - } - if len(credits) != 1 { - t.Fatalf("Unexpected number of credits in txstore; got %d, want 1", len(credits)) - } - changeOutpoint := wire.OutPoint{Hash: hash, Index: uint32(tx.changeIdx)} - if credits[0].OutPoint != changeOutpoint { - t.Fatalf("Credit's outpoint (%v) doesn't match the one from change output (%v)", - credits[0].OutPoint, changeOutpoint) - } -} - -// createWithdrawalTxWithStoreCredits creates a new Credit in the given store -// for each entry in inputAmounts, and uses them to construct a withdrawalTx -// with one output for every entry in outputAmounts. -func createWithdrawalTxWithStoreCredits(t *testing.T, dbtx walletdb.ReadWriteTx, store *wtxmgr.Store, pool *Pool, - inputAmounts []int64, outputAmounts []int64) *withdrawalTx { - masters := []*hdkeychain.ExtendedKey{ - TstCreateMasterKey(t, bytes.Repeat(uint32ToBytes(getUniqueID()), 4)), - TstCreateMasterKey(t, bytes.Repeat(uint32ToBytes(getUniqueID()), 4)), - TstCreateMasterKey(t, bytes.Repeat(uint32ToBytes(getUniqueID()), 4)), - } - def := TstCreateSeriesDef(t, pool, 2, masters) - TstCreateSeries(t, dbtx, pool, []TstSeriesDef{def}) - net := pool.Manager().ChainParams() - tx := newWithdrawalTx(defaultTxOptions) - for _, c := range TstCreateSeriesCreditsOnStore(t, dbtx, pool, def.SeriesID, inputAmounts, store) { - tx.addInput(c) - } - for i, amount := range outputAmounts { - request := TstNewOutputRequest( - t, uint32(i), "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", btcutil.Amount(amount), net) - tx.addOutput(request) - } - return tx -} - -// checkNonEmptySigsForPrivKeys checks that every signature list in txSigs has -// one non-empty signature for every non-nil private key in the given list. This -// is to make sure every signature list matches the specification at -// http://opentransactions.org/wiki/index.php/Siglist. -func checkNonEmptySigsForPrivKeys(t *testing.T, txSigs TxSigs, privKeys []*hdkeychain.ExtendedKey) { - for _, txInSigs := range txSigs { - if len(txInSigs) != len(privKeys) { - t.Fatalf("Number of items in sig list (%d) does not match number of privkeys (%d)", - len(txInSigs), len(privKeys)) - } - for sigIdx, sig := range txInSigs { - key := privKeys[sigIdx] - if bytes.Equal(sig, []byte{}) && key != nil { - t.Fatalf("Empty signature (idx=%d) but key (%s) is available", - sigIdx, key.String()) - } else if !bytes.Equal(sig, []byte{}) && key == nil { - t.Fatalf("Signature not empty (idx=%d) but key is not available", sigIdx) - } - } - } -} - -// checkTxOutputs uses reflect.DeepEqual() to ensure that the tx outputs match -// the given slice of withdrawalTxOuts. -func checkTxOutputs(t *testing.T, tx *withdrawalTx, outputs []*withdrawalTxOut) { - nOutputs := len(outputs) - if len(tx.outputs) != nOutputs { - t.Fatalf("Wrong number of outputs in tx; got %d, want %d", len(tx.outputs), nOutputs) - } - for i, output := range tx.outputs { - if !reflect.DeepEqual(output, outputs[i]) { - t.Fatalf("Unexpected output; got %s, want %s", output, outputs[i]) - } - } -} - -// checkMsgTxOutputs checks that the pkScript and amount of every output in the -// given msgtx match the pkScript and amount of every item in the slice of -// OutputRequests. -func checkMsgTxOutputs(t *testing.T, msgtx *wire.MsgTx, requests []OutputRequest) { - nRequests := len(requests) - if len(msgtx.TxOut) != nRequests { - t.Fatalf("Unexpected number of TxOuts; got %d, want %d", len(msgtx.TxOut), nRequests) - } - for i, request := range requests { - txOut := msgtx.TxOut[i] - if !bytes.Equal(txOut.PkScript, request.PkScript) { - t.Fatalf( - "Unexpected pkScript for request %d; got %v, want %v", i, txOut.PkScript, - request.PkScript) - } - gotAmount := btcutil.Amount(txOut.Value) - if gotAmount != request.Amount { - t.Fatalf( - "Unexpected amount for request %d; got %v, want %v", i, gotAmount, request.Amount) - } - } -} - -// checkTxInputs ensures that the tx.inputs match the given inputs. -func checkTxInputs(t *testing.T, tx *withdrawalTx, inputs []credit) { - if len(tx.inputs) != len(inputs) { - t.Fatalf("Wrong number of inputs in tx; got %d, want %d", len(tx.inputs), len(inputs)) - } - for i, input := range tx.inputs { - if !reflect.DeepEqual(input, inputs[i]) { - t.Fatalf("Unexpected input; got %v, want %v", input, inputs[i]) - } - } -} - -// signTxAndValidate will construct the signature script for each input of the given -// transaction (using the given raw signatures and the pkScripts from credits) and execute -// those scripts to validate them. -func signTxAndValidate(t *testing.T, mgr *waddrmgr.Manager, addrmgrNs walletdb.ReadBucket, tx *wire.MsgTx, txSigs TxSigs, - credits []credit) { - for i := range tx.TxIn { - pkScript := credits[i].PkScript - TstRunWithManagerUnlocked(t, mgr, addrmgrNs, func() { - if err := signMultiSigUTXO(mgr, addrmgrNs, tx, i, pkScript, txSigs[i]); err != nil { - t.Fatal(err) - } - }) - } -} - -func compareMsgTxAndWithdrawalTxInputs(t *testing.T, msgtx *wire.MsgTx, tx *withdrawalTx) { - if len(msgtx.TxIn) != len(tx.inputs) { - t.Fatalf("Wrong number of inputs; got %d, want %d", len(msgtx.TxIn), len(tx.inputs)) - } - - for i, txin := range msgtx.TxIn { - outpoint := tx.inputs[i].OutPoint - if txin.PreviousOutPoint != outpoint { - t.Fatalf("Wrong outpoint; got %v expected %v", txin.PreviousOutPoint, outpoint) - } - } -} - -func compareMsgTxAndWithdrawalTxOutputs(t *testing.T, msgtx *wire.MsgTx, tx *withdrawalTx) { - nOutputs := len(tx.outputs) - - if tx.changeOutput != nil { - nOutputs++ - } - - if len(msgtx.TxOut) != nOutputs { - t.Fatalf("Unexpected number of TxOuts; got %d, want %d", len(msgtx.TxOut), nOutputs) - } - - for i, output := range tx.outputs { - outputRequest := output.request - txOut := msgtx.TxOut[i] - if !bytes.Equal(txOut.PkScript, outputRequest.PkScript) { - t.Fatalf( - "Unexpected pkScript for outputRequest %d; got %x, want %x", - i, txOut.PkScript, outputRequest.PkScript) - } - gotAmount := btcutil.Amount(txOut.Value) - if gotAmount != outputRequest.Amount { - t.Fatalf( - "Unexpected amount for outputRequest %d; got %v, want %v", - i, gotAmount, outputRequest.Amount) - } - } - - // Finally check the change output if it exists - if tx.changeOutput != nil { - msgTxChange := msgtx.TxOut[len(msgtx.TxOut)-1] - if msgTxChange != tx.changeOutput { - t.Fatalf("wrong TxOut in msgtx; got %v, want %v", msgTxChange, tx.changeOutput) - } - } -} - -func checkTxChangeAmount(t *testing.T, tx *withdrawalTx, amount btcutil.Amount) { - if !tx.hasChange() { - t.Fatalf("Transaction has no change.") - } - if tx.changeOutput.Value != int64(amount) { - t.Fatalf("Wrong change output amount; got %d, want %d", - tx.changeOutput.Value, int64(amount)) - } -} - -// checkLastOutputWasSplit ensures that the amount of the last output in the -// given tx matches newAmount and that the splitRequest amount is equal to -// origAmount - newAmount. It also checks that splitRequest is identical (except -// for its amount) to the request of the last output in the tx. -func checkLastOutputWasSplit(t *testing.T, w *withdrawal, tx *withdrawalTx, - origAmount, newAmount btcutil.Amount) { - splitRequest := w.pendingRequests[0] - lastOutput := tx.outputs[len(tx.outputs)-1] - if lastOutput.amount != newAmount { - t.Fatalf("Wrong amount in last output; got %s, want %s", lastOutput.amount, newAmount) - } - - wantSplitAmount := origAmount - newAmount - if splitRequest.Amount != wantSplitAmount { - t.Fatalf("Wrong amount in split output; got %v, want %v", splitRequest.Amount, - wantSplitAmount) - } - - // Check that the split request is identical (except for its amount) to the - // original one. - origRequest := lastOutput.request - if !bytes.Equal(origRequest.PkScript, splitRequest.PkScript) { - t.Fatalf("Wrong pkScript in split request; got %x, want %x", splitRequest.PkScript, - origRequest.PkScript) - } - if origRequest.Server != splitRequest.Server { - t.Fatalf("Wrong server in split request; got %s, want %s", splitRequest.Server, - origRequest.Server) - } - if origRequest.Transaction != splitRequest.Transaction { - t.Fatalf("Wrong transaction # in split request; got %d, want %d", splitRequest.Transaction, - origRequest.Transaction) - } - - status := w.status.outputs[origRequest.outBailmentID()].status - if status != statusPartial { - t.Fatalf("Wrong output status; got '%s', want '%s'", status, statusPartial) - } -}