diff --git a/txstore/tx.go b/txstore/tx.go index 37a5e1d..344c088 100644 --- a/txstore/tx.go +++ b/txstore/tx.go @@ -628,7 +628,7 @@ func (t *TxRecord) AddDebits() (Debits, error) { defer t.s.mtx.Unlock() if t.debits == nil { - spent, err := t.s.findPreviousCredits(t.Tx()) + spent, err := t.s.FindPreviousCredits(t.Tx()) if err != nil { return Debits{}, err } @@ -654,9 +654,9 @@ func (t *TxRecord) AddDebits() (Debits, error) { return d, nil } -// findPreviousCredits searches for all unspent credits that make up the inputs +// FindPreviousCredits searches for all unspent credits that make up the inputs // for tx. -func (s *Store) findPreviousCredits(tx *btcutil.Tx) ([]Credit, error) { +func (s *Store) FindPreviousCredits(tx *btcutil.Tx) ([]Credit, error) { type createdCredit struct { credit Credit err error diff --git a/votingpool/common_test.go b/votingpool/common_test.go new file mode 100644 index 0000000..c37ea2b --- /dev/null +++ b/votingpool/common_test.go @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * 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 ( + "fmt" + "os" + "runtime" + "testing" + + "github.com/btcsuite/btclog" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/waddrmgr" +) + +func init() { + runtime.GOMAXPROCS(runtime.NumCPU()) + + // Enable logging (Debug level) to aid debugging failing tests. + logger, err := btclog.NewLoggerFromWriter(os.Stdout, btclog.DebugLvl) + if err != nil { + fmt.Printf("Failed to initialize stdout logger: %v\n", err) + os.Exit(1) + } + 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, callback func()) { + if err := mgr.Unlock(privPassphrase); err != nil { + t.Fatal(err) + } + defer mgr.Lock() + callback() +} + +// replaceCalculateTxFee replaces the calculateTxFee func with the given one +// and returns a function that restores it to the original one. +func replaceCalculateTxFee(f func(*withdrawalTx) btcutil.Amount) func() { + orig := calculateTxFee + calculateTxFee = f + return func() { calculateTxFee = orig } +} + +// replaceIsTxTooBig replaces the isTxTooBig func with the given one +// and returns a function that restores it to the original one. +func replaceIsTxTooBig(f func(*withdrawalTx) bool) func() { + orig := isTxTooBig + isTxTooBig = f + return func() { isTxTooBig = orig } +} + +// replaceCalculateTxSize replaces the calculateTxSize func with the given one +// and returns a function that restores it to the original one. +func replaceCalculateTxSize(f func(*withdrawalTx) int) func() { + orig := calculateTxSize + calculateTxSize = f + return func() { calculateTxSize = orig } +} diff --git a/votingpool/db.go b/votingpool/db.go index d153bbc..d9a2a9d 100644 --- a/votingpool/db.go +++ b/votingpool/db.go @@ -22,12 +22,11 @@ import ( "fmt" "github.com/btcsuite/btcwallet/snacl" - "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/walletdb" ) // These constants define the serialized length for a given encrypted extended -// public or private key. +// public or private key. const ( // We can calculate the encrypted extended key length this way: // snacl.Overhead == overhead for encrypting (16) @@ -44,6 +43,8 @@ const ( ) var ( + usedAddrsBucketName = []byte("usedaddrs") + seriesBucketName = []byte("series") // string representing a non-existent private key seriesNullPrivKey = [seriesKeyLength]byte{} ) @@ -56,29 +57,103 @@ type dbSeriesRow struct { privKeysEncrypted [][]byte } -// putPool stores a voting pool in the database, creating a bucket named -// after the voting pool id. -func putPool(tx walletdb.Tx, votingPoolID []byte) error { - _, err := tx.RootBucket().CreateBucket(votingPoolID) +// 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(tx walletdb.Tx, poolID []byte, seriesID uint32, branch Branch, + index Index, encryptedHash []byte) error { + + usedAddrs := tx.RootBucket().Bucket(poolID).Bucket(usedAddrsBucketName) + bucket, err := usedAddrs.CreateBucketIfNotExists(getUsedAddrBucketID(seriesID, branch)) if err != nil { - str := fmt.Sprintf("cannot create voting pool %v", votingPoolID) - return managerError(waddrmgr.ErrDatabase, str, err) + 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(tx walletdb.Tx, poolID []byte, seriesID uint32, branch Branch, + index Index) []byte { + + usedAddrs := tx.RootBucket().Bucket(poolID).Bucket(usedAddrsBucketName) + bucket := usedAddrs.Bucket(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(tx walletdb.Tx, poolID []byte, seriesID uint32, branch Branch) (Index, error) { + maxIdx := Index(0) + usedAddrs := tx.RootBucket().Bucket(poolID).Bucket(usedAddrsBucketName) + bucket := usedAddrs.Bucket(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(tx walletdb.Tx, poolID []byte) error { + poolBucket, err := tx.RootBucket().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) } return nil } // loadAllSeries returns a map of all the series stored inside a voting pool // bucket, keyed by id. -func loadAllSeries(tx walletdb.Tx, votingPoolID []byte) (map[uint32]*dbSeriesRow, error) { - bucket := tx.RootBucket().Bucket(votingPoolID) +func loadAllSeries(tx walletdb.Tx, poolID []byte) (map[uint32]*dbSeriesRow, error) { + bucket := tx.RootBucket().Bucket(poolID).Bucket(seriesBucketName) allSeries := make(map[uint32]*dbSeriesRow) err := bucket.ForEach( func(k, v []byte) error { seriesID := bytesToUint32(k) series, err := deserializeSeriesRow(v) if err != nil { - str := fmt.Sprintf("cannot deserialize series %v", v) - return managerError(waddrmgr.ErrSeriesStorage, str, err) + return err } allSeries[seriesID] = series return nil @@ -91,14 +166,14 @@ func loadAllSeries(tx walletdb.Tx, votingPoolID []byte) (map[uint32]*dbSeriesRow // existsPool checks the existence of a bucket named after the given // voting pool id. -func existsPool(tx walletdb.Tx, votingPoolID []byte) bool { - bucket := tx.RootBucket().Bucket(votingPoolID) +func existsPool(tx walletdb.Tx, poolID []byte) bool { + bucket := tx.RootBucket().Bucket(poolID) return bucket != nil } // putSeries stores the given series inside a voting pool bucket named after -// votingPoolID. The voting pool bucket does not need to be created beforehand. -func putSeries(tx walletdb.Tx, votingPoolID []byte, version, ID uint32, active bool, reqSigs uint32, pubKeysEncrypted, privKeysEncrypted [][]byte) error { +// poolID. The voting pool bucket does not need to be created beforehand. +func putSeries(tx walletdb.Tx, poolID []byte, version, ID uint32, active bool, reqSigs uint32, pubKeysEncrypted, privKeysEncrypted [][]byte) error { row := &dbSeriesRow{ version: version, active: active, @@ -106,27 +181,27 @@ func putSeries(tx walletdb.Tx, votingPoolID []byte, version, ID uint32, active b pubKeysEncrypted: pubKeysEncrypted, privKeysEncrypted: privKeysEncrypted, } - return putSeriesRow(tx, votingPoolID, ID, row) + return putSeriesRow(tx, poolID, ID, row) } // putSeriesRow stores the given series row inside a voting pool bucket named -// after votingPoolID. The voting pool bucket does not need to be created +// after poolID. The voting pool bucket does not need to be created // beforehand. -func putSeriesRow(tx walletdb.Tx, votingPoolID []byte, ID uint32, row *dbSeriesRow) error { - bucket, err := tx.RootBucket().CreateBucketIfNotExists(votingPoolID) +func putSeriesRow(tx walletdb.Tx, poolID []byte, ID uint32, row *dbSeriesRow) error { + bucket, err := tx.RootBucket().CreateBucketIfNotExists(poolID) if err != nil { - str := fmt.Sprintf("cannot create bucket %v", votingPoolID) - return managerError(waddrmgr.ErrDatabase, str, err) + str := fmt.Sprintf("cannot create bucket %v", poolID) + return newError(ErrDatabase, str, err) } + bucket = bucket.Bucket(seriesBucketName) serialized, err := serializeSeriesRow(row) if err != nil { - str := fmt.Sprintf("cannot serialize series %v", row) - return managerError(waddrmgr.ErrSeriesStorage, str, err) + return err } err = bucket.Put(uint32ToBytes(ID), serialized) if err != nil { - str := fmt.Sprintf("cannot put series %v into bucket %v", serialized, votingPoolID) - return managerError(waddrmgr.ErrSeriesStorage, str, err) + str := fmt.Sprintf("cannot put series %v into bucket %v", serialized, poolID) + return newError(ErrDatabase, str, err) } return nil } @@ -142,17 +217,15 @@ func deserializeSeriesRow(serializedSeries []byte) (*dbSeriesRow, error) { // 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, managerError(waddrmgr.ErrSeriesStorage, str, nil) + 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, managerError(waddrmgr.ErrSeriesStorage, str, nil) + 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. @@ -163,7 +236,7 @@ func deserializeSeriesRow(serializedSeries []byte) (*dbSeriesRow, error) { if row.version > seriesMaxVersion { str := fmt.Sprintf("deserialization supports up to version %v not %v", seriesMaxVersion, row.version) - return nil, managerError(waddrmgr.ErrSeriesVersion, str, nil) + return nil, newError(ErrSeriesVersion, str, nil) } current += 4 @@ -178,13 +251,11 @@ func deserializeSeriesRow(serializedSeries []byte) (*dbSeriesRow, error) { // 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, managerError(waddrmgr.ErrSeriesStorage, str, nil) + 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, managerError(waddrmgr.ErrSeriesStorage, str, nil) + str := fmt.Sprintf("serialized series has too much data: %v", serializedSeries) + return nil, newError(ErrSeriesSerialization, str, nil) } // Deserialize the pubkey/privkey pairs. @@ -219,13 +290,13 @@ func serializeSeriesRow(row *dbSeriesRow) ([]byte, error) { 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, managerError(waddrmgr.ErrSeriesStorage, str, nil) + 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, managerError(waddrmgr.ErrSeriesVersion, str, nil) + return nil, newError(ErrSeriesVersion, str, nil) } serialized := make([]byte, 0, serializedLen) @@ -245,7 +316,7 @@ func serializeSeriesRow(row *dbSeriesRow) ([]byte, error) { if len(pubKeyEncrypted) != seriesKeyLength { str := fmt.Sprintf("wrong length of Encrypted Public Key: %v", pubKeyEncrypted) - return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil) + return nil, newError(ErrSeriesSerialization, str, nil) } serialized = append(serialized, pubKeyEncrypted...) @@ -260,7 +331,7 @@ func serializeSeriesRow(row *dbSeriesRow) ([]byte, error) { } else if len(privKeyEncrypted) != seriesKeyLength { str := fmt.Sprintf("wrong length of Encrypted Private Key: %v", len(privKeyEncrypted)) - return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil) + return nil, newError(ErrSeriesSerialization, str, nil) } else { serialized = append(serialized, privKeyEncrypted...) } diff --git a/votingpool/db_wb_test.go b/votingpool/db_wb_test.go new file mode 100644 index 0000000..7fda088 --- /dev/null +++ b/votingpool/db_wb_test.go @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2015 Conformal Systems LLC + * + * 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" + "testing" + + "github.com/btcsuite/btcwallet/walletdb" +) + +func TestPutUsedAddrHash(t *testing.T) { + tearDown, _, pool := TstCreatePool(t) + defer tearDown() + + dummyHash := bytes.Repeat([]byte{0x09}, 10) + err := pool.namespace.Update( + func(tx walletdb.Tx) error { + return putUsedAddrHash(tx, pool.ID, 0, 0, 0, dummyHash) + }) + if err != nil { + t.Fatal(err) + } + + var storedHash []byte + err = pool.namespace.View( + func(tx walletdb.Tx) error { + storedHash = getUsedAddrHash(tx, 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, _, pool := TstCreatePool(t) + defer tearDown() + + var err error + pool.namespace.Update( + func(tx walletdb.Tx) error { + for i, idx := range []int{0, 7, 9, 3001, 41, 500, 6} { + dummyHash := bytes.Repeat([]byte{byte(i)}, 10) + err = putUsedAddrHash(tx, pool.ID, 0, 0, Index(idx), dummyHash) + if err != nil { + t.Fatal(err) + } + } + return nil + }) + + var maxIdx Index + pool.namespace.View( + func(tx walletdb.Tx) error { + maxIdx, err = getMaxUsedIdx(tx, pool.ID, 0, 0) + if err != nil { + t.Fatal(err) + } + return nil + }) + if maxIdx != Index(3001) { + t.Fatalf("Wrong max idx; got %d, want %d", maxIdx, Index(3001)) + } +} diff --git a/votingpool/doc.go b/votingpool/doc.go index 71c5944..894aa53 100644 --- a/votingpool/doc.go +++ b/votingpool/doc.go @@ -20,19 +20,24 @@ 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. Each member of the pool -holds one of the n private keys needed to create a transaction and can -only create transactions that can spend the bitcoins if m - 1 other -members of the pool agree to it. +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. -This package depends on the waddrmgr package, and in particular -instances of the waddrgmgr.Manager structure. +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 about the pool as well as a poolID. +information related to that pool under a bucket whose key is the +pool's ID. Loading an existing pool @@ -43,28 +48,52 @@ 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. +(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 based on a seriesID a branch number and an index -creates a pay-to-script-hash address, where the script is a multisig -script. The public keys used as inputs for generating the address are -generated from the public keys passed to CreateSeries. In [1] the -generated public keys correspend to the lowest level or the -'address_index' in the hierarchy. +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 -Documentation +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. -[1] https://github.com/justusranvier/bips/blob/master/bip-draft-Hierarchy%20for%20Non-Colored%20Voting%20Pool%20Deterministic%20Multisig%20Wallets.mediawiki +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 new file mode 100644 index 0000000..f2a9e94 --- /dev/null +++ b/votingpool/error.go @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * 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 "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 + + // 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", +} + +// 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 new file mode 100644 index 0000000..b65a6ef --- /dev/null +++ b/votingpool/error_test.go @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * 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 ( + "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"}, + {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 index a77539e..3c72c92 100644 --- a/votingpool/example_test.go +++ b/votingpool/example_test.go @@ -17,69 +17,92 @@ package votingpool_test import ( + "bytes" "fmt" "io/ioutil" "os" "path/filepath" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/txstore" "github.com/btcsuite/btcwallet/votingpool" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/walletdb" _ "github.com/btcsuite/btcwallet/walletdb/bdb" ) -func Example_basic() { - // This example demonstrates how to create a voting pool, create a - // series, get a deposit address from a series and lastly how to - // replace a series. +var ( + pubPassphrase = []byte("pubPassphrase") + privPassphrase = []byte("privPassphrase") +) - // Create a new wallet DB. - dir, err := ioutil.TempDir("", "pool_test") +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.Printf("Failed to create db dir: %v\n", err) + fmt.Println(err) return } - db, err := walletdb.Create("bdb", filepath.Join(dir, "wallet.db")) - if err != nil { - fmt.Printf("Failed to create wallet DB: %v\n", err) - return - } - defer os.RemoveAll(dir) - defer db.Close() + defer dbTearDown() // Create a new walletdb namespace for the address manager. mgrNamespace, err := db.Namespace([]byte("waddrmgr")) if err != nil { - fmt.Printf("Failed to create addr manager DB namespace: %v\n", err) + fmt.Println(err) return } - // Create the address manager - mgr, err := waddrmgr.Create(mgrNamespace, seed, pubPassphrase, privPassphrase, - &chaincfg.MainNetParams, nil) + // Create the address manager. + seed := bytes.Repeat([]byte{0x2a, 0x64, 0xdf, 0x08}, 8) + var fastScrypt = &waddrmgr.Options{ScryptN: 16, ScryptR: 8, ScryptP: 1} + mgr, err := waddrmgr.Create( + mgrNamespace, seed, pubPassphrase, privPassphrase, &chaincfg.MainNetParams, fastScrypt) if err != nil { - fmt.Printf("Failed to create addr manager: %v\n", err) + fmt.Println(err) return } - defer mgr.Close() - // Create a walletdb for votingpools. + // Create a walletdb namespace for votingpools. vpNamespace, err := db.Namespace([]byte("votingpool")) if err != nil { - fmt.Printf("Failed to create VotingPool DB namespace: %v\n", err) + 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. + mgr, vpNamespace, tearDownFunc, err := exampleCreateMgrAndDBNamespace() + if err != nil { + fmt.Println(err) + return + } + defer tearDownFunc() + // Create the voting pool. pool, err := votingpool.Create(vpNamespace, mgr, []byte{0x00}) if err != nil { - fmt.Printf("Voting Pool creation failed: %v\n", err) + fmt.Println(err) return } // Create a 2-of-3 series. - apiVersion := uint32(1) seriesID := uint32(1) requiredSignatures := uint32(2) pubKeys := []string{ @@ -87,39 +110,220 @@ func Example_basic() { "xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9", "xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh", } - err = pool.CreateSeries(apiVersion, seriesID, requiredSignatures, pubKeys) + err = pool.CreateSeries(votingpool.CurrentVersion, seriesID, requiredSignatures, pubKeys) if err != nil { - fmt.Printf("Cannot create series: %v\n", err) + fmt.Println(err) return } // Create a deposit address. - branch := uint32(0) // The change branch - index := uint32(1) - addr, err := pool.DepositScriptAddress(seriesID, branch, index) + addr, err := pool.DepositScriptAddress(seriesID, votingpool.Branch(0), votingpool.Index(1)) if err != nil { - fmt.Printf("DepositScriptAddress failed for series: %d, branch: %d, index: %d\n", - seriesID, branch, index) + fmt.Println(err) return } fmt.Println("Generated deposit address:", addr.EncodeAddress()) - // Replace the existing series with a 3-of-5 series. - pubKeys = []string{ - "xpub661MyMwAqRbcFQfXKHwz8ZbTtePwAKu8pmGYyVrWEM96DYUTWDYipMnHrFcemZHn13jcRMfsNU3UWQUudiaE7mhkWCHGFRMavF167DQM4Va", - "xpub661MyMwAqRbcGnTEXx3ehjx8EiqQGnL4uhwZw3ZxvZAa2E6E4YVAp63UoVtvm2vMDDF8BdPpcarcf7PWcEKvzHhxzAYw1zG23C2egeh82AR", - "xpub661MyMwAqRbcG83KwFyr1RVrNUmqVwYxV6nzxbqoRTNc8fRnWxq1yQiTBifTHhevcEM9ucZ1TqFS7Kv17Gd81cesv6RDrrvYS9SLPjPXhV5", - "xpub661MyMwAqRbcFGJbLPhMjtpC1XntFpg6jjQWjr6yXN8b9wfS1RiU5EhJt5L7qoFuidYawc3XJoLjT2PcjVpXryS3hn1WmSPCyvQDNuKsfgM", - "xpub661MyMwAqRbcGJDX4GYocn7qCzvMJwNisxpzkYZAakcvXtWV6CanXuz9xdfe5kTptFMJ4hDt2iTiT11zyN14u8R5zLvoZ1gnEVqNLxp1r3v", - "xpub661MyMwAqRbcG13FtwvZVaA15pTerP4JdAGvytPykqDr2fKXePqw3wLhCALPAixsE176jFkc2ac9K3tnF4KwaTRKUqFF5apWD6XL9LHCu7E", - } - requiredSignatures = 3 - err = pool.ReplaceSeries(apiVersion, seriesID, requiredSignatures, pubKeys) + // 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. + mgr, vpNamespace, tearDownFunc, err := exampleCreateMgrAndDBNamespace() if err != nil { - fmt.Printf("Cannot replace series: %v\n", err) + fmt.Println(err) + return + } + defer tearDownFunc() + + // Create a pool and a series. See the DepositAddress example for more info + // on how this is done. + pool, seriesID, err := exampleCreatePoolAndSeries(mgr, vpNamespace) + if err != nil { + fmt.Println(err) + return + } + + // Now empower the series with one of its private keys. Notice that in order + // to do that we need to unlock the address manager. + if err := mgr.Unlock(privPassphrase); err != nil { + fmt.Println(err) + return + } + defer mgr.Lock() + privKey := "xprv9s21ZrQH143K2j9PK4CXkCu8sgxkpUxCF7p1KVwiV5tdnkeYzJXReUkxz5iB2FUzTXC1L15abCDG4RMxSYT5zhm67uvsnLYxuDhZfoFcB6a" + err = pool.EmpowerSeries(seriesID, privKey) + 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_startWithdrawal() { + // Create the address manager and votingpool DB namespace. See the example + // for the Create() function for more info on how this is done. + mgr, vpNamespace, tearDownFunc, err := exampleCreateMgrAndDBNamespace() + if err != nil { + fmt.Println(err) + return + } + defer tearDownFunc() + + // Create a pool and a series. See the DepositAddress example for more info + // on how this is done. + pool, seriesID, err := exampleCreatePoolAndSeries(mgr, vpNamespace) + if err != nil { + fmt.Println(err) + return + } + + // Unlock the manager + if err := mgr.Unlock(privPassphrase); err != nil { + fmt.Println(err) + return + } + defer mgr.Lock() + + addr, _ := btcutil.DecodeAddress("1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX", mgr.ChainParams()) + pkScript, _ := txscript.PayToAddrScript(addr) + requests := []votingpool.OutputRequest{ + votingpool.OutputRequest{ + PkScript: pkScript, + Address: addr, + Amount: 1e6, + Server: "server-id", + Transaction: 123}, + } + changeStart, err := pool.ChangeAddress(seriesID, votingpool.Index(0)) + if err != nil { + fmt.Println(err) + return + } + // 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. + if err = pool.EnsureUsedAddr(seriesID, votingpool.Branch(1), votingpool.Index(0)); err != nil { + fmt.Println(err) + return + } + startAddr, err := pool.WithdrawalAddress(seriesID, votingpool.Branch(1), votingpool.Index(0)) + if err != nil { + fmt.Println(err) + return + } + lastSeriesID := seriesID + dustThreshold := btcutil.Amount(1e4) + currentBlock := int32(19432) + roundID := uint32(0) + txstore, tearDownFunc, err := exampleCreateTxStore() + if err != nil { + fmt.Println(err) + return + } + _, err = pool.StartWithdrawal( + roundID, requests, *startAddr, lastSeriesID, *changeStart, txstore, currentBlock, + dustThreshold) + if err != nil { + fmt.Println(err) + } + + // 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 +} + +func exampleCreateMgrAndDBNamespace() (*waddrmgr.Manager, walletdb.Namespace, func(), error) { + db, dbTearDown, err := createWalletDB() + if err != nil { + return nil, nil, nil, err + } + + // Create a new walletdb namespace for the address manager. + mgrNamespace, err := db.Namespace([]byte("waddrmgr")) + if err != nil { + dbTearDown() + return nil, nil, nil, err + } + + // Create the address manager + seed := bytes.Repeat([]byte{0x2a, 0x64, 0xdf, 0x08}, 8) + var fastScrypt = &waddrmgr.Options{ScryptN: 16, ScryptR: 8, ScryptP: 1} + mgr, err := waddrmgr.Create( + mgrNamespace, seed, pubPassphrase, privPassphrase, &chaincfg.MainNetParams, fastScrypt) + if err != nil { + dbTearDown() + return nil, nil, nil, err + } + + tearDownFunc := func() { + mgr.Close() + dbTearDown() + } + + // Create a walletdb namespace for votingpools. + vpNamespace, err := db.Namespace([]byte("votingpool")) + if err != nil { + tearDownFunc() + return nil, nil, nil, err + } + return mgr, vpNamespace, tearDownFunc, nil +} + +func exampleCreatePoolAndSeries(mgr *waddrmgr.Manager, vpNamespace walletdb.Namespace) ( + *votingpool.Pool, uint32, error) { + pool, err := votingpool.Create(vpNamespace, mgr, []byte{0x00}) + if err != nil { + return nil, 0, err + } + + // Create a 2-of-3 series. + seriesID := uint32(1) + requiredSignatures := uint32(2) + pubKeys := []string{ + "xpub661MyMwAqRbcFDDrR5jY7LqsRioFDwg3cLjc7tML3RRcfYyhXqqgCH5SqMSQdpQ1Xh8EtVwcfm8psD8zXKPcRaCVSY4GCqbb3aMEs27GitE", + "xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9", + "xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh", + } + err = pool.CreateSeries(votingpool.CurrentVersion, seriesID, requiredSignatures, pubKeys) + if err != nil { + return nil, 0, err + } + err = pool.ActivateSeries(seriesID) + if err != nil { + return nil, 0, err + } + return pool, seriesID, nil +} + +func exampleCreateTxStore() (*txstore.Store, func(), error) { + dir, err := ioutil.TempDir("", "tx.bin") + if err != nil { + return nil, nil, err + } + s := txstore.New(dir) + return s, func() { os.RemoveAll(dir) }, nil } diff --git a/votingpool/factory_test.go b/votingpool/factory_test.go new file mode 100644 index 0000000..1610db3 --- /dev/null +++ b/votingpool/factory_test.go @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * 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" + + "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/txstore" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" +) + +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, pool *Pool, store *txstore.Store, inputAmounts []int64, + outputAmounts []int64) *withdrawalTx { + net := pool.Manager().ChainParams() + tx := newWithdrawalTx() + _, credits := TstCreateCredits(t, pool, inputAmounts, store) + 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: wire.ShaHash{}, + 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 +} + +// 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, p *Pool, seriesID uint32, branch Branch, idx Index) []byte { + addr, err := p.getUsedAddr(seriesID, branch, idx) + if err != nil { + t.Fatal(err) + } else if addr != nil { + var script []byte + TstRunWithManagerUnlocked(t, p.Manager(), func() { + script, err = addr.Script() + }) + if err != nil { + t.Fatal(err) + } + return script + } + TstRunWithManagerUnlocked(t, p.Manager(), func() { + err = p.EnsureUsedAddr(seriesID, branch, idx) + }) + if err != nil { + t.Fatal(err) + } + return TstNewDepositScript(t, p, seriesID, branch, idx) +} + +func TstCreatePkScript(t *testing.T, p *Pool, seriesID uint32, branch Branch, idx Index) []byte { + script := TstEnsureUsedAddr(t, 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 +} + +func TstCreateTxStore(t *testing.T) (store *txstore.Store, tearDown func()) { + dir, err := ioutil.TempDir("", "tx.bin") + if err != nil { + t.Fatalf("Failed to create db file: %v", err) + } + s := txstore.New(dir) + return s, func() { os.RemoveAll(dir) } +} + +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, pool *Pool, definitions []TstSeriesDef) { + for _, def := range definitions { + err := pool.CreateSeries(CurrentVersion, def.SeriesID, def.ReqSigs, def.PubKeys) + if err != nil { + t.Fatalf("Cannot creates series %d: %v", def.SeriesID, err) + } + TstRunWithManagerUnlocked(t, pool.Manager(), func() { + for _, key := range def.PrivKeys { + if err := pool.EmpowerSeries(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) + 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)) + if seriesID == 0 { + seriesID++ + } + return TstSeriesDef{ + ReqSigs: reqSigs, SeriesID: seriesID, PubKeys: pubKeys, PrivKeys: privKeys} +} + +func TstCreatePoolAndTxStore(t *testing.T) (tearDown func(), pool *Pool, store *txstore.Store) { + mgrTearDown, _, pool := TstCreatePool(t) + store, storeTearDown := TstCreateTxStore(t) + tearDown = func() { + mgrTearDown() + storeTearDown() + } + return tearDown, pool, store +} + +// TstCreateCredits 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 TstCreateCredits(t *testing.T, pool *Pool, amounts []int64, store *txstore.Store) ( + 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, pool, []TstSeriesDef{def}) + return def.SeriesID, TstCreateCreditsOnSeries(t, pool, def.SeriesID, amounts, store) +} + +// TstCreateCreditsOnSeries creates a slice of credits locked to the given +// series' address with branch==1 and index==0. +func TstCreateCreditsOnSeries(t *testing.T, pool *Pool, seriesID uint32, amounts []int64, + store *txstore.Store) []Credit { + branch := Branch(1) + idx := Index(0) + pkScript := TstCreatePkScript(t, pool, seriesID, branch, idx) + eligible := make([]Credit, len(amounts)) + for i, credit := range TstCreateInputs(t, store, pkScript, amounts) { + eligible[i] = newCredit(credit, *TstNewWithdrawalAddress(t, pool, seriesID, branch, idx)) + } + return eligible +} + +// TstCreateInputs is a convenience function. See TstCreateInputsOnBlock +// for a more flexible version. +func TstCreateInputs(t *testing.T, store *txstore.Store, pkScript []byte, amounts []int64) []txstore.Credit { + return TstCreateInputsOnBlock(t, store, 1, pkScript, amounts) +} + +// TstCreateInputsOnBlock creates a number of inputs by creating a transaction +// with a number of outputs corresponding to the elements of the amounts slice. +// +// The transaction is added to a block and the index and blockheight must be +// specified. +func TstCreateInputsOnBlock(t *testing.T, s *txstore.Store, + blockTxIndex int, pkScript []byte, amounts []int64) []txstore.Credit { + msgTx := createMsgTx(pkScript, amounts) + block := &txstore.Block{ + Height: TstInputsBlock, + } + + tx := btcutil.NewTx(msgTx) + tx.SetIndex(blockTxIndex) + + r, err := s.InsertTx(tx, block) + if err != nil { + t.Fatal("Failed to create inputs: ", err) + } + + credits := make([]txstore.Credit, len(msgTx.TxOut)) + for i := range msgTx.TxOut { + credit, err := r.AddCredit(uint32(i), false) + if err != nil { + t.Fatal("Failed to create inputs: ", err) + } + credits[i] = credit + } + return credits +} + +// TstCreatePool creates a Pool on a fresh walletdb and returns it. It also +// returns the pool's waddrmgr.Manager (which uses the same walletdb, but with a +// different namespace) as a convenience, and a teardown function that closes +// the Manager and removes the directory used to store the database. +func TstCreatePool(t *testing.T) (tearDownFunc func(), mgr *waddrmgr.Manager, 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) + } + mgrNamespace, err := db.Namespace([]byte("waddrmgr")) + if err != nil { + t.Fatalf("Failed to create addr manager DB namespace: %v", err) + } + var fastScrypt = &waddrmgr.Options{ScryptN: 16, ScryptR: 8, ScryptP: 1} + mgr, err = waddrmgr.Create(mgrNamespace, seed, pubPassphrase, privPassphrase, + &chaincfg.MainNetParams, fastScrypt) + if err != nil { + t.Fatalf("Failed to create addr manager: %v", err) + } + + // Create a walletdb for votingpools. + vpNamespace, err := db.Namespace([]byte("votingpool")) + if err != nil { + t.Fatalf("Failed to create VotingPool DB namespace: %v", err) + } + pool, err = Create(vpNamespace, mgr, []byte{0x00}) + if err != nil { + t.Fatalf("Voting Pool creation failed: %v", err) + } + tearDownFunc = func() { + db.Close() + mgr.Close() + os.RemoveAll(dir) + } + return tearDownFunc, mgr, pool +} + +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, p *Pool, seriesID uint32, branch Branch, + index Index) (addr *WithdrawalAddress) { + TstEnsureUsedAddr(t, p, seriesID, branch, index) + var err error + TstRunWithManagerUnlocked(t, p.Manager(), func() { + addr, err = p.WithdrawalAddress(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(tx *withdrawalTx) btcutil.Amount { + return func(tx *withdrawalTx) btcutil.Amount { return fee } +} diff --git a/votingpool/input_selection.go b/votingpool/input_selection.go new file mode 100644 index 0000000..cc93a97 --- /dev/null +++ b/votingpool/input_selection.go @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2015 Conformal Systems LLC + * + * 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/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/txstore" +) + +const eligibleInputMinConfirmations = 100 + +// Credit is an abstraction over txstore.Credit used in the construction of +// voting pool withdrawal transactions. +type Credit interface { + TxSha() *wire.ShaHash + OutputIndex() uint32 + Address() WithdrawalAddress + Amount() btcutil.Amount + OutPoint() *wire.OutPoint + TxOut() *wire.TxOut +} + +// credit implements the Credit interface. +type credit struct { + txstore.Credit + addr WithdrawalAddress +} + +// newCredit initialises a new credit. +func newCredit(c txstore.Credit, addr WithdrawalAddress) *credit { + return &credit{Credit: c, addr: addr} +} + +func (c *credit) String() string { + return fmt.Sprintf("credit of %v to %v", c.Amount(), c.Address()) +} + +// TxSha returns the sha hash of the underlying transaction. +func (c *credit) TxSha() *wire.ShaHash { + return c.Credit.TxRecord.Tx().Sha() +} + +// OutputIndex returns the outputindex of the ouput in the underlying +// transaction. +func (c *credit) OutputIndex() uint32 { + return c.Credit.OutputIndex +} + +// Address returns the voting pool address. +func (c *credit) Address() WithdrawalAddress { + return c.addr +} + +// Compile time check that credit implements Credit interface. +var _ Credit = (*credit)(nil) + +// 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].Address() + jAddr := c[j].Address() + 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].TxSha().Bytes(), c[j].TxSha().Bytes()) + 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].OutputIndex() < c[j].OutputIndex() +} + +// getEligibleInputs returns eligible inputs with addresses between startAddress +// and the last used address of lastSeriesID. +func (p *Pool) getEligibleInputs(store *txstore.Store, 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() + 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 { + if p.isCreditEligible(c, minConf, chainHeight, dustThreshold) { + eligibles = append(eligibles, newCredit(c, address)) + } + } + // Make sure the eligibles are correctly sorted. + sort.Sort(byAddress(eligibles)) + inputs = append(inputs, eligibles...) + } + nAddr, err := nextAddr(p, 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 + } + 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, 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(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(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, 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(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(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 []txstore.Credit, chainParams *chaincfg.Params) ( + map[string][]txstore.Credit, error) { + addrMap := make(map[string][]txstore.Credit) + for _, c := range credits { + _, addrs, _, err := c.Addresses(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] = []txstore.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 txstore.Credit, minConf int, chainHeight int32, + dustThreshold btcutil.Amount) bool { + if c.Amount() < dustThreshold { + return false + } + if !c.Confirmed(minConf, chainHeight) { + 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 txstore.Credit) bool { + return false +} diff --git a/votingpool/input_selection_wb_test.go b/votingpool/input_selection_wb_test.go new file mode 100644 index 0000000..ebf092b --- /dev/null +++ b/votingpool/input_selection_wb_test.go @@ -0,0 +1,410 @@ +/* + * Copyright (c) 2015 Conformal Systems LLC + * + * 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" + "reflect" + "sort" + "testing" + + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/txstore" +) + +var ( + // random small number of satoshis used as dustThreshold + dustThreshold btcutil.Amount = 1e4 +) + +func TestGetEligibleInputs(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + series := []TstSeriesDef{ + {ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1}, + {ReqSigs: 2, PubKeys: TstPubKeys[3:6], SeriesID: 2}, + } + TstCreateSeries(t, pool, series) + scripts := append( + getPKScriptsForAddressRange(t, pool, 1, 0, 2, 0, 4), + getPKScriptsForAddressRange(t, 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 []txstore.Credit + for i := 0; i < len(scripts); i++ { + txIndex := int(i) + 1 + created := TstCreateInputsOnBlock(t, store, txIndex, scripts[i], eligibleAmounts) + inputs = append(inputs, created...) + } + + startAddr := TstNewWithdrawalAddress(t, pool, 1, 0, 0) + lastSeriesID := uint32(2) + currentBlock := int32(TstInputsBlock + eligibleInputMinConfirmations + 1) + var eligibles []Credit + var err error + TstRunWithManagerUnlocked(t, pool.Manager(), func() { + eligibles, err = pool.getEligibleInputs( + store, *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 sorted by address. + if !sort.IsSorted(byAddress(eligibles)) { + t.Fatal("Eligible inputs are not sorted.") + } + + // Check that all credits are unique + checkUniqueness(t, eligibles) +} + +func TestNextAddrWithVaryingHighestIndices(t *testing.T) { + tearDown, mgr, pool := TstCreatePool(t) + defer tearDown() + + series := []TstSeriesDef{ + {ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1}, + } + TstCreateSeries(t, pool, series) + stopSeriesID := uint32(2) + + // Populate the used addr DB for branch 0 and indices ranging from 0 to 2. + TstEnsureUsedAddr(t, pool, 1, Branch(0), 2) + + // Populate the used addr DB for branch 1 and indices ranging from 0 to 1. + TstEnsureUsedAddr(t, pool, 1, Branch(1), 1) + + // Start with the address for branch==0, index==1. + addr := TstNewWithdrawalAddress(t, pool, 1, 0, 1) + + var err error + // The first call to nextAddr() should give us the address for branch==1 + // and index==1. + TstRunWithManagerUnlocked(t, mgr, func() { + addr, err = nextAddr(pool, 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, mgr, func() { + addr, err = nextAddr(pool, 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, mgr, func() { + addr, err = nextAddr(pool, 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, mgr, pool := TstCreatePool(t) + defer tearDown() + + series := []TstSeriesDef{ + {ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1}, + {ReqSigs: 2, PubKeys: TstPubKeys[3:6], SeriesID: 2}, + } + TstCreateSeries(t, 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, pool, 1, Branch(i), lastIdx) + } + addr := TstNewWithdrawalAddress(t, pool, 1, 0, lastIdx-1) + var err error + // 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, mgr, func() { + addr, err = nextAddr(pool, 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, mgr, func() { + addr, err = nextAddr(pool, 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, 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, mgr, func() { + addr, err = nextAddr(pool, 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, pool, 2, 3, lastIdx) + TstRunWithManagerUnlocked(t, mgr, func() { + addr, err = nextAddr(pool, 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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + seriesID := uint32(1) + branch := Branch(0) + index := Index(0) + + // create the series + series := []TstSeriesDef{{ReqSigs: 3, PubKeys: TstPubKeys[1:6], SeriesID: seriesID}} + TstCreateSeries(t, pool, series) + + // Create the input. + pkScript := TstCreatePkScript(t, pool, seriesID, branch, index) + var chainHeight int32 = 1000 + c := TstCreateInputs(t, store, pkScript, []int64{int64(dustThreshold)})[0] + + // Make sure credits is old enough to pass the minConf check. + c.BlockHeight = 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, pool, store1 := TstCreatePoolAndTxStore(t) + store2, storeTearDown2 := TstCreateTxStore(t) + defer tearDown() + defer storeTearDown2() + seriesID := uint32(1) + branch := Branch(0) + index := Index(0) + + // create the series + series := []TstSeriesDef{{ReqSigs: 3, PubKeys: TstPubKeys[1:6], SeriesID: seriesID}} + TstCreateSeries(t, pool, series) + + pkScript := TstCreatePkScript(t, pool, seriesID, branch, index) + var chainHeight int32 = 1000 + + // Check that credit below dustThreshold is rejected. + c1 := TstCreateInputs(t, store1, pkScript, []int64{int64(dustThreshold - 1)})[0] + c1.BlockHeight = int32(100) // make sure it has enough confirmations. + if pool.isCreditEligible(c1, eligibleInputMinConfirmations, chainHeight, dustThreshold) { + t.Errorf("Input is eligible and it should not be.") + } + + // Check that a credit with not enough confirmations is rejected. + c2 := TstCreateInputs(t, store2, pkScript, []int64{int64(dustThreshold)})[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 as *that* makes 1000 - 902 +1 = 99 >= + // 100 false + c2.BlockHeight = int32(902) + if pool.isCreditEligible(c2, eligibleInputMinConfirmations, chainHeight, dustThreshold) { + t.Errorf("Input is eligible and it should not be.") + } +} + +func TestCreditSortingByAddress(t *testing.T) { + teardown, _, pool := TstCreatePool(t) + defer teardown() + + series := []TstSeriesDef{ + {ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1}, + {ReqSigs: 2, PubKeys: TstPubKeys[3:6], SeriesID: 2}, + } + TstCreateSeries(t, pool, series) + + shaHash0 := bytes.Repeat([]byte{0}, 32) + shaHash1 := bytes.Repeat([]byte{1}, 32) + shaHash2 := bytes.Repeat([]byte{2}, 32) + c0 := TstNewFakeCredit(t, pool, 1, 0, 0, shaHash0, 0) + c1 := TstNewFakeCredit(t, pool, 1, 0, 0, shaHash0, 1) + c2 := TstNewFakeCredit(t, pool, 1, 0, 0, shaHash1, 0) + c3 := TstNewFakeCredit(t, pool, 1, 0, 0, shaHash2, 0) + c4 := TstNewFakeCredit(t, pool, 1, 0, 1, shaHash0, 0) + c5 := TstNewFakeCredit(t, pool, 1, 1, 0, shaHash0, 0) + c6 := TstNewFakeCredit(t, pool, 2, 0, 0, shaHash0, 0) + + randomCredits := [][]Credit{ + []Credit{c6, c5, c4, c3, c2, c1, c0}, + []Credit{c2, c1, c0, c6, c5, c4, c3}, + []Credit{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]) + } + } + } +} + +// TstFakeCredit is a structure implementing the Credit interface used to test +// the byAddress sorting. It exists because to test the sorting properly we need +// to be able to set the Credit's TxSha and OutputIndex. +type TstFakeCredit struct { + addr WithdrawalAddress + txSha *wire.ShaHash + outputIndex uint32 + amount btcutil.Amount +} + +func (c *TstFakeCredit) String() string { return "" } +func (c *TstFakeCredit) TxSha() *wire.ShaHash { return c.txSha } +func (c *TstFakeCredit) OutputIndex() uint32 { return c.outputIndex } +func (c *TstFakeCredit) Address() WithdrawalAddress { return c.addr } +func (c *TstFakeCredit) Amount() btcutil.Amount { return c.amount } +func (c *TstFakeCredit) TxOut() *wire.TxOut { return nil } +func (c *TstFakeCredit) OutPoint() *wire.OutPoint { + return &wire.OutPoint{Hash: *c.txSha, Index: c.outputIndex} +} + +func TstNewFakeCredit(t *testing.T, pool *Pool, series uint32, index Index, branch Branch, + txSha []byte, outputIdx int) *TstFakeCredit { + var hash wire.ShaHash + if err := hash.SetBytes(txSha); 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, pool, series, branch, index) + addr := TstNewWithdrawalAddress(t, pool, series, branch, index) + return &TstFakeCredit{ + addr: *addr, + txSha: &hash, + outputIndex: uint32(outputIdx), + } +} + +// Compile time check that TstFakeCredit implements the +// Credit interface. +var _ Credit = (*TstFakeCredit)(nil) + +func checkUniqueness(t *testing.T, credits byAddress) { + type uniq struct { + series uint32 + branch Branch + index Index + hash wire.ShaHash + outputIndex uint32 + } + + uniqMap := make(map[uniq]bool) + for _, c := range credits { + u := uniq{ + series: c.Address().SeriesID(), + branch: c.Address().Branch(), + index: c.Address().Index(), + hash: *c.TxSha(), + outputIndex: c.OutputIndex(), + } + if _, exists := uniqMap[u]; exists { + t.Fatalf("Duplicate found: %v", u) + } else { + uniqMap[u] = true + } + } +} + +func getPKScriptsForAddressRange(t *testing.T, 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, 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 index 83009fc..bef0915 100644 --- a/votingpool/internal_test.go +++ b/votingpool/internal_test.go @@ -17,11 +17,16 @@ 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(version, seriesID, reqSigs uint32, inRawPubKeys []string) error { return vp.putSeries(version, seriesID, reqSigs, inRawPubKeys) @@ -31,7 +36,24 @@ var TstBranchOrder = branchOrder // TstExistsSeries checks whether a series is stored in the database. func (vp *Pool) TstExistsSeries(seriesID uint32) (bool, error) { - return vp.existsSeries(seriesID) + var exists bool + err := vp.namespace.View( + func(tx walletdb.Tx) error { + poolBucket := tx.RootBucket().Bucket(vp.ID) + if poolBucket == nil { + return nil + } + bucket := poolBucket.Bucket(seriesBucketName) + if bucket == nil { + return nil + } + exists = bucket.Get(uint32ToBytes(seriesID)) != nil + return nil + }) + if err != nil { + return false, err + } + return exists, nil } // TstNamespace exposes the Pool's namespace as it's needed in some tests. @@ -74,44 +96,7 @@ func (vp *Pool) TstDecryptExtendedKey(keyType waddrmgr.CryptoKeyType, encrypted return vp.decryptExtendedKey(keyType, encrypted) } -// SeriesRow mimics dbSeriesRow defined in db.go . -type SeriesRow struct { - Version uint32 - Active bool - ReqSigs uint32 - PubKeysEncrypted [][]byte - PrivKeysEncrypted [][]byte +// TstGetMsgTx returns the withdrawal transaction with the given ntxid. +func (s *WithdrawalStatus) TstGetMsgTx(ntxid Ntxid) *wire.MsgTx { + return s.transactions[ntxid].MsgTx } - -// SerializeSeries wraps serializeSeriesRow by passing it a freshly-built -// dbSeriesRow. -func SerializeSeries(version uint32, active bool, reqSigs uint32, pubKeys, privKeys [][]byte) ([]byte, error) { - row := &dbSeriesRow{ - version: version, - active: active, - reqSigs: reqSigs, - pubKeysEncrypted: pubKeys, - privKeysEncrypted: privKeys, - } - return serializeSeriesRow(row) -} - -// DeserializeSeries wraps deserializeSeriesRow and returns a freshly-built -// SeriesRow. -func DeserializeSeries(serializedSeries []byte) (*SeriesRow, error) { - row, err := deserializeSeriesRow(serializedSeries) - - if err != nil { - return nil, err - } - - return &SeriesRow{ - Version: row.version, - Active: row.active, - ReqSigs: row.reqSigs, - PubKeysEncrypted: row.pubKeysEncrypted, - PrivKeysEncrypted: row.privKeysEncrypted, - }, nil -} - -var TstValidateAndDecryptKeys = validateAndDecryptKeys diff --git a/votingpool/log.go b/votingpool/log.go new file mode 100644 index 0000000..80fa0c7 --- /dev/null +++ b/votingpool/log.go @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2015 Conformal Systems LLC + * + * 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 "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 index e1d483b..a543568 100644 --- a/votingpool/pool.go +++ b/votingpool/pool.go @@ -30,8 +30,16 @@ import ( 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 @@ -55,6 +63,36 @@ type Pool struct { namespace walletdb.Namespace } +// 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(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID []byte) (*Pool, error) { @@ -64,7 +102,7 @@ func Create(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID []byte) (* }) if err != nil { str := fmt.Sprintf("unable to add voting pool %v to db", poolID) - return nil, managerError(waddrmgr.ErrVotingPoolAlreadyExists, str, err) + return nil, newError(ErrPoolAlreadyExists, str, err) } return newPool(namespace, m, poolID), nil } @@ -76,18 +114,18 @@ func Load(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID []byte) (*Po func(tx walletdb.Tx) error { if exists := existsPool(tx, poolID); !exists { str := fmt.Sprintf("unable to find voting pool %v in db", poolID) - return managerError(waddrmgr.ErrVotingPoolNotExists, str, nil) + return newError(ErrPoolNotExists, str, nil) } return nil }) if err != nil { return nil, err } - vp := newPool(namespace, m, poolID) - if err = vp.LoadAllSeries(); err != nil { + p := newPool(namespace, m, poolID) + if err = p.LoadAllSeries(); err != nil { return nil, err } - return vp, nil + return p, nil } // newPool creates a new Pool instance. @@ -102,13 +140,13 @@ func newPool(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID []byte) * // LoadAndGetDepositScript generates and returns a deposit script for the given seriesID, // branch and index of the Pool identified by poolID. -func LoadAndGetDepositScript(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID string, seriesID, branch, index uint32) ([]byte, error) { +func LoadAndGetDepositScript(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID string, seriesID uint32, branch Branch, index Index) ([]byte, error) { pid := []byte(poolID) - vp, err := Load(namespace, m, pid) + p, err := Load(namespace, m, pid) if err != nil { return nil, err } - script, err := vp.DepositScript(seriesID, branch, index) + script, err := p.DepositScript(seriesID, branch, index) if err != nil { return nil, err } @@ -121,11 +159,11 @@ func LoadAndGetDepositScript(namespace walletdb.Namespace, m *waddrmgr.Manager, func LoadAndCreateSeries(namespace walletdb.Namespace, m *waddrmgr.Manager, version uint32, poolID string, seriesID, reqSigs uint32, rawPubKeys []string) error { pid := []byte(poolID) - vp, err := Load(namespace, m, pid) + p, err := Load(namespace, m, pid) if err != nil { - managerErr := err.(waddrmgr.ManagerError) - if managerErr.ErrorCode == waddrmgr.ErrVotingPoolNotExists { - vp, err = Create(namespace, m, pid) + vpErr := err.(Error) + if vpErr.ErrorCode == ErrPoolNotExists { + p, err = Create(namespace, m, pid) if err != nil { return err } @@ -133,7 +171,7 @@ func LoadAndCreateSeries(namespace walletdb.Namespace, m *waddrmgr.Manager, vers return err } } - return vp.CreateSeries(version, seriesID, reqSigs, rawPubKeys) + return p.CreateSeries(version, seriesID, reqSigs, rawPubKeys) } // LoadAndReplaceSeries loads the voting pool with the given ID and calls ReplaceSeries, @@ -141,11 +179,11 @@ func LoadAndCreateSeries(namespace walletdb.Namespace, m *waddrmgr.Manager, vers func LoadAndReplaceSeries(namespace walletdb.Namespace, m *waddrmgr.Manager, version uint32, poolID string, seriesID, reqSigs uint32, rawPubKeys []string) error { pid := []byte(poolID) - vp, err := Load(namespace, m, pid) + p, err := Load(namespace, m, pid) if err != nil { return err } - return vp.ReplaceSeries(version, seriesID, reqSigs, rawPubKeys) + return p.ReplaceSeries(version, seriesID, reqSigs, rawPubKeys) } // LoadAndEmpowerSeries loads the voting pool with the given ID and calls EmpowerSeries, @@ -160,27 +198,34 @@ func LoadAndEmpowerSeries(namespace walletdb.Namespace, m *waddrmgr.Manager, return pool.EmpowerSeries(seriesID, rawPrivKey) } -// GetSeries returns the series with the given ID, or nil if it doesn't +// Series returns the series with the given ID, or nil if it doesn't // exist. -func (vp *Pool) GetSeries(seriesID uint32) *SeriesData { - series, exists := vp.seriesLookup[seriesID] +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. -func (vp *Pool) saveSeriesToDisk(seriesID uint32, data *SeriesData) error { +// +// This method must be called with the Pool's manager unlocked. +func (p *Pool) saveSeriesToDisk(seriesID uint32, data *SeriesData) error { var err error encryptedPubKeys := make([][]byte, len(data.publicKeys)) for i, pubKey := range data.publicKeys { - encryptedPubKeys[i], err = vp.manager.Encrypt( + encryptedPubKeys[i], err = p.manager.Encrypt( waddrmgr.CKTPublic, []byte(pubKey.String())) if err != nil { str := fmt.Sprintf("key %v failed encryption", pubKey) - return managerError(waddrmgr.ErrCrypto, str, err) + return newError(ErrCrypto, str, err) } } encryptedPrivKeys := make([][]byte, len(data.privateKeys)) @@ -188,22 +233,22 @@ func (vp *Pool) saveSeriesToDisk(seriesID uint32, data *SeriesData) error { if privKey == nil { encryptedPrivKeys[i] = nil } else { - encryptedPrivKeys[i], err = vp.manager.Encrypt( + encryptedPrivKeys[i], err = p.manager.Encrypt( waddrmgr.CKTPrivate, []byte(privKey.String())) } if err != nil { str := fmt.Sprintf("key %v failed encryption", privKey) - return managerError(waddrmgr.ErrCrypto, str, err) + return newError(ErrCrypto, str, err) } } - err = vp.namespace.Update(func(tx walletdb.Tx) error { - return putSeries(tx, vp.ID, data.version, seriesID, data.active, + err = p.namespace.Update(func(tx walletdb.Tx) error { + return putSeries(tx, 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 managerError(waddrmgr.ErrSeriesStorage, str, err) + return newError(ErrSeriesSerialization, str, err) } return nil } @@ -226,19 +271,19 @@ func convertAndValidatePubKeys(rawPubKeys []string) ([]*hdkeychain.ExtendedKey, for i, rawPubKey := range rawPubKeys { if _, seen := seenKeys[rawPubKey]; seen { str := fmt.Sprintf("duplicated public key: %v", rawPubKey) - return nil, managerError(waddrmgr.ErrKeyDuplicate, str, nil) + 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, managerError(waddrmgr.ErrKeyChain, str, err) + return nil, newError(ErrKeyChain, str, err) } if key.IsPrivate() { str := fmt.Sprintf("private keys not accepted: %v", rawPubKey) - return nil, managerError(waddrmgr.ErrKeyIsPrivate, str, nil) + return nil, newError(ErrKeyIsPrivate, str, nil) } keys[i] = key } @@ -251,16 +296,18 @@ func convertAndValidatePubKeys(rawPubKeys []string) ([]*hdkeychain.ExtendedKey, // 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. -func (vp *Pool) putSeries(version, seriesID, reqSigs uint32, inRawPubKeys []string) error { +// +// This method must be called with the Pool's manager unlocked. +func (p *Pool) putSeries(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 managerError(waddrmgr.ErrTooFewPublicKeys, str, nil) + 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 managerError(waddrmgr.ErrTooManyReqSignatures, str, nil) + return newError(ErrTooManyReqSignatures, str, nil) } rawPubKeys := CanonicalKeyOrder(inRawPubKeys) @@ -278,59 +325,90 @@ func (vp *Pool) putSeries(version, seriesID, reqSigs uint32, inRawPubKeys []stri privateKeys: make([]*hdkeychain.ExtendedKey, len(keys)), } - err = vp.saveSeriesToDisk(seriesID, data) + err = p.saveSeriesToDisk(seriesID, data) if err != nil { return err } - vp.seriesLookup[seriesID] = data + 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 (vp *Pool) CreateSeries(version, seriesID, reqSigs uint32, rawPubKeys []string) error { - if series := vp.GetSeries(seriesID); series != nil { - str := fmt.Sprintf("series #%d already exists", seriesID) - return managerError(waddrmgr.ErrSeriesAlreadyExists, str, nil) +func (p *Pool) CreateSeries(version, seriesID, reqSigs uint32, rawPubKeys []string) error { + if seriesID == 0 { + return newError(ErrSeriesIDInvalid, "series ID cannot be 0", nil) } - return vp.putSeries(version, seriesID, reqSigs, rawPubKeys) + 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(version, seriesID, reqSigs, rawPubKeys) +} + +// ActivateSeries marks the series with the given ID as active. +func (p *Pool) ActivateSeries(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(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 (vp *Pool) ReplaceSeries(version, seriesID, reqSigs uint32, rawPubKeys []string) error { - series := vp.GetSeries(seriesID) +func (p *Pool) ReplaceSeries(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 managerError(waddrmgr.ErrSeriesNotExists, str, nil) + return newError(ErrSeriesNotExists, str, nil) } if series.IsEmpowered() { str := fmt.Sprintf("series #%d has private keys and cannot be replaced", seriesID) - return managerError(waddrmgr.ErrSeriesAlreadyEmpowered, str, nil) + return newError(ErrSeriesAlreadyEmpowered, str, nil) } - return vp.putSeries(version, seriesID, reqSigs, rawPubKeys) + return p.putSeries(version, seriesID, reqSigs, rawPubKeys) } // decryptExtendedKey uses Manager.Decrypt() to decrypt the encrypted byte slice and return // an extended (public or private) key representing it. -func (vp *Pool) decryptExtendedKey(keyType waddrmgr.CryptoKeyType, encrypted []byte) (*hdkeychain.ExtendedKey, error) { - decrypted, err := vp.manager.Decrypt(keyType, encrypted) +// +// 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, managerError(waddrmgr.ErrCrypto, str, err) + 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, managerError(waddrmgr.ErrKeyChain, str, err) + return nil, newError(ErrKeyChain, str, err) } return result, nil } @@ -338,17 +416,19 @@ func (vp *Pool) decryptExtendedKey(keyType waddrmgr.CryptoKeyType, encrypted []b // 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. -func validateAndDecryptKeys(rawPubKeys, rawPrivKeys [][]byte, vp *Pool) (pubKeys, privKeys []*hdkeychain.ExtendedKey, err error) { +// +// 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, managerError(waddrmgr.ErrKeysPrivatePublicMismatch, + 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 := vp.decryptExtendedKey(waddrmgr.CKTPublic, encryptedPub) + pubKey, err := p.decryptExtendedKey(waddrmgr.CKTPublic, encryptedPub) if err != nil { return nil, nil, err } @@ -359,7 +439,7 @@ func validateAndDecryptKeys(rawPubKeys, rawPrivKeys [][]byte, vp *Pool) (pubKeys if encryptedPriv == nil { privKey = nil } else { - privKey, err = vp.decryptExtendedKey(waddrmgr.CKTPrivate, encryptedPriv) + privKey, err = p.decryptExtendedKey(waddrmgr.CKTPrivate, encryptedPriv) if err != nil { return nil, nil, err } @@ -370,12 +450,12 @@ func validateAndDecryptKeys(rawPubKeys, rawPrivKeys [][]byte, vp *Pool) (pubKeys checkPubKey, err := privKey.Neuter() if err != nil { str := fmt.Sprintf("cannot neuter key %v", privKey) - return nil, nil, managerError(waddrmgr.ErrKeyNeuter, str, err) + 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, managerError(waddrmgr.ErrKeyMismatch, str, nil) + return nil, nil, newError(ErrKeyMismatch, str, nil) } } } @@ -387,11 +467,15 @@ func validateAndDecryptKeys(rawPubKeys, rawPrivKeys [][]byte, vp *Pool) (pubKeys // 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. -func (vp *Pool) LoadAllSeries() error { +// +// 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() error { var series map[uint32]*dbSeriesRow - err := vp.namespace.View(func(tx walletdb.Tx) error { + err := p.namespace.View(func(tx walletdb.Tx) error { var err error - series, err = loadAllSeries(tx, vp.ID) + series, err = loadAllSeries(tx, p.ID) return err }) if err != nil { @@ -399,11 +483,11 @@ func (vp *Pool) LoadAllSeries() error { } for id, series := range series { pubKeys, privKeys, err := validateAndDecryptKeys( - series.pubKeysEncrypted, series.privKeysEncrypted, vp) + series.pubKeysEncrypted, series.privKeysEncrypted, p) if err != nil { return err } - vp.seriesLookup[id] = &SeriesData{ + p.seriesLookup[id] = &SeriesData{ publicKeys: pubKeys, privateKeys: privKeys, reqSigs: series.reqSigs, @@ -412,41 +496,22 @@ func (vp *Pool) LoadAllSeries() error { return nil } -// existsSeries checks whether a series is stored in the database. -// Used solely by the series creation test. -func (vp *Pool) existsSeries(seriesID uint32) (bool, error) { - var exists bool - err := vp.namespace.View( - func(tx walletdb.Tx) error { - bucket := tx.RootBucket().Bucket(vp.ID) - if bucket == nil { - exists = false - return nil - } - exists = bucket.Get(uint32ToBytes(seriesID)) != nil - return nil - }) - if err != nil { - return false, err - } - return exists, 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 uint32) ([]*hdkeychain.ExtendedKey, error) { +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, managerError(waddrmgr.ErrInvalidValue, "pks cannot be nil", nil) + return nil, newError(ErrInvalidValue, "pks cannot be nil", nil) } - if branch > uint32(len(pks)) { - return nil, managerError(waddrmgr.ErrInvalidBranch, "branch number is bigger than number of public keys", nil) + if branch > Branch(len(pks)) { + return nil, newError( + ErrInvalidBranch, "branch number is bigger than number of public keys", nil) } if branch == 0 { @@ -472,27 +537,29 @@ func branchOrder(pks []*hdkeychain.ExtendedKey, branch uint32) ([]*hdkeychain.Ex return tmp, nil } -// DepositScriptAddress constructs a multi-signature redemption script using DepositScript -// and returns the pay-to-script-hash-address for that script. -func (vp *Pool) DepositScriptAddress(seriesID, branch, index uint32) (btcutil.Address, error) { - script, err := vp.DepositScript(seriesID, branch, index) +// 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 } - scriptHash := btcutil.Hash160(script) + return p.addressFor(script) +} - return btcutil.NewAddressScriptHashFromHash(scriptHash, - vp.manager.ChainParams()) +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 (vp *Pool) DepositScript(seriesID, branch, index uint32) ([]byte, error) { - series := vp.GetSeries(seriesID) +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, managerError(waddrmgr.ErrSeriesNotExists, str, nil) + return nil, newError(ErrSeriesNotExists, str, nil) } pubKeys, err := branchOrder(series.publicKeys, branch) @@ -502,68 +569,137 @@ func (vp *Pool) DepositScript(seriesID, branch, index uint32) ([]byte, error) { pks := make([]*btcutil.AddressPubKey, len(pubKeys)) for i, key := range pubKeys { - child, err := key.Child(index) + 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, managerError(waddrmgr.ErrKeyChain, str, err) + 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, managerError(waddrmgr.ErrKeyChain, str, err) + return nil, newError(ErrKeyChain, str, err) } pks[i], err = btcutil.NewAddressPubKey(pubkey.SerializeCompressed(), - vp.manager.ChainParams()) + 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, managerError(waddrmgr.ErrKeyChain, str, err) + 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, managerError(waddrmgr.ErrScriptCreation, str, err) + 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(seriesID uint32, branch Branch, index Index) ( + *WithdrawalAddress, error) { + // TODO: Ensure the given series is hot. + addr, err := p.getUsedAddr(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. -func (vp *Pool) EmpowerSeries(seriesID uint32, rawPrivKey string) error { +// +// This method must be called with the Pool's manager unlocked. +func (p *Pool) EmpowerSeries(seriesID uint32, rawPrivKey string) error { // make sure this series exists - series := vp.GetSeries(seriesID) + series := p.Series(seriesID) if series == nil { str := fmt.Sprintf("series %d does not exist for this voting pool", seriesID) - return managerError(waddrmgr.ErrSeriesNotExists, str, nil) + 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 managerError(waddrmgr.ErrKeyChain, str, err) + 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 managerError(waddrmgr.ErrKeyIsPublic, str, err) + 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 managerError(waddrmgr.ErrKeyNeuter, str, err) + return newError(ErrKeyNeuter, str, err) } lookingFor := pubKey.String() @@ -581,18 +717,153 @@ func (vp *Pool) EmpowerSeries(seriesID uint32, rawPrivKey string) error { if !found { str := fmt.Sprintf( "private Key does not have a corresponding public key in this series") - return managerError(waddrmgr.ErrKeysPrivatePublicMismatch, str, nil) + return newError(ErrKeysPrivatePublicMismatch, str, nil) } - err = vp.saveSeriesToDisk(seriesID, series) - - if err != nil { + if err = p.saveSeriesToDisk(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(seriesID uint32, branch Branch, index Index) error { + lastIdx, err := p.highestUsedIndexFor(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(seriesID, branch, lastIdx); err != nil { + return err + } + } + lastIdx++ + for lastIdx <= index { + if err := p.addUsedAddr(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(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. + _, err = p.manager.ImportScript(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 = p.namespace.Update( + func(tx walletdb.Tx) error { + return putUsedAddrHash(tx, 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(seriesID uint32, branch Branch, index Index) ( + waddrmgr.ManagedScriptAddress, error) { + + mgr := p.manager + var encryptedHash []byte + err := p.namespace.View( + func(tx walletdb.Tx) error { + encryptedHash = getUsedAddrHash(tx, p.ID, seriesID, branch, index) + return nil + }) + if err != nil { + return nil, newError(ErrDatabase, "failed to lookup script hash for used addr", err) + } + 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(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(seriesID uint32, branch Branch) (Index, error) { + maxIdx := Index(0) + err := p.namespace.View( + func(tx walletdb.Tx) error { + var err error + maxIdx, err = getMaxUsedIdx(tx, p.ID, seriesID, branch) + return err + }) + return maxIdx, err +} + +// 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 { @@ -604,8 +875,12 @@ func (s *SeriesData) IsEmpowered() bool { return false } -// managerError creates a waddrmgr.ManagerError given a set of arguments. -// XXX(lars): We should probably make our own votingpoolError function. -func managerError(c waddrmgr.ErrorCode, desc string, err error) waddrmgr.ManagerError { - return waddrmgr.ManagerError{ErrorCode: c, Description: desc, Err: err} +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 index f3d183f..4830dfd 100644 --- a/votingpool/pool_test.go +++ b/votingpool/pool_test.go @@ -20,136 +20,29 @@ import ( "bytes" "encoding/hex" "fmt" - "io/ioutil" - "os" - "path/filepath" "reflect" - "runtime" "testing" - "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcutil/hdkeychain" - "github.com/btcsuite/btcwallet/votingpool" + vp "github.com/btcsuite/btcwallet/votingpool" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/walletdb" _ "github.com/btcsuite/btcwallet/walletdb/bdb" ) -var fastScrypt = &waddrmgr.Options{ - ScryptN: 16, - ScryptR: 8, - ScryptP: 1, -} - -// checkManagerError ensures the passed error is a ManagerError with an error -// code that matches the passed error code. -func checkManagerError(t *testing.T, testName string, gotErr error, wantErrCode waddrmgr.ErrorCode) bool { - merr, ok := gotErr.(waddrmgr.ManagerError) - if !ok { - t.Errorf("%s: unexpected error type - got %T, want %T", - testName, gotErr, waddrmgr.ManagerError{}) - return false - } - if merr.ErrorCode != wantErrCode { - t.Errorf("%s: unexpected error code - got %s, want %s", - testName, merr.ErrorCode, wantErrCode) - return false - } - - return true -} - -const ( - privKey0 = "xprv9s21ZrQH143K2j9PK4CXkCu8sgxkpUxCF7p1KVwiV5tdnkeYzJXReUkxz5iB2FUzTXC1L15abCDG4RMxSYT5zhm67uvsnLYxuDhZfoFcB6a" - privKey1 = "xprv9s21ZrQH143K4PtW77ATQAKAGk7KAFFCzxFuAcWduoMEeQhCgWpuYWQvMGZknqdispUbgLZV1YPqFCbpzMJij8tSZ5xPSaZqPbchojeNuq7" - privKey2 = "xprv9s21ZrQH143K27XboWxXZGU5j7VZ9SqVBnmMQPKTbddiWAhuNzeLynKHaZTAti6N454tVUUcvy6u15DfuW68NCBUxry6ZsHHzqoA8UtzdMn" - privKey3 = "xprv9s21ZrQH143K2vb4DGQymRejLcZSksBHTYLxB7Stg1c7Lk9JxgEUGZTozwUKxoEWJPoGSdGnJY1TW7LNFQCWrpZjDdEXJeqJuDde6BmdD4P" - privKey4 = "xprv9s21ZrQH143K4JNmRvWeLc1PggzusKcDYV1y8fAMNDdb9Rm5X1AvGHizxEdhTVR3sc62XvifC6dLAXMuQesX1y6999xnDwQ3aVno8KviU9d" - privKey5 = "xprv9s21ZrQH143K3dxrqESqeHZ7pSwM6Uq77ssQADSBs7qdFs6dyRWmRcPyLUTQRpgB3EduNhJuWkCGG2LHjuUisw8KKfXJpPqYJ1MSPrZpe1z" - privKey6 = "xprv9s21ZrQH143K2nE8ENAMNksTTVxPrMxFNWUuwThMy2bcH9LHTtQDXSNq2pTNcbuq36n5A3J9pbXVqnq5LDXvqniFRLN299kW7Svnxsx9tQv" - privKey7 = "xprv9s21ZrQH143K3p93xF1oFeB6ey5ruUesWjuPxA9Z2R5wf6BLYfGXz7fg7NavWkQ2cx3Vm8w2HV9uKpSprNNHnenGeW9XhYDPSjwS9hyCs33" - privKey8 = "xprv9s21ZrQH143K3WxnnvPZ8SDGXndASvLTFwMLBVzNCVgs9rzP6rXgW92DLvozdyBm8T9bSQvrFm1jMpTJrRE6w1KY5tshFeDk9Nn3K6V5FYX" - - pubKey0 = "xpub661MyMwAqRbcFDDrR5jY7LqsRioFDwg3cLjc7tML3RRcfYyhXqqgCH5SqMSQdpQ1Xh8EtVwcfm8psD8zXKPcRaCVSY4GCqbb3aMEs27GitE" - pubKey1 = "xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9" - pubKey2 = "xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh" - pubKey3 = "xpub661MyMwAqRbcFQfXKHwz8ZbTtePwAKu8pmGYyVrWEM96DYUTWDYipMnHrFcemZHn13jcRMfsNU3UWQUudiaE7mhkWCHGFRMavF167DQM4Va" - pubKey4 = "xpub661MyMwAqRbcGnTEXx3ehjx8EiqQGnL4uhwZw3ZxvZAa2E6E4YVAp63UoVtvm2vMDDF8BdPpcarcf7PWcEKvzHhxzAYw1zG23C2egeh82AR" - pubKey5 = "xpub661MyMwAqRbcG83KwFyr1RVrNUmqVwYxV6nzxbqoRTNc8fRnWxq1yQiTBifTHhevcEM9ucZ1TqFS7Kv17Gd81cesv6RDrrvYS9SLPjPXhV5" - pubKey6 = "xpub661MyMwAqRbcFGJbLPhMjtpC1XntFpg6jjQWjr6yXN8b9wfS1RiU5EhJt5L7qoFuidYawc3XJoLjT2PcjVpXryS3hn1WmSPCyvQDNuKsfgM" - pubKey7 = "xpub661MyMwAqRbcGJDX4GYocn7qCzvMJwNisxpzkYZAakcvXtWV6CanXuz9xdfe5kTptFMJ4hDt2iTiT11zyN14u8R5zLvoZ1gnEVqNLxp1r3v" - pubKey8 = "xpub661MyMwAqRbcG13FtwvZVaA15pTerP4JdAGvytPykqDr2fKXePqw3wLhCALPAixsE176jFkc2ac9K3tnF4KwaTRKUqFF5apWD6XL9LHCu7E" -) - -var ( - // seed is the master seed used throughout the tests. - seed = []byte{ - 0x2a, 0x64, 0xdf, 0x08, 0x5e, 0xef, 0xed, 0xd8, 0xbf, - 0xdb, 0xb3, 0x31, 0x76, 0xb5, 0xba, 0x2e, 0x62, 0xe8, - 0xbe, 0x8b, 0x56, 0xc8, 0x83, 0x77, 0x95, 0x59, 0x8b, - 0xb6, 0xc4, 0x40, 0xc0, 0x64, - } - - pubPassphrase = []byte("_DJr{fL4H0O}*-0\n:V1izc)(6BomK") - privPassphrase = []byte("81lUHXnOMZ@?XXd7O9xyDIWIbXX-lj") -) - -func init() { - runtime.GOMAXPROCS(runtime.NumCPU()) -} - -func setUp(t *testing.T) (tearDownFunc func(), mgr *waddrmgr.Manager, pool *votingpool.Pool) { - 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) - } - mgrNamespace, err := db.Namespace([]byte("waddrmgr")) - if err != nil { - t.Fatalf("Failed to create addr manager DB namespace: %v", err) - } - mgr, err = waddrmgr.Create(mgrNamespace, seed, pubPassphrase, privPassphrase, - &chaincfg.MainNetParams, fastScrypt) - if err != nil { - t.Fatalf("Failed to create addr manager: %v", err) - } - - // Create a walletdb for votingpools. - vpNamespace, err := db.Namespace([]byte("votingpool")) - if err != nil { - t.Fatalf("Failed to create VotingPool DB namespace: %v", err) - } - pool, err = votingpool.Create(vpNamespace, mgr, []byte{0x00}) - if err != nil { - t.Fatalf("Voting Pool creation failed: %v", err) - } - tearDownFunc = func() { - db.Close() - mgr.Close() - os.RemoveAll(dir) - } - return tearDownFunc, mgr, pool -} - -func TestLoadVotingPoolAndDepositScript(t *testing.T) { - tearDown, manager, pool := setUp(t) +func TestLoadPoolAndDepositScript(t *testing.T) { + tearDown, manager, pool := vp.TstCreatePool(t) defer tearDown() // setup poolID := "test" - pubKeys := []string{pubKey0, pubKey1, pubKey2} - err := votingpool.LoadAndCreateSeries(pool.TstNamespace(), manager, 1, poolID, 0, 2, pubKeys) + pubKeys := vp.TstPubKeys[0:3] + err := vp.LoadAndCreateSeries(pool.TstNamespace(), manager, 1, poolID, 1, 2, pubKeys) if err != nil { t.Fatalf("Failed to create voting pool and series: %v", err) } // execute - script, err := votingpool.LoadAndGetDepositScript(pool.TstNamespace(), manager, poolID, 0, 0, 0) + script, err := vp.LoadAndGetDepositScript(pool.TstNamespace(), manager, poolID, 1, 0, 0) if err != nil { t.Fatalf("Failed to get deposit script: %v", err) } @@ -163,70 +56,69 @@ func TestLoadVotingPoolAndDepositScript(t *testing.T) { } } -func TestLoadVotingPoolAndCreateSeries(t *testing.T) { - tearDown, manager, pool := setUp(t) +func TestLoadPoolAndCreateSeries(t *testing.T) { + tearDown, manager, pool := vp.TstCreatePool(t) defer tearDown() poolID := "test" // first time, the voting pool is created - pubKeys := []string{pubKey0, pubKey1, pubKey2} - err := votingpool.LoadAndCreateSeries(pool.TstNamespace(), manager, 1, poolID, 0, 2, pubKeys) + pubKeys := vp.TstPubKeys[0:3] + err := vp.LoadAndCreateSeries(pool.TstNamespace(), 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 = []string{pubKey3, pubKey4, pubKey5} - err = votingpool.LoadAndCreateSeries(pool.TstNamespace(), manager, 1, poolID, 1, 2, pubKeys) + pubKeys = vp.TstPubKeys[3:6] + err = vp.LoadAndCreateSeries(pool.TstNamespace(), manager, 1, poolID, 2, 2, pubKeys) if err != nil { t.Fatalf("Loading voting pool and Creating series failed: %v", err) } } -func TestLoadVotingPoolAndReplaceSeries(t *testing.T) { - tearDown, manager, pool := setUp(t) +func TestLoadPoolAndReplaceSeries(t *testing.T) { + tearDown, manager, pool := vp.TstCreatePool(t) defer tearDown() // setup poolID := "test" - pubKeys := []string{pubKey0, pubKey1, pubKey2} - err := votingpool.LoadAndCreateSeries(pool.TstNamespace(), manager, 1, poolID, 0, 2, pubKeys) + pubKeys := vp.TstPubKeys[0:3] + err := vp.LoadAndCreateSeries(pool.TstNamespace(), manager, 1, poolID, 1, 2, pubKeys) if err != nil { t.Fatalf("Failed to create voting pool and series: %v", err) } - pubKeys = []string{pubKey3, pubKey4, pubKey5} - err = votingpool.LoadAndReplaceSeries(pool.TstNamespace(), manager, 1, poolID, 0, 2, pubKeys) + pubKeys = vp.TstPubKeys[3:6] + err = vp.LoadAndReplaceSeries(pool.TstNamespace(), manager, 1, poolID, 1, 2, pubKeys) if err != nil { t.Fatalf("Failed to replace series: %v", err) } } -func TestLoadVotingPoolAndEmpowerSeries(t *testing.T) { - tearDown, manager, pool := setUp(t) +func TestLoadPoolAndEmpowerSeries(t *testing.T) { + tearDown, manager, pool := vp.TstCreatePool(t) defer tearDown() // setup poolID := "test" - pubKeys := []string{pubKey0, pubKey1, pubKey2} - err := votingpool.LoadAndCreateSeries(pool.TstNamespace(), manager, 1, poolID, 0, 2, pubKeys) + pubKeys := vp.TstPubKeys[0:3] + err := vp.LoadAndCreateSeries(pool.TstNamespace(), manager, 1, poolID, 1, 2, pubKeys) if err != nil { t.Fatalf("Creating voting pool and Creating series failed: %v", err) } - // We need to unlock the manager in order to empower a series - manager.Unlock(privPassphrase) - - err = votingpool.LoadAndEmpowerSeries(pool.TstNamespace(), manager, poolID, 0, privKey0) + vp.TstRunWithManagerUnlocked(t, pool.Manager(), func() { + err = vp.LoadAndEmpowerSeries(pool.TstNamespace(), 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, _, pool := setUp(t) + tearDown, _, pool := vp.TstCreatePool(t) defer tearDown() tests := []struct { @@ -239,9 +131,9 @@ func TestDepositScriptAddress(t *testing.T) { }{ { version: 1, - series: 0, + series: 1, reqSigs: 2, - pubKeys: []string{pubKey0, pubKey1, pubKey2}, + pubKeys: vp.TstPubKeys[0:3], addresses: map[uint32]string{ 0: "3Hb4xcebcKg4DiETJfwjh8sF4uDw9rqtVC", 1: "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", @@ -257,7 +149,7 @@ func TestDepositScriptAddress(t *testing.T) { t.Fatalf("Cannot creates series %v", test.series) } for branch, expectedAddress := range test.addresses { - addr, err := pool.DepositScriptAddress(test.series, branch, 0) + 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) } @@ -271,77 +163,65 @@ func TestDepositScriptAddress(t *testing.T) { } func TestDepositScriptAddressForNonExistentSeries(t *testing.T) { - tearDown, _, pool := setUp(t) + tearDown, _, pool := vp.TstCreatePool(t) defer tearDown() - if _, err := pool.DepositScriptAddress(0, 0, 0); err == nil { - t.Fatalf("Expected an error, got none") - } else { - rerr := err.(waddrmgr.ManagerError) - if waddrmgr.ErrSeriesNotExists != rerr.ErrorCode { - t.Errorf("Got %v, want ErrSeriesNotExists", rerr.ErrorCode) - } - } + _, err := pool.DepositScriptAddress(1, 0, 0) + + vp.TstCheckError(t, "", err, vp.ErrSeriesNotExists) } func TestDepositScriptAddressForHardenedPubKey(t *testing.T) { - tearDown, _, pool := setUp(t) + tearDown, _, pool := vp.TstCreatePool(t) defer tearDown() - if err := pool.CreateSeries(1, 0, 2, []string{pubKey0, pubKey1, pubKey2}); err != nil { + if err := pool.CreateSeries(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(0, 0, uint32(hdkeychain.HardenedKeyStart+1)) + _, err := pool.DepositScriptAddress(1, 0, vp.Index(hdkeychain.HardenedKeyStart+1)) - if err == nil { - t.Fatalf("Expected an error, got none") - } else { - rerr := err.(waddrmgr.ManagerError) - if waddrmgr.ErrKeyChain != rerr.ErrorCode { - t.Errorf("Got %v, want ErrKeyChain", rerr.ErrorCode) - } - } + vp.TstCheckError(t, "", err, vp.ErrKeyChain) } -func TestLoadVotingPool(t *testing.T) { - tearDown, mgr, pool := setUp(t) +func TestLoadPool(t *testing.T) { + tearDown, mgr, pool := vp.TstCreatePool(t) defer tearDown() - pool2, err := votingpool.Load(pool.TstNamespace(), mgr, pool.ID) + pool2, err := vp.Load(pool.TstNamespace(), mgr, pool.ID) if err != nil { - t.Errorf("Error loading VotingPool: %v", err) + 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 TestCreateVotingPool(t *testing.T) { - tearDown, mgr, pool := setUp(t) +func TestCreatePool(t *testing.T) { + tearDown, mgr, pool := vp.TstCreatePool(t) defer tearDown() - pool2, err := votingpool.Create(pool.TstNamespace(), mgr, []byte{0x02}) + pool2, err := vp.Create(pool.TstNamespace(), mgr, []byte{0x02}) if err != nil { - t.Errorf("Error creating VotingPool: %v", err) + t.Errorf("Error creating Pool: %v", err) } if !bytes.Equal(pool2.ID, []byte{0x02}) { - t.Errorf("VotingPool ID mismatch: got %v, want %v", pool2.ID, []byte{0x02}) + t.Errorf("Pool ID mismatch: got %v, want %v", pool2.ID, []byte{0x02}) } } -func TestCreateVotingPoolWhenAlreadyExists(t *testing.T) { - tearDown, mgr, pool := setUp(t) +func TestCreatePoolWhenAlreadyExists(t *testing.T) { + tearDown, mgr, pool := vp.TstCreatePool(t) defer tearDown() - _, err := votingpool.Create(pool.TstNamespace(), mgr, pool.ID) + _, err := vp.Create(pool.TstNamespace(), mgr, pool.ID) - checkManagerError(t, "", err, waddrmgr.ErrVotingPoolAlreadyExists) + vp.TstCheckError(t, "", err, vp.ErrPoolAlreadyExists) } func TestCreateSeries(t *testing.T) { - tearDown, _, pool := setUp(t) + tearDown, _, pool := vp.TstCreatePool(t) defer tearDown() tests := []struct { @@ -350,31 +230,29 @@ func TestCreateSeries(t *testing.T) { reqSigs uint32 pubKeys []string }{ - { - version: 1, - series: 0, - reqSigs: 2, - pubKeys: []string{pubKey0, pubKey1, pubKey2}, - }, { version: 1, series: 1, - reqSigs: 3, - pubKeys: []string{pubKey0, pubKey1, pubKey2, pubKey3, pubKey4}, + reqSigs: 2, + pubKeys: vp.TstPubKeys[0:3], }, { version: 1, series: 2, - reqSigs: 4, - pubKeys: []string{pubKey0, pubKey1, pubKey2, pubKey3, pubKey4, - pubKey5, pubKey6}, + 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: []string{pubKey0, pubKey1, pubKey2, pubKey3, pubKey4, - pubKey5, pubKey6, pubKey7, pubKey8}, + pubKeys: vp.TstPubKeys[0:9], }, } @@ -393,237 +271,118 @@ func TestCreateSeries(t *testing.T) { } } -func TestCreateSeriesWhenAlreadyExists(t *testing.T) { - tearDown, _, pool := setUp(t) +func TestPoolCreateSeriesInvalidID(t *testing.T) { + tearDown, _, pool := vp.TstCreatePool(t) defer tearDown() - pubKeys := []string{pubKey0, pubKey1, pubKey2} - if err := pool.CreateSeries(1, 0, 1, pubKeys); err != nil { + + err := pool.CreateSeries(vp.CurrentVersion, 0, 1, vp.TstPubKeys[0:3]) + + vp.TstCheckError(t, "", err, vp.ErrSeriesIDInvalid) +} + +func TestPoolCreateSeriesWhenAlreadyExists(t *testing.T) { + tearDown, _, pool := vp.TstCreatePool(t) + defer tearDown() + pubKeys := vp.TstPubKeys[0:3] + if err := pool.CreateSeries(1, 1, 1, pubKeys); err != nil { t.Fatalf("Cannot create series: %v", err) } - err := pool.CreateSeries(1, 0, 1, pubKeys) + err := pool.CreateSeries(1, 1, 1, pubKeys) - checkManagerError(t, "", err, waddrmgr.ErrSeriesAlreadyExists) + vp.TstCheckError(t, "", err, vp.ErrSeriesAlreadyExists) +} + +func TestPoolCreateSeriesIDNotSequential(t *testing.T) { + tearDown, _, pool := vp.TstCreatePool(t) + defer tearDown() + + pubKeys := vp.TstPubKeys[0:4] + if err := pool.CreateSeries(1, 1, 2, pubKeys); err != nil { + t.Fatalf("Cannot create series: %v", err) + } + + err := pool.CreateSeries(1, 3, 2, pubKeys) + + vp.TstCheckError(t, "", err, vp.ErrSeriesIDNotSequential) } func TestPutSeriesErrors(t *testing.T) { - tearDown, _, pool := setUp(t) + tearDown, _, pool := vp.TstCreatePool(t) defer tearDown() tests := []struct { version uint32 reqSigs uint32 pubKeys []string - err waddrmgr.ManagerError + err vp.ErrorCode msg string }{ { - pubKeys: []string{pubKey0}, - err: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrTooFewPublicKeys}, + pubKeys: vp.TstPubKeys[0:1], + err: vp.ErrTooFewPublicKeys, msg: "Should return error when passed too few pubkeys", }, { reqSigs: 5, - pubKeys: []string{pubKey0, pubKey1, pubKey2}, - err: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrTooManyReqSignatures}, + pubKeys: vp.TstPubKeys[0:3], + err: vp.ErrTooManyReqSignatures, msg: "Should return error when reqSigs > len(pubKeys)", }, { - pubKeys: []string{pubKey0, pubKey1, pubKey2, pubKey0}, - err: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrKeyDuplicate}, + 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: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrKeyChain}, + err: vp.ErrKeyChain, msg: "Should return error when passed invalid pubkey", }, { - pubKeys: []string{privKey0, privKey1, privKey2}, - err: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrKeyIsPrivate}, + pubKeys: vp.TstPrivKeys[0:3], + err: vp.ErrKeyIsPrivate, msg: "Should return error when passed private keys", }, } for i, test := range tests { - err := pool.TstPutSeries(test.version, uint32(i), test.reqSigs, test.pubKeys) - if err == nil { - str := fmt.Sprintf(test.msg+" pubKeys: %v, reqSigs: %v", - test.pubKeys, test.reqSigs) - t.Errorf(str) - } else { - retErr := err.(waddrmgr.ManagerError) - if test.err.ErrorCode != retErr.ErrorCode { - t.Errorf( - "Create series #%d - Incorrect error type. Got %s, want %s", - i, retErr.ErrorCode, test.err.ErrorCode) - } - } + err := pool.TstPutSeries(test.version, uint32(i+1), test.reqSigs, test.pubKeys) + vp.TstCheckError(t, fmt.Sprintf("Create series #%d", i), err, test.err) } } -func TestValidateAndDecryptKeys(t *testing.T) { - tearDown, manager, pool := setUp(t) - defer tearDown() - - rawPubKeys, err := encryptKeys([]string{pubKey0, pubKey1}, manager, waddrmgr.CKTPublic) - if err != nil { - t.Fatalf("Failed to encrypt public keys: %v", err) - } - - // We need to unlock the manager in order to encrypt with the - // private key. - manager.Unlock(privPassphrase) - - rawPrivKeys, err := encryptKeys([]string{privKey0, ""}, manager, waddrmgr.CKTPrivate) - if err != nil { - t.Fatalf("Failed to encrypt private keys: %v", err) - } - - pubKeys, privKeys, err := votingpool.TstValidateAndDecryptKeys(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() != pubKey0 || pubKeys[1].String() != pubKey1 { - t.Fatalf("Public keys don't match: %v, %v", []string{pubKey0, pubKey1}, pubKeys) - } - - if privKeys[0].String() != privKey0 || privKeys[1] != nil { - t.Fatalf("Private keys don't match: %v, %v", []string{privKey0, ""}, 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, manager, pool := setUp(t) - defer tearDown() - - encryptedPubKeys, err := encryptKeys([]string{pubKey0}, manager, waddrmgr.CKTPublic) - if err != nil { - t.Fatalf("Failed to encrypt public key: %v", err) - } - - // We need to unlock the manager in order to encrypt with the - // private key. - manager.Unlock(privPassphrase) - - encryptedPrivKeys, err := encryptKeys([]string{privKey1}, manager, waddrmgr.CKTPrivate) - if err != nil { - t.Fatalf("Failed to encrypt private key: %v", err) - } - - tests := []struct { - rawPubKeys [][]byte - rawPrivKeys [][]byte - err waddrmgr.ErrorCode - }{ - { - // Number of public keys does not match number of private keys. - rawPubKeys: [][]byte{[]byte(pubKey0)}, - rawPrivKeys: [][]byte{}, - err: waddrmgr.ErrKeysPrivatePublicMismatch, - }, - { - // Failure to decrypt public key. - rawPubKeys: [][]byte{[]byte(pubKey0)}, - rawPrivKeys: [][]byte{[]byte(privKey0)}, - err: waddrmgr.ErrCrypto, - }, - { - // Failure to decrypt private key. - rawPubKeys: encryptedPubKeys, - rawPrivKeys: [][]byte{[]byte(privKey0)}, - err: waddrmgr.ErrCrypto, - }, - { - // One public and one private key, but they don't match. - rawPubKeys: encryptedPubKeys, - rawPrivKeys: encryptedPrivKeys, - err: waddrmgr.ErrKeyMismatch, - }, - } - - for i, test := range tests { - _, _, err := votingpool.TstValidateAndDecryptKeys(test.rawPubKeys, test.rawPrivKeys, pool) - - checkManagerError(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 -} - func TestCannotReplaceEmpoweredSeries(t *testing.T) { - tearDown, manager, pool := setUp(t) + tearDown, mgr, pool := vp.TstCreatePool(t) defer tearDown() - var seriesID uint32 = 1 + seriesID := uint32(1) - if err := pool.CreateSeries(1, seriesID, 3, []string{pubKey0, pubKey1, pubKey2, pubKey3}); err != nil { + if err := pool.CreateSeries(1, seriesID, 3, vp.TstPubKeys[0:4]); err != nil { t.Fatalf("Failed to create series: %v", err) } - // We need to unlock the manager in order to empower a series. - manager.Unlock(privPassphrase) - - if err := pool.EmpowerSeries(seriesID, privKey1); err != nil { - t.Fatalf("Failed to empower series: %v", err) - } - - if err := pool.ReplaceSeries(1, seriesID, 2, []string{pubKey0, pubKey2, pubKey3}); err == nil { - t.Errorf("Replaced an empowered series. That should not be possible: %v", err) - } else { - gotErr := err.(waddrmgr.ManagerError) - wantErrCode := waddrmgr.ErrorCode(waddrmgr.ErrSeriesAlreadyEmpowered) - if wantErrCode != gotErr.ErrorCode { - t.Errorf("Got %s, want %s", gotErr.ErrorCode, wantErrCode) + vp.TstRunWithManagerUnlocked(t, mgr, func() { + if err := pool.EmpowerSeries(seriesID, vp.TstPrivKeys[1]); err != nil { + t.Fatalf("Failed to empower series: %v", err) } - } + }) + + err := pool.ReplaceSeries(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, _, pool := setUp(t) + tearDown, _, pool := vp.TstCreatePool(t) defer tearDown() - pubKeys := []string{pubKey0, pubKey1, pubKey2} - if err := pool.ReplaceSeries(1, 1, 3, pubKeys); err == nil { - t.Errorf("Replaced non-existent series. This should not be possible.") - } else { - gotErr := err.(waddrmgr.ManagerError) - wantErrCode := waddrmgr.ErrorCode(waddrmgr.ErrSeriesNotExists) - if wantErrCode != gotErr.ErrorCode { - t.Errorf("Got %s, want %s", gotErr.ErrorCode, wantErrCode) - } - } + pubKeys := vp.TstPubKeys[0:3] + + err := pool.ReplaceSeries(1, 1, 3, pubKeys) + + vp.TstCheckError(t, "", err, vp.ErrSeriesNotExists) } type replaceSeriesTestEntry struct { @@ -636,18 +395,17 @@ var replaceSeriesTestData = []replaceSeriesTestEntry{ { testID: 0, orig: seriesRaw{ - id: 0, + id: 1, version: 1, reqSigs: 2, - pubKeys: votingpool.CanonicalKeyOrder( - []string{pubKey0, pubKey1, pubKey2, pubKey4}), + pubKeys: vp.CanonicalKeyOrder([]string{vp.TstPubKeys[0], vp.TstPubKeys[1], + vp.TstPubKeys[2], vp.TstPubKeys[4]}), }, replaceWith: seriesRaw{ - id: 0, + id: 1, version: 1, reqSigs: 1, - pubKeys: votingpool.CanonicalKeyOrder( - []string{pubKey3, pubKey4, pubKey5}), + pubKeys: vp.CanonicalKeyOrder(vp.TstPubKeys[3:6]), }, }, { @@ -656,36 +414,34 @@ var replaceSeriesTestData = []replaceSeriesTestEntry{ id: 2, version: 1, reqSigs: 2, - pubKeys: votingpool.CanonicalKeyOrder( - []string{pubKey0, pubKey1, pubKey2}), + pubKeys: vp.CanonicalKeyOrder(vp.TstPubKeys[0:3]), }, replaceWith: seriesRaw{ id: 2, version: 1, reqSigs: 2, - pubKeys: votingpool.CanonicalKeyOrder( - []string{pubKey3, pubKey4, pubKey5, pubKey6}), + pubKeys: vp.CanonicalKeyOrder(vp.TstPubKeys[3:7]), }, }, { testID: 2, orig: seriesRaw{ - id: 4, + id: 3, version: 1, reqSigs: 8, - pubKeys: votingpool.CanonicalKeyOrder([]string{pubKey0, pubKey1, pubKey2, pubKey3, pubKey4, pubKey5, pubKey6, pubKey7, pubKey8}), + pubKeys: vp.CanonicalKeyOrder(vp.TstPubKeys[0:9]), }, replaceWith: seriesRaw{ - id: 4, + id: 3, version: 1, reqSigs: 7, - pubKeys: votingpool.CanonicalKeyOrder([]string{pubKey0, pubKey1, pubKey2, pubKey3, pubKey4, pubKey5, pubKey6, pubKey7}), + pubKeys: vp.CanonicalKeyOrder(vp.TstPubKeys[0:8]), }, }, } func TestReplaceExistingSeries(t *testing.T) { - tearDown, _, pool := setUp(t) + tearDown, _, pool := vp.TstCreatePool(t) defer tearDown() for _, data := range replaceSeriesTestData { @@ -708,9 +464,9 @@ func TestReplaceExistingSeries(t *testing.T) { // validateReplaceSeries validate the created series stored in the system // corresponds to the series we replaced the original with. -func validateReplaceSeries(t *testing.T, pool *votingpool.Pool, testID int, replacedWith seriesRaw) { +func validateReplaceSeries(t *testing.T, pool *vp.Pool, testID int, replacedWith seriesRaw) { seriesID := replacedWith.id - series := pool.GetSeries(seriesID) + series := pool.Series(seriesID) if series == nil { t.Fatalf("Test #%d Series #%d: series not found", testID, seriesID) } @@ -736,100 +492,80 @@ func validateReplaceSeries(t *testing.T, pool *votingpool.Pool, testID int, repl } func TestEmpowerSeries(t *testing.T) { - tearDown, manager, pool := setUp(t) + tearDown, mgr, pool := vp.TstCreatePool(t) defer tearDown() - seriesID := uint32(0) - err := pool.CreateSeries(1, seriesID, 2, []string{pubKey0, pubKey1, pubKey2}) - if err != nil { + seriesID := uint32(1) + if err := pool.CreateSeries(1, seriesID, 2, vp.TstPubKeys[0:3]); err != nil { + t.Fatalf("Failed to create series: %v", err) + } + + vp.TstRunWithManagerUnlocked(t, mgr, func() { + if err := pool.EmpowerSeries(seriesID, vp.TstPrivKeys[0]); err != nil { + t.Errorf("Failed to empower series: %v", err) + } + }) +} + +func TestEmpowerSeriesErrors(t *testing.T) { + tearDown, _, pool := vp.TstCreatePool(t) + defer tearDown() + + seriesID := uint32(1) + if err := pool.CreateSeries(1, seriesID, 2, vp.TstPubKeys[0:3]); err != nil { t.Fatalf("Failed to create series: %v", err) } tests := []struct { seriesID uint32 key string - err error + err vp.ErrorCode }{ { - seriesID: 0, - key: privKey0, + seriesID: 2, + key: vp.TstPrivKeys[0], + // Invalid series. + err: vp.ErrSeriesNotExists, }, { - seriesID: 0, - key: privKey1, - }, - { - seriesID: 1, - key: privKey0, - // invalid series - err: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrSeriesNotExists}, - }, - { - seriesID: 0, + seriesID: seriesID, key: "NONSENSE", - // invalid private key - err: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrKeyChain}, + // Invalid private key. + err: vp.ErrKeyChain, }, { - seriesID: 0, - key: pubKey5, - // wrong type of key - err: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrKeyIsPublic}, + seriesID: seriesID, + key: vp.TstPubKeys[5], + // Wrong type of key. + err: vp.ErrKeyIsPublic, }, { - seriesID: 0, - key: privKey5, - // key not corresponding to pub key - err: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrKeysPrivatePublicMismatch}, + seriesID: seriesID, + key: vp.TstPrivKeys[5], + // Key not corresponding to public key. + err: vp.ErrKeysPrivatePublicMismatch, }, } - // We need to unlock the manager in order to empower a series. - manager.Unlock(privPassphrase) - - for testNum, test := range tests { - // Add the extended private key to voting pool. + for i, test := range tests { err := pool.EmpowerSeries(test.seriesID, test.key) - if test.err != nil { - if err == nil { - t.Errorf("EmpowerSeries #%d Expected an error and got none", testNum) - continue - } - if reflect.TypeOf(err) != reflect.TypeOf(test.err) { - t.Errorf("DepositScript #%d wrong error type. Got: %v <%T>, want: %T", - testNum, err, err, test.err) - continue - } - rerr := err.(waddrmgr.ManagerError) - trerr := test.err.(waddrmgr.ManagerError) - if rerr.ErrorCode != trerr.ErrorCode { - t.Errorf("DepositScript #%d wrong error code. Got: %v, want: %v", - testNum, rerr.ErrorCode, trerr.ErrorCode) - continue - } - continue - } - - if err != nil { - t.Errorf("EmpowerSeries #%d Unexpected error %v", testNum, err) - continue - } + vp.TstCheckError(t, fmt.Sprintf("EmpowerSeries #%d", i), err, test.err) } } -func TestGetSeries(t *testing.T) { - tearDown, _, pool := setUp(t) +func TestPoolSeries(t *testing.T) { + tearDown, _, pool := vp.TstCreatePool(t) defer tearDown() - expectedPubKeys := votingpool.CanonicalKeyOrder([]string{pubKey0, pubKey1, pubKey2}) - if err := pool.CreateSeries(1, 0, 2, expectedPubKeys); err != nil { + expectedPubKeys := vp.CanonicalKeyOrder(vp.TstPubKeys[0:3]) + if err := pool.CreateSeries(vp.CurrentVersion, 1, 2, expectedPubKeys); err != nil { t.Fatalf("Failed to create series: %v", err) } - series := pool.GetSeries(0) + series := pool.Series(1) if series == nil { - t.Fatal("GetSeries() returned nil") + t.Fatal("Series() returned nil") } pubKeys := series.TstGetRawPublicKeys() if !reflect.DeepEqual(pubKeys, expectedPubKeys) { @@ -855,24 +591,24 @@ var testLoadAllSeriesTests = []testLoadAllSeriesTest{ id: 1, series: []seriesRaw{ { - id: 0, + id: 1, version: 1, reqSigs: 2, - pubKeys: []string{pubKey0, pubKey1, pubKey2}, - }, - { - id: 1, - version: 1, - reqSigs: 2, - pubKeys: []string{pubKey3, pubKey4, pubKey5}, - privKeys: []string{privKey4}, + 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: []string{pubKey0, pubKey1, pubKey2, pubKey3, pubKey4}, - privKeys: []string{privKey0, privKey2}, + pubKeys: vp.TstPubKeys[0:5], + privKeys: []string{vp.TstPrivKeys[0], vp.TstPrivKeys[2]}, }, }, }, @@ -880,18 +616,18 @@ var testLoadAllSeriesTests = []testLoadAllSeriesTest{ id: 2, series: []seriesRaw{ { - id: 0, + id: 1, version: 1, reqSigs: 2, - pubKeys: []string{pubKey0, pubKey1, pubKey2}, + pubKeys: vp.TstPubKeys[0:3], }, }, }, } func setUpLoadAllSeries(t *testing.T, namespace walletdb.Namespace, mgr *waddrmgr.Manager, - test testLoadAllSeriesTest) *votingpool.Pool { - pool, err := votingpool.Create(namespace, mgr, []byte{byte(test.id + 1)}) + test testLoadAllSeriesTest) *vp.Pool { + pool, err := vp.Create(namespace, mgr, []byte{byte(test.id + 1)}) if err != nil { t.Fatalf("Voting Pool creation failed: %v", err) } @@ -905,38 +641,37 @@ func setUpLoadAllSeries(t *testing.T, namespace walletdb.Namespace, mgr *waddrmg } for _, privKey := range series.privKeys { - err := pool.EmpowerSeries(series.id, privKey) - if err != nil { - t.Fatalf("Test #%d Series #%d: empower with privKey %v failed: %v", - test.id, series.id, privKey, err) - } + vp.TstRunWithManagerUnlocked(t, mgr, func() { + if err := pool.EmpowerSeries(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, manager, pool := setUp(t) + tearDown, manager, pool := vp.TstCreatePool(t) defer tearDown() - // We need to unlock the manager in order to empower a series. - manager.Unlock(privPassphrase) - for _, test := range testLoadAllSeriesTests { pool := setUpLoadAllSeries(t, pool.TstNamespace(), manager, test) pool.TstEmptySeriesLookup() - err := pool.LoadAllSeries() - if err != nil { - t.Fatalf("Test #%d: failed to load voting pool: %v", test.id, err) - } + vp.TstRunWithManagerUnlocked(t, manager, func() { + if err := pool.LoadAllSeries(); 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 *votingpool.Pool, testID int, seriesData seriesRaw) { - series := pool.GetSeries(seriesData.id) +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 { @@ -957,7 +692,7 @@ func validateLoadAllSeries(t *testing.T, pool *votingpool.Pool, testID int, seri testID, seriesData.id, len(privateKeys), len(publicKeys)) } - sortedKeys := votingpool.CanonicalKeyOrder(seriesData.pubKeys) + 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) @@ -970,8 +705,8 @@ func validateLoadAllSeries(t *testing.T, pool *votingpool.Pool, testID int, seri foundPrivKeys = append(foundPrivKeys, privateKey) } } - foundPrivKeys = votingpool.CanonicalKeyOrder(foundPrivKeys) - privKeys := votingpool.CanonicalKeyOrder(seriesData.privKeys) + 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) @@ -992,7 +727,7 @@ func TestBranchOrderZero(t *testing.T) { for i := 0; i < 10; i++ { inKeys := createTestPubKeys(t, i, 0) wantKeys := reverse(inKeys) - resKeys, err := votingpool.TstBranchOrder(inKeys, 0) + resKeys, err := vp.TstBranchOrder(inKeys, 0) if err != nil { t.Fatalf("Error ordering keys: %v", err) } @@ -1005,7 +740,6 @@ func TestBranchOrderZero(t *testing.T) { for keyIdx := 0; i < len(inKeys); i++ { if resKeys[keyIdx] != wantKeys[keyIdx] { - fmt.Printf("%p, %p\n", resKeys[i], wantKeys[i]) t.Errorf("BranchOrder(keys, 0): got %v, want %v", resKeys[i], wantKeys[i]) } @@ -1028,7 +762,7 @@ func TestBranchOrderNonZero(t *testing.T) { inKeys := append(append(first, pivot...), last...) wantKeys := append(append(pivot, first...), last...) - resKeys, err := votingpool.TstBranchOrder(inKeys, uint32(branch)) + resKeys, err := vp.TstBranchOrder(inKeys, vp.Branch(branch)) if err != nil { t.Fatalf("Error ordering keys: %v", err) } @@ -1049,15 +783,15 @@ func TestBranchOrderNonZero(t *testing.T) { } func TestBranchOrderNilKeys(t *testing.T) { - _, err := votingpool.TstBranchOrder(nil, 1) + _, err := vp.TstBranchOrder(nil, 1) - checkManagerError(t, "", err, waddrmgr.ErrInvalidValue) + vp.TstCheckError(t, "", err, vp.ErrInvalidValue) } func TestBranchOrderInvalidBranch(t *testing.T) { - _, err := votingpool.TstBranchOrder(createTestPubKeys(t, 3, 0), 4) + _, err := vp.TstBranchOrder(createTestPubKeys(t, 3, 0), 4) - checkManagerError(t, "", err, waddrmgr.ErrInvalidBranch) + vp.TstCheckError(t, "", err, vp.ErrInvalidBranch) } func branchErrorFormat(orig, want, got []*hdkeychain.ExtendedKey) (origOrder, wantOrder, gotOrder []int) { @@ -1120,11 +854,11 @@ func TestReverse(t *testing.T) { } func TestEmpowerSeriesNeuterFailed(t *testing.T) { - tearDown, _, pool := setUp(t) + tearDown, _, pool := vp.TstCreatePool(t) defer tearDown() - seriesID := uint32(0) - err := pool.CreateSeries(1, seriesID, 2, []string{pubKey0, pubKey1, pubKey2}) + seriesID := uint32(1) + err := pool.CreateSeries(1, seriesID, 2, vp.TstPubKeys[0:3]) if err != nil { t.Fatalf("Failed to create series: %v", err) } @@ -1135,11 +869,11 @@ func TestEmpowerSeriesNeuterFailed(t *testing.T) { badKey := "wM5uZBNTYmaYGiK8VaGi7zPGbZGLuQgDiR2Zk4nGfbRFLXwHGcMUdVdazRpNHFSR7X7WLmzzbAq8dA1ViN6eWKgKqPye1rJTDQTvBiXvZ7E3nmdx" err = pool.EmpowerSeries(seriesID, badKey) - checkManagerError(t, "", err, waddrmgr.ErrKeyNeuter) + vp.TstCheckError(t, "", err, vp.ErrKeyNeuter) } func TestDecryptExtendedKeyCannotCreateResultKey(t *testing.T) { - tearDown, mgr, pool := setUp(t) + tearDown, mgr, pool := vp.TstCreatePool(t) defer tearDown() // the plaintext not being base58 encoded triggers the error @@ -1148,238 +882,63 @@ func TestDecryptExtendedKeyCannotCreateResultKey(t *testing.T) { t.Fatalf("Failed to encrypt plaintext: %v", err) } - if _, err := pool.TstDecryptExtendedKey(waddrmgr.CKTPublic, cipherText); err == nil { - t.Errorf("Expected function to fail, but it didn't") - } else { - gotErr := err.(waddrmgr.ManagerError) - wantErrCode := waddrmgr.ErrorCode(waddrmgr.ErrKeyChain) - if gotErr.ErrorCode != wantErrCode { - t.Errorf("Got %s, want %s", gotErr.ErrorCode, wantErrCode) - } - } + _, err = pool.TstDecryptExtendedKey(waddrmgr.CKTPublic, cipherText) + + vp.TstCheckError(t, "", err, vp.ErrKeyChain) } func TestDecryptExtendedKeyCannotDecrypt(t *testing.T) { - tearDown, _, pool := setUp(t) + tearDown, _, pool := vp.TstCreatePool(t) defer tearDown() - if _, err := pool.TstDecryptExtendedKey(waddrmgr.CKTPublic, []byte{}); err == nil { - t.Errorf("Expected function to fail, but it didn't") - } else { - gotErr := err.(waddrmgr.ManagerError) - wantErrCode := waddrmgr.ErrorCode(waddrmgr.ErrCrypto) - if gotErr.ErrorCode != wantErrCode { - t.Errorf("Got %s, want %s", gotErr.ErrorCode, wantErrCode) - } - } + _, err := pool.TstDecryptExtendedKey(waddrmgr.CKTPublic, []byte{}) + + vp.TstCheckError(t, "", err, vp.ErrCrypto) } -func TestSerializationErrors(t *testing.T) { - tearDown, mgr, _ := setUp(t) +func TestPoolChangeAddress(t *testing.T) { + tearDown, _, pool := vp.TstCreatePool(t) defer tearDown() - tests := []struct { - version uint32 - pubKeys []string - privKeys []string - reqSigs uint32 - err waddrmgr.ErrorCode - }{ - { - version: 2, - pubKeys: []string{pubKey0, pubKey1, pubKey2}, - err: waddrmgr.ErrSeriesVersion, - }, - { - pubKeys: []string{"NONSENSE"}, - // Not a valid length public key. - err: waddrmgr.ErrSeriesStorage, - }, - { - pubKeys: []string{pubKey0, pubKey1, pubKey2}, - privKeys: []string{privKey0}, - // The number of public and private keys should be the same. - err: waddrmgr.ErrSeriesStorage, - }, - { - pubKeys: []string{pubKey0}, - privKeys: []string{"NONSENSE"}, - // Not a valid length private key. - err: waddrmgr.ErrSeriesStorage, - }, - } + pubKeys := vp.TstPubKeys[1:4] + vp.TstCreateSeries(t, pool, []vp.TstSeriesDef{{ReqSigs: 2, PubKeys: pubKeys, SeriesID: 1}}) - // We need to unlock the manager in order to encrypt with the - // private key. - mgr.Unlock(privPassphrase) + addr := vp.TstNewChangeAddress(t, pool, 1, 0) + checkPoolAddress(t, addr, 1, 0, 0) - active := true - for testNum, test := range tests { - encryptedPubs, err := encryptKeys(test.pubKeys, mgr, waddrmgr.CKTPublic) - if err != nil { - t.Fatalf("Test #%d - Error encrypting pubkeys: %v", testNum, err) - } - encryptedPrivs, err := encryptKeys(test.privKeys, mgr, waddrmgr.CKTPrivate) - if err != nil { - t.Fatalf("Test #%d - Error encrypting privkeys: %v", testNum, err) - } - - _, err = votingpool.SerializeSeries( - test.version, active, test.reqSigs, encryptedPubs, encryptedPrivs) - - checkManagerError(t, fmt.Sprintf("Test #%d", testNum), err, test.err) - } + // When the series is not active, we should get an error. + pubKeys = vp.TstPubKeys[3:6] + vp.TstCreateSeries(t, pool, + []vp.TstSeriesDef{{ReqSigs: 2, PubKeys: pubKeys, SeriesID: 2, Inactive: true}}) + _, err := pool.ChangeAddress(2, 0) + vp.TstCheckError(t, "", err, vp.ErrSeriesNotActive) } -func TestSerialization(t *testing.T) { - tearDown, mgr, _ := setUp(t) +func TestPoolWithdrawalAddress(t *testing.T) { + tearDown, _, pool := vp.TstCreatePool(t) defer tearDown() - tests := []struct { - version uint32 - active bool - pubKeys []string - privKeys []string - reqSigs uint32 - }{ - { - version: 1, - active: true, - pubKeys: []string{pubKey0}, - reqSigs: 1, - }, - { - version: 0, - active: false, - pubKeys: []string{pubKey0}, - privKeys: []string{privKey0}, - reqSigs: 1, - }, - { - pubKeys: []string{pubKey0, pubKey1, pubKey2}, - privKeys: []string{privKey0, "", ""}, - reqSigs: 2, - }, - { - pubKeys: []string{pubKey0, pubKey1, pubKey2, pubKey3, pubKey4}, - reqSigs: 3, - }, - { - pubKeys: []string{pubKey0, pubKey1, pubKey2, pubKey3, pubKey4, pubKey5, pubKey6}, - privKeys: []string{"", privKey1, "", privKey3, "", "", ""}, - reqSigs: 4, - }, - } + pubKeys := vp.TstPubKeys[1:4] + vp.TstCreateSeries(t, pool, []vp.TstSeriesDef{{ReqSigs: 2, PubKeys: pubKeys, SeriesID: 1}}) + addr := vp.TstNewWithdrawalAddress(t, pool, 1, 0, 0) + checkPoolAddress(t, addr, 1, 0, 0) - // We need to unlock the manager in order to encrypt with the - // private key. - mgr.Unlock(privPassphrase) - - for testNum, test := range tests { - encryptedPubs, err := encryptKeys(test.pubKeys, mgr, waddrmgr.CKTPublic) - if err != nil { - t.Fatalf("Test #%d - Error encrypting pubkeys: %v", testNum, err) - } - encryptedPrivs, err := encryptKeys(test.privKeys, mgr, waddrmgr.CKTPrivate) - if err != nil { - t.Fatalf("Test #%d - Error encrypting privkeys: %v", testNum, err) - } - - serialized, err := votingpool.SerializeSeries( - test.version, test.active, test.reqSigs, encryptedPubs, encryptedPrivs) - if err != nil { - t.Fatalf("Test #%d - Error in serialization %v", testNum, err) - } - - row, err := votingpool.DeserializeSeries(serialized) - if err != nil { - t.Fatalf("Test #%d - Failed to deserialize %v %v", testNum, serialized, err) - } - - // TODO: Move all of these checks into one or more separate functions. - 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)) - } - } - } + // When the requested address is not present in the set of used addresses + // for that Pool, we should get an error. + _, err := pool.WithdrawalAddress(1, 2, 3) + vp.TstCheckError(t, "", err, vp.ErrWithdrawFromUnusedAddr) } -func TestDeserializationErrors(t *testing.T) { - tearDown, _, _ := setUp(t) - defer tearDown() +func checkPoolAddress(t *testing.T, addr vp.PoolAddress, seriesID uint32, branch vp.Branch, + index vp.Index) { - tests := []struct { - serialized []byte - err waddrmgr.ErrorCode - }{ - { - serialized: make([]byte, 1000000), - // Too many bytes (over waddrmgr.seriesMaxSerial). - err: waddrmgr.ErrSeriesStorage, - }, - { - serialized: make([]byte, 10), - // Not enough bytes (under waddrmgr.seriesMinSerial). - err: waddrmgr.ErrSeriesStorage, - }, - { - 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: waddrmgr.ErrSeriesStorage, - }, - { - serialized: []byte{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - // Unsupported version. - err: waddrmgr.ErrSeriesVersion, - }, + if addr.SeriesID() != seriesID { + t.Fatalf("Wrong SeriesID; got %d, want %d", addr.SeriesID(), seriesID) } - - for testNum, test := range tests { - _, err := votingpool.DeserializeSeries(test.serialized) - - checkManagerError(t, fmt.Sprintf("Test #%d", testNum), err, test.err) + 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 new file mode 100644 index 0000000..8d0df03 --- /dev/null +++ b/votingpool/pool_wb_test.go @@ -0,0 +1,463 @@ +/* + * Copyright (c) 2015 Conformal Systems LLC + * + * 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" + "testing" + + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/btcsuite/btcwallet/waddrmgr" +) + +func TestPoolEnsureUsedAddr(t *testing.T) { + tearDown, mgr, pool := TstCreatePool(t) + defer tearDown() + + var err error + var script []byte + var addr waddrmgr.ManagedScriptAddress + TstCreateSeries(t, pool, []TstSeriesDef{{ReqSigs: 2, PubKeys: TstPubKeys[0:3], SeriesID: 1}}) + + idx := Index(0) + TstRunWithManagerUnlocked(t, mgr, func() { + err = pool.EnsureUsedAddr(1, 0, idx) + }) + if err != nil { + t.Fatalf("Failed to ensure used addresses: %v", err) + } + addr, err = pool.getUsedAddr(1, 0, 0) + if err != nil { + t.Fatalf("Failed to get addr from used addresses set: %v", err) + } + TstRunWithManagerUnlocked(t, mgr, 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, mgr, func() { + err = pool.EnsureUsedAddr(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(1, 0, Index(i)) + if err != nil { + t.Fatalf("Failed to get addr from used addresses set: %v", err) + } + TstRunWithManagerUnlocked(t, mgr, 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, mgr, pool := TstCreatePool(t) + defer tearDown() + + TstCreateSeries(t, 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(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, mgr, func() { + err = pool.addUsedAddr(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(1, 0, 10) + if err != nil { + t.Fatalf("Error when looking up used addr: %v", err) + } + TstRunWithManagerUnlocked(t, mgr, 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, mgr, _ := TstCreatePool(t) + defer tearDown() + + 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, mgr, waddrmgr.CKTPublic) + if err != nil { + t.Fatalf("Test #%d - Error encrypting pubkeys: %v", testNum, err) + } + var encryptedPrivs [][]byte + TstRunWithManagerUnlocked(t, mgr, func() { + encryptedPrivs, err = encryptKeys(test.privKeys, mgr, 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, mgr, _ := TstCreatePool(t) + defer tearDown() + + 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, mgr, waddrmgr.CKTPublic) + if err != nil { + t.Fatalf("Test #%d - Error encrypting pubkeys: %v", testNum, err) + } + TstRunWithManagerUnlocked(t, mgr, func() { + encryptedPrivs, err = encryptKeys(test.privKeys, mgr, 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) { + tearDown, _, _ := TstCreatePool(t) + defer tearDown() + + 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, manager, pool := TstCreatePool(t) + defer tearDown() + + rawPubKeys, err := encryptKeys(TstPubKeys[0:2], manager, waddrmgr.CKTPublic) + if err != nil { + t.Fatalf("Failed to encrypt public keys: %v", err) + } + + var rawPrivKeys [][]byte + TstRunWithManagerUnlocked(t, manager, func() { + rawPrivKeys, err = encryptKeys([]string{TstPrivKeys[0], ""}, manager, waddrmgr.CKTPrivate) + }) + if err != nil { + t.Fatalf("Failed to encrypt private keys: %v", err) + } + + var pubKeys, privKeys []*hdkeychain.ExtendedKey + TstRunWithManagerUnlocked(t, manager, 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, manager, pool := TstCreatePool(t) + defer tearDown() + + encryptedPubKeys, err := encryptKeys(TstPubKeys[0:1], manager, waddrmgr.CKTPublic) + if err != nil { + t.Fatalf("Failed to encrypt public key: %v", err) + } + + var encryptedPrivKeys [][]byte + TstRunWithManagerUnlocked(t, manager, func() { + encryptedPrivKeys, err = encryptKeys(TstPrivKeys[1:2], 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, manager, 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 index 34ed6db..0d02016 100644 --- a/votingpool/test_coverage.txt +++ b/votingpool/test_coverage.txt @@ -1,92 +1,142 @@ -github.com/conformal/btcwallet/votingpool/db.go serializeSeriesRow 100.00% (29/29) -github.com/conformal/btcwallet/votingpool/pool.go branchOrder 100.00% (19/19) -github.com/conformal/btcwallet/votingpool/pool.go convertAndValidatePubKeys 100.00% (16/16) -github.com/conformal/btcwallet/votingpool/input_selection.go Credits.Less 100.00% (12/12) -github.com/conformal/btcwallet/votingpool/pool.go Pool.decryptExtendedKey 100.00% (10/10) -github.com/conformal/btcwallet/votingpool/pool.go Pool.ReplaceSeries 100.00% (8/8) -github.com/conformal/btcwallet/votingpool/input_selection.go AddressRange.NumAddresses 100.00% (7/7) -github.com/conformal/btcwallet/votingpool/pool.go Create 100.00% (5/5) -github.com/conformal/btcwallet/votingpool/db.go putPool 100.00% (5/5) -github.com/conformal/btcwallet/votingpool/pool.go Pool.DepositScriptAddress 100.00% (5/5) -github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.addTxIn 100.00% (4/4) -github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.addTxOut 100.00% (4/4) -github.com/conformal/btcwallet/votingpool/pool.go CanonicalKeyOrder 100.00% (4/4) -github.com/conformal/btcwallet/votingpool/pool.go @81:3 100.00% (4/4) -github.com/conformal/btcwallet/votingpool/pool.go Pool.GetSeries 100.00% (4/4) -github.com/conformal/btcwallet/votingpool/pool.go seriesData.IsEmpowered 100.00% (4/4) -github.com/conformal/btcwallet/votingpool/pool.go Pool.CreateSeries 100.00% (4/4) -github.com/conformal/btcwallet/votingpool/pool.go Pool.existsSeries 100.00% (3/3) -github.com/conformal/btcwallet/votingpool/db.go uint32ToBytes 100.00% (3/3) -github.com/conformal/btcwallet/votingpool/pool.go @398:27 100.00% (3/3) -github.com/conformal/btcwallet/votingpool/pool.go zero 100.00% (2/2) -github.com/conformal/btcwallet/votingpool/db.go putSeries 100.00% (2/2) -github.com/conformal/btcwallet/votingpool/db.go existsPool 100.00% (2/2) -github.com/conformal/btcwallet/votingpool/withdrawal.go Ntxid 100.00% (2/2) -github.com/conformal/btcwallet/votingpool/withdrawal.go NewOutputRequest 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.Index 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/withdrawal.go init 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/withdrawal.go estimateSize 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/withdrawal.go calculateFee 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/db.go bytesToUint32 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.isTooBig 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/error.go newError 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/input_selection.go Credit.TxSha 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/input_selection.go Credit.OutputIndex 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/input_selection.go Credit.Address 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/input_selection.go newCredit 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/input_selection.go Credits.Len 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/input_selection.go Credits.Swap 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Outpoints 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Address 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/input_selection.go Pool.isCharterOutput 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/pool.go @67:3 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/pool.go newPool 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Status 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.addOutpoint 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalStatus.Outputs 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Amount 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/withdrawal.go ChangeAddress.Next 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/pool.go @205:28 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.Addr 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.SeriesID 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/pool.go managerError 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.Branch 100.00% (1/1) -github.com/conformal/btcwallet/votingpool/pool.go Pool.EmpowerSeries 96.43% (27/28) -github.com/conformal/btcwallet/votingpool/db.go deserializeSeriesRow 94.87% (37/39) -github.com/conformal/btcwallet/votingpool/pool.go Pool.putSeries 93.75% (15/16) -github.com/conformal/btcwallet/votingpool/pool.go validateAndDecryptKeys 92.31% (24/26) -github.com/conformal/btcwallet/votingpool/input_selection.go Pool.getEligibleInputsFromSeries 86.36% (19/22) -github.com/conformal/btcwallet/votingpool/input_selection.go Pool.getEligibleInputs 85.71% (6/7) -github.com/conformal/btcwallet/votingpool/pool.go Load 85.71% (6/7) -github.com/conformal/btcwallet/votingpool/input_selection.go Pool.isCreditEligible 85.71% (6/7) -github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.finalizeCurrentTx 84.21% (16/19) -github.com/conformal/btcwallet/votingpool/input_selection.go groupCreditsByAddr 83.33% (10/12) -github.com/conformal/btcwallet/votingpool/db.go loadAllSeries 83.33% (5/6) -github.com/conformal/btcwallet/votingpool/withdrawal.go Pool.ChangeAddress 83.33% (5/6) -github.com/conformal/btcwallet/votingpool/pool.go LoadAndCreateSeries 80.00% (8/10) -github.com/conformal/btcwallet/votingpool/pool.go Pool.LoadAllSeries 80.00% (8/10) -github.com/conformal/btcwallet/votingpool/pool.go LoadAndEmpowerSeries 80.00% (4/5) -github.com/conformal/btcwallet/votingpool/pool.go LoadAndReplaceSeries 80.00% (4/5) -github.com/conformal/btcwallet/votingpool/withdrawal.go Pool.WithdrawalAddress 80.00% (4/5) -github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.sign 75.76% (25/33) -github.com/conformal/btcwallet/votingpool/pool.go LoadAndGetDepositScript 75.00% (6/8) -github.com/conformal/btcwallet/votingpool/withdrawal.go OutputRequest.pkScript 75.00% (3/4) -github.com/conformal/btcwallet/votingpool/pool.go Pool.DepositScript 73.08% (19/26) -github.com/conformal/btcwallet/votingpool/withdrawal.go ValidateSigScripts 72.73% (8/11) -github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.fulfilNextOutput 72.41% (21/29) -github.com/conformal/btcwallet/votingpool/withdrawal.go SignMultiSigUTXO 71.43% (15/21) -github.com/conformal/btcwallet/votingpool/db.go @77:3 71.43% (5/7) -github.com/conformal/btcwallet/votingpool/withdrawal.go getRedeemScript 71.43% (5/7) -github.com/conformal/btcwallet/votingpool/pool.go Pool.saveSeriesToDisk 70.00% (14/20) -github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.fulfilOutputs 70.00% (7/10) -github.com/conformal/btcwallet/votingpool/withdrawal.go getPrivKey 70.00% (7/10) -github.com/conformal/btcwallet/votingpool/withdrawal.go Pool.Withdrawal 66.67% (12/18) -github.com/conformal/btcwallet/votingpool/pool.go @426:3 66.67% (4/6) -github.com/conformal/btcwallet/votingpool/error.go ErrorCode.String 66.67% (2/3) -github.com/conformal/btcwallet/votingpool/db.go putSeriesRow 53.85% (7/13) -github.com/conformal/btcwallet/votingpool/error.go Error.Error 0.00% (0/3) -github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.updateStatusFor 0.00% (0/0) -github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.rollBackLastOutput 0.00% (0/0) -github.com/conformal/btcwallet/votingpool -------------------------------- 85.36% (554/649) +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 new file mode 100644 index 0000000..7ecf25d --- /dev/null +++ b/votingpool/test_data_test.go @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2015 Conformal Systems LLC + * + * 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 + +// 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 new file mode 100644 index 0000000..4a88687 --- /dev/null +++ b/votingpool/withdrawal.go @@ -0,0 +1,919 @@ +/* + * Copyright (c) 2015 Conformal Systems LLC + * + * 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" + "math" + "sort" + "strconv" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/txstore" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/fastsha256" +) + +// 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 fulfil 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 +} + +// 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] +} + +// 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 := fastsha256.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 +} + +// 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 +} + +func newWithdrawalTx() *withdrawalTx { + return &withdrawalTx{} +} + +// 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 + } + // Ignore the error as TxSha() can't fail. + sha, _ := msgtx.TxSha() + return Ntxid(sha.String()) +} + +// 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() + 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{})) + } + 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 = calculateTxFee(tx) + 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()+calculateTxFee(tx) { + 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 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, + current: newWithdrawalTx(), + pendingRequests: requests, + eligibleInputs: inputs, + status: status, + } +} + +// 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 +func (p *Pool) StartWithdrawal(roundID uint32, requests []OutputRequest, + startAddress WithdrawalAddress, lastSeriesID uint32, changeStart ChangeAddress, + txStore *txstore.Store, chainHeight int32, dustThreshold btcutil.Amount) ( + *WithdrawalStatus, error) { + + eligible, err := p.getEligibleInputs(txStore, 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 + } + + 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[0] + w.eligibleInputs = w.eligibleInputs[1:] + return input +} + +// pushInput adds a new input to the top of the stack of eligible inputs. +// TODO: Reverse the stack semantics here as the current one generates a lot of +// extra garbage since it always creates a new single-element slice and append +// the rest of the items to it. +func (w *withdrawal) pushInput(input Credit) { + w.eligibleInputs = append([]Credit{input}, w.eligibleInputs...) +} + +// 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 isTxTooBig(w.current) { + return w.handleOversizeTx() + } + + fee := calculateTxFee(w.current) + 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 = calculateTxFee(w.current) + + if isTxTooBig(w.current) { + 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() + 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)) + + 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()+calculateTxFee(tx) { + // 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() + calculateTxFee(tx) - 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 + } +} + +// 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.Address() + 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, store *txstore.Store) error { + credits, err := store.FindPreviousCredits(btcutil.NewTx(msgtx)) + for i, credit := range credits { + if err = signMultiSigUTXO(mgr, msgtx, i, credit.TxOut().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, addr *btcutil.AddressScriptHash) ([]byte, error) { + address, err := mgr.Address(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, 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, 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 { + txIn := msgtx.TxIn[idx] + engine, err := txscript.NewScript( + txIn.SignatureScript, pkScript, idx, msgtx, txscript.StandardVerifyFlags) + if err != nil { + return newError(ErrTxSigning, "cannot create script engine", err) + } + if err = engine.Execute(); err != nil { + return newError(ErrTxSigning, "cannot validate tx signature", err) + } + return nil +} + +// calculateTxFee calculates the expected network fees for a given tx. We use +// a variable instead of a function so that it can be replaced in tests. +var calculateTxFee = func(tx *withdrawalTx) btcutil.Amount { + return btcutil.Amount(1+calculateTxSize(tx)/1000) * feeIncrement +} + +// isTxTooBig returns true if the size (in bytes) of the given tx is greater +// than or equal to txMaxSize. It is defined as a variable so it can be +// replaced for testing purposes. +var isTxTooBig = func(tx *withdrawalTx) 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 calculateTxSize(tx) >= txMaxSize +} + +// calculateTxSize returns an estimate of the serialized size (in bytes) of the +// given transaction. It assumes all tx inputs are P2SH multi-sig. We use a +// variable instead of a function so that it can be replaced in tests. +var calculateTxSize = func(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].Address() + 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 +} diff --git a/votingpool/withdrawal_test.go b/votingpool/withdrawal_test.go new file mode 100644 index 0000000..7ba6c89 --- /dev/null +++ b/votingpool/withdrawal_test.go @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * 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" + "testing" + + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" + vp "github.com/btcsuite/btcwallet/votingpool" +) + +func TestStartWithdrawal(t *testing.T) { + tearDown, pool, store := vp.TstCreatePoolAndTxStore(t) + defer tearDown() + 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, pool, []vp.TstSeriesDef{def}) + // Create eligible inputs and the list of outputs we need to fulfil. + vp.TstCreateCreditsOnSeries(t, 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, pool, def.SeriesID, 0, 0) + lastSeriesID := def.SeriesID + dustThreshold := btcutil.Amount(1e4) + currentBlock := int32(vp.TstInputsBlock + vp.TstEligibleInputMinConfirmations + 1) + var status *vp.WithdrawalStatus + var err error + vp.TstRunWithManagerUnlocked(t, mgr, func() { + status, err = pool.StartWithdrawal(0, requests, *startAddr, lastSeriesID, *changeStart, + store, 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, func() { + if err = vp.SignTx(msgtx, txSigs, mgr, store); err != nil { + t.Fatal(err) + } + }) +} + +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 new file mode 100644 index 0000000..21bca8c --- /dev/null +++ b/votingpool/withdrawal_wb_test.go @@ -0,0 +1,1170 @@ +/* + * Copyright (c) 2015 Conformal Systems LLC + * + * 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" + "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/txstore" + "github.com/btcsuite/btcwallet/waddrmgr" +) + +// TestOutputSplittingNotEnoughInputs checks that an output will get split if we +// don't have enough inputs to fulfil it. +func TestOutputSplittingNotEnoughInputs(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + 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 := TstCreateCredits(t, pool, []int64{7}, store) + w := newWithdrawal(0, requests, eligible, *TstNewChangeAddress(t, pool, seriesID, 0)) + + // 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. + restoreCalculateTxFee := replaceCalculateTxFee(TstConstantFee(3)) + defer restoreCalculateTxFee() + 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 - calculateTxFee(tx) + checkLastOutputWasSplit(t, w, tx, output2Amount, newAmount) +} + +func TestOutputSplittingOversizeTx(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + requestAmount := btcutil.Amount(5) + bigInput := int64(3) + smallInput := int64(2) + request := TstNewOutputRequest( + t, 1, "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", requestAmount, pool.Manager().ChainParams()) + seriesID, eligible := TstCreateCredits(t, pool, []int64{bigInput, smallInput}, store) + changeStart := TstNewChangeAddress(t, pool, seriesID, 0) + w := newWithdrawal(0, []OutputRequest{request}, eligible, *changeStart) + restoreCalculateTxFee := replaceCalculateTxFee(TstConstantFee(0)) + defer restoreCalculateTxFee() + restoreIsTxTooBig := replaceIsTxTooBig(func(tx *withdrawalTx) bool { + // Trigger an output split right after the second input is added. + return len(tx.inputs) == 2 + }) + defer restoreIsTxTooBig() + + 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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + w := newWithdrawal(0, []OutputRequest{}, []Credit{}, ChangeAddress{}) + w.current = createWithdrawalTx(t, pool, store, []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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + net := pool.Manager().ChainParams() + + // Create eligible inputs and the list of outputs we need to fulfil. + seriesID, eligible := TstCreateCredits(t, pool, []int64{2e6, 4e6}, store) + 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 + calculateTxFee(tx)) + 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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + seriesID, eligible := TstCreateCredits(t, pool, []int64{1e6}, store) + 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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + net := pool.Manager().ChainParams() + + // Create eligible inputs and the list of outputs we need to fulfil. + seriesID, eligible := TstCreateCredits(t, pool, []int64{2e6, 4e6}, store) + 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 + calculateTxFee(tx)) + 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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []int64{3, 3, 2, 1, 3}, []int64{3, 3, 2, 2}) + initialInputs := tx.inputs + initialOutputs := tx.outputs + + restoreCalcTxFee := replaceCalculateTxFee(TstConstantFee(1)) + defer restoreCalcTxFee() + 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 removedInputs[0] != lastInput { + t.Fatalf("Wrong rolled back input; got %s want %s", 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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + // 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, pool, store, []int64{1, 2, 3, 4}, []int64{1, 8}) + initialInputs := tx.inputs + initialOutputs := tx.outputs + + restoreCalcTxFee := replaceCalculateTxFee(TstConstantFee(0)) + defer restoreCalcTxFee() + 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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []int64{4}, []int64{2, 3}) + initialInputs := tx.inputs + initialOutputs := tx.outputs + + restoreCalcTxFee := replaceCalculateTxFee(TstConstantFee(1)) + defer restoreCalcTxFee() + 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() + _, _, 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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + net := pool.Manager().ChainParams() + series, eligible := TstCreateCredits(t, pool, []int64{5, 5}, store) + 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) + restoreCalculateTxFee := replaceCalculateTxFee(TstConstantFee(0)) + defer restoreCalculateTxFee() + restoreIsTxTooBig := replaceIsTxTooBig(func(tx *withdrawalTx) bool { + return len(tx.outputs) > 1 + }) + defer restoreIsTxTooBig() + + 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{&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{&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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + net := pool.Manager().ChainParams() + series, eligible := TstCreateCredits(t, pool, []int64{1, 2, 3, 4, 5, 6}, store) + 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) + restoreCalculateTxFee := replaceCalculateTxFee(TstConstantFee(0)) + defer restoreCalculateTxFee() + restoreIsTxTooBig := replaceIsTxTooBig(func(tx *withdrawalTx) bool { + // Make a transaction too big as soon as a fourth input is added to it. + return len(tx.inputs) > 3 + }) + defer restoreIsTxTooBig() + + // 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 list of eligible inputs, and no change output. + firstTx := w.transactions[0] + req1 := requests[0] + checkTxOutputs(t, firstTx, + []*withdrawalTxOut{&withdrawalTxOut{request: req1, amount: req1.Amount}}) + checkTxInputs(t, firstTx, eligible[0:1]) + + // 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 (also in the same order as they were passed to + // newWithdrawal) and no change output. + secondTx := w.transactions[1] + wantOutputs := []*withdrawalTxOut{ + &withdrawalTxOut{request: requests[1], amount: requests[1].Amount}, + &withdrawalTxOut{request: requests[2], amount: requests[2].Amount}} + checkTxOutputs(t, secondTx, wantOutputs) + checkTxInputs(t, secondTx, eligible[1:4]) +} + +func TestWithdrawalTxRemoveOutput(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []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 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 tx.inputs[0] != remainingInput { + t.Fatalf("Wrong input: got %v, want %v", tx.inputs[0], remainingInput) + } +} + +func TestWithdrawalTxAddChange(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + input, output, fee := int64(4e6), int64(3e6), int64(10) + tx := createWithdrawalTx(t, pool, store, []int64{input}, []int64{output}) + restoreCalcTxFee := replaceCalculateTxFee(TstConstantFee(btcutil.Amount(fee))) + defer restoreCalcTxFee() + + 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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + input, output, fee := int64(4e6), int64(4e6), int64(0) + tx := createWithdrawalTx(t, pool, store, []int64{input}, []int64{output}) + restoreCalcTxFee := replaceCalculateTxFee(TstConstantFee(btcutil.Amount(fee))) + defer restoreCalcTxFee() + + 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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []int64{}, []int64{}) + msgtx := tx.toMsgTx() + compareMsgTxAndWithdrawalTxOutputs(t, msgtx, tx) + compareMsgTxAndWithdrawalTxInputs(t, msgtx, tx) +} + +func TestWithdrawalTxToMsgTxNoInputsOrOutputsWithChange(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []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, pool, store := TstCreatePoolAndTxStore(t) + + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []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 TestSignMultiSigUTXO(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + // Create a new tx with a single input that we're going to sign. + mgr := pool.Manager() + tx := createWithdrawalTx(t, pool, store, []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].TxOut().PkScript + TstRunWithManagerUnlocked(t, mgr, func() { + if err = signMultiSigUTXO(mgr, msgtx, idx, pkScript, txSigs[idx]); err != nil { + t.Fatal(err) + } + }) +} + +func TestSignMultiSigUTXOUnparseablePkScript(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + mgr := pool.Manager() + tx := createWithdrawalTx(t, pool, store, []int64{4e6}, []int64{}) + msgtx := tx.toMsgTx() + + unparseablePkScript := []byte{0x01} + err := signMultiSigUTXO(mgr, msgtx, 0, unparseablePkScript, []RawSig{RawSig{}}) + + TstCheckError(t, "", err, ErrTxSigning) +} + +func TestSignMultiSigUTXOPkScriptNotP2SH(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + mgr := pool.Manager() + tx := createWithdrawalTx(t, pool, store, []int64{4e6}, []int64{}) + addr, _ := btcutil.DecodeAddress("1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX", mgr.ChainParams()) + pubKeyHashPkScript, _ := txscript.PayToAddrScript(addr.(*btcutil.AddressPubKeyHash)) + msgtx := tx.toMsgTx() + + err := signMultiSigUTXO(mgr, msgtx, 0, pubKeyHashPkScript, []RawSig{RawSig{}}) + + TstCheckError(t, "", err, ErrTxSigning) +} + +func TestSignMultiSigUTXORedeemScriptNotFound(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + mgr := pool.Manager() + tx := createWithdrawalTx(t, pool, store, []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(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, msgtx, 0, pkScript, []RawSig{RawSig{}}) + + TstCheckError(t, "", err, ErrTxSigning) +} + +func TestSignMultiSigUTXONotEnoughSigs(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + mgr := pool.Manager() + tx := createWithdrawalTx(t, pool, store, []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].Address().series().TstGetReqSigs() + txInSigs := txSigs[idx][:reqSigs-1] + pkScript := tx.inputs[idx].TxOut().PkScript + TstRunWithManagerUnlocked(t, mgr, func() { + err = signMultiSigUTXO(mgr, msgtx, idx, pkScript, txInSigs) + }) + + TstCheckError(t, "", err, ErrTxSigning) +} + +func TestSignMultiSigUTXOWrongRawSigs(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + mgr := pool.Manager() + tx := createWithdrawalTx(t, pool, store, []int64{4e6}, []int64{}) + sigs := []RawSig{RawSig{0x00}, RawSig{0x01}} + + idx := 0 // The index of the tx input we're going to sign. + pkScript := tx.inputs[idx].TxOut().PkScript + var err error + TstRunWithManagerUnlocked(t, mgr, func() { + err = signMultiSigUTXO(mgr, tx.toMsgTx(), idx, pkScript, sigs) + }) + + TstCheckError(t, "", err, ErrTxSigning) +} + +func TestGetRawSigs(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []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].Address().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(), msgtx, txSigs, tx.inputs) +} + +func TestGetRawSigsOnlyOnePrivKeyAvailable(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []int64{5e6, 4e6}, []int64{}) + // Remove all private keys but the first one from the credit's series. + series := tx.inputs[0].Address().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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []int64{5e6, 4e6}, []int64{}) + // Change the redeem script for one of our tx inputs, to force an error in + // getRawSigs(). + tx.inputs[0].Address().script = []byte{0x01} + + _, err := getRawSigs([]*withdrawalTx{tx}) + + TstCheckError(t, "", err, ErrRawSigning) +} + +func TestGetRawSigsInvalidAddrBranch(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []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].Address().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, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []int64{5}, []int64{1}) + + restoreCalcTxSize := replaceCalculateTxSize(func(tx *withdrawalTx) int { return txMaxSize - 1 }) + if isTxTooBig(tx) { + t.Fatalf("Tx is smaller than max size (%d < %d) but was considered too big", + calculateTxSize(tx), txMaxSize) + } + restoreCalcTxSize() + + // A tx whose size is equal to txMaxSize should be considered too big. + restoreCalcTxSize = replaceCalculateTxSize(func(tx *withdrawalTx) int { return txMaxSize }) + if !isTxTooBig(tx) { + t.Fatalf("Tx size is equal to the max size (%d == %d) but was not considered too big", + calculateTxSize(tx), txMaxSize) + } + restoreCalcTxSize() + + restoreCalcTxSize = replaceCalculateTxSize(func(tx *withdrawalTx) int { return txMaxSize + 1 }) + if !isTxTooBig(tx) { + t.Fatalf("Tx size is bigger than max size (%d > %d) but was not considered too big", + calculateTxSize(tx), txMaxSize) + } + restoreCalcTxSize() +} + +func TestTxSizeCalculation(t *testing.T) { + tearDown, pool, store := TstCreatePoolAndTxStore(t) + defer tearDown() + + tx := createWithdrawalTx(t, pool, store, []int64{1, 5}, []int64{2}) + + size := calculateTxSize(tx) + + // 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 calculateTxFee + // function so that the tx.addChange() call below always adds a change + // output. + restoreCalcTxFee := replaceCalculateTxFee(TstConstantFee(1)) + defer restoreCalcTxFee() + seriesID := tx.inputs[0].Address().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(), msgtx, sigs[tx.ntxid()], tx.inputs) + + // ECDSA signatures have variable length (71-73 bytes) but in + // calculateTxSize() 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].Address().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() + + // A tx that is smaller than 1000 bytes in size should have a fee of 10000 + // satoshis. + restoreCalcTxSize := replaceCalculateTxSize(func(tx *withdrawalTx) int { return 999 }) + defer restoreCalcTxSize() + fee := calculateTxFee(tx) + + 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() + + // A tx that is larger than 1000 bytes in size should have a fee of 1e3 + // satoshis plus 1e3 for every 1000 bytes. + restoreCalcTxSize := replaceCalculateTxSize(func(tx *withdrawalTx) int { return 3000 }) + defer restoreCalcTxSize() + fee := calculateTxFee(tx) + + wantFee := btcutil.Amount(4e3) + if fee != wantFee { + t.Fatalf("Unexpected tx fee; got %v, want %v", fee, wantFee) + } +} + +// lookupStoredTx returns the TxRecord from the given store whose SHA matches the +// given ShaHash. +func lookupStoredTx(store *txstore.Store, sha *wire.ShaHash) *txstore.TxRecord { + for _, r := range store.Records() { + if bytes.Equal(r.Tx().Sha()[:], sha[:]) { + return r + } + } + return nil +} + +// 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 input != inputs[i] { + t.Fatalf("Unexpected input; got %s, want %s", 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, tx *wire.MsgTx, txSigs TxSigs, + credits []Credit) { + for i := range tx.TxIn { + pkScript := credits[i].TxOut().PkScript + TstRunWithManagerUnlocked(t, mgr, func() { + if err := signMultiSigUTXO(mgr, 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) + } +} diff --git a/waddrmgr/error.go b/waddrmgr/error.go index 82c63b2..fc03895 100644 --- a/waddrmgr/error.go +++ b/waddrmgr/error.go @@ -154,26 +154,6 @@ var errorCodeStrings = map[ErrorCode]string{ ErrTooManyAddresses: "ErrTooManyAddresses", ErrWrongPassphrase: "ErrWrongPassphrase", ErrWrongNet: "ErrWrongNet", - - // The following error codes are defined in pool_error.go. - ErrSeriesStorage: "ErrSeriesStorage", - ErrSeriesVersion: "ErrSeriesVersion", - ErrSeriesNotExists: "ErrSeriesNotExists", - ErrSeriesAlreadyExists: "ErrSeriesAlreadyExists", - ErrSeriesAlreadyEmpowered: "ErrSeriesAlreadyEmpowered", - ErrKeyIsPrivate: "ErrKeyIsPrivate", - ErrKeyIsPublic: "ErrKeyIsPublic", - ErrKeyNeuter: "ErrKeyNeuter", - ErrKeyMismatch: "ErrKeyMismatch", - ErrKeysPrivatePublicMismatch: "ErrKeysPrivatePublicMismatch", - ErrKeyDuplicate: "ErrKeyDuplicate", - ErrTooFewPublicKeys: "ErrTooFewPublicKeys", - ErrVotingPoolAlreadyExists: "ErrVotingPoolAlreadyExists", - ErrVotingPoolNotExists: "ErrVotingPoolNotExists", - ErrScriptCreation: "ErrScriptCreation", - ErrTooManyReqSignatures: "ErrTooManyReqSignatures", - ErrInvalidBranch: "ErrInvalidBranch", - ErrInvalidValue: "ErrInvalidValue", } // String returns the ErrorCode as a human-readable name. diff --git a/waddrmgr/error_test.go b/waddrmgr/error_test.go index 07dab02..7763270 100644 --- a/waddrmgr/error_test.go +++ b/waddrmgr/error_test.go @@ -48,22 +48,6 @@ func TestErrorCodeStringer(t *testing.T) { {waddrmgr.ErrTooManyAddresses, "ErrTooManyAddresses"}, {waddrmgr.ErrWrongPassphrase, "ErrWrongPassphrase"}, {waddrmgr.ErrWrongNet, "ErrWrongNet"}, - - // The following error codes are defined in pool_error.go. - {waddrmgr.ErrSeriesStorage, "ErrSeriesStorage"}, - {waddrmgr.ErrSeriesNotExists, "ErrSeriesNotExists"}, - {waddrmgr.ErrSeriesAlreadyExists, "ErrSeriesAlreadyExists"}, - {waddrmgr.ErrSeriesAlreadyEmpowered, "ErrSeriesAlreadyEmpowered"}, - {waddrmgr.ErrKeyIsPrivate, "ErrKeyIsPrivate"}, - {waddrmgr.ErrKeyNeuter, "ErrKeyNeuter"}, - {waddrmgr.ErrKeyMismatch, "ErrKeyMismatch"}, - {waddrmgr.ErrKeysPrivatePublicMismatch, "ErrKeysPrivatePublicMismatch"}, - {waddrmgr.ErrKeyDuplicate, "ErrKeyDuplicate"}, - {waddrmgr.ErrTooFewPublicKeys, "ErrTooFewPublicKeys"}, - {waddrmgr.ErrVotingPoolNotExists, "ErrVotingPoolNotExists"}, - {waddrmgr.ErrScriptCreation, "ErrScriptCreation"}, - {waddrmgr.ErrTooManyReqSignatures, "ErrTooManyReqSignatures"}, - {0xffff, "Unknown ErrorCode (65535)"}, } t.Logf("Running %d tests", len(tests)) diff --git a/waddrmgr/pool_error.go b/waddrmgr/pool_error.go deleted file mode 100644 index db5a884..0000000 --- a/waddrmgr/pool_error.go +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) 2014 Conformal Systems LLC - * - * 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 waddrmgr - -// XXX: All errors defined here will soon be moved to the votingpool package, where they -// belong. - -// Constants that identify voting pool-related errors. -// The codes start from 1000 to avoid confusion with the ones in error.go. -const ( - // ErrSeriesStorage indicates that an error occurred while serializing - // or deserializing one or more series for storing into database. - ErrSeriesStorage ErrorCode = iota + 1000 - - // 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 - - // 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 - - // ErrVotingPoolAlreadyExists indicates that an attempt has been made to - // create a voting pool that already exists. - ErrVotingPoolAlreadyExists - - // ErrVotingPoolNotExists indicates that an attempt has been made to access - // a voting pool that does not exist. - ErrVotingPoolNotExists - - // 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 -)