diff --git a/snacl/snacl.go b/snacl/snacl.go index 30da163..9a134e0 100644 --- a/snacl/snacl.go +++ b/snacl/snacl.go @@ -31,6 +31,8 @@ func zero(b []byte) { } const ( + // Expose secretbox's Overhead const here for convenience. + Overhead = secretbox.Overhead KeySize = 32 NonceSize = 24 DefaultN = 16384 // 2^14 diff --git a/votingpool/README.md b/votingpool/README.md new file mode 100644 index 0000000..af4d8f5 --- /dev/null +++ b/votingpool/README.md @@ -0,0 +1,40 @@ +votingpool +======== + +[![Build Status](https://travis-ci.org/conformal/btcwallet.png?branch=master)] +(https://travis-ci.org/conformal/btcwallet) + +Package votingpool provides voting pool functionality for btcwallet as +described here: +[Voting Pools](http://opentransactions.org/wiki/index.php?title=Category:Voting_Pools). + +A suite of tests is provided to ensure proper functionality. See +`test_coverage.txt` for the gocov coverage report. Alternatively, if you are +running a POSIX OS, you can run the `cov_report.sh` script for a real-time +report. Package votingpool is licensed under the liberal ISC license. + +Note that this is still a work in progress. + +## Feature Overview + +- Create/Load pools +- Create series +- Replace series +- Create deposit addresses +- Comprehensive test coverage + +## Documentation + +[![GoDoc](https://godoc.org/github.com/conformal/btcwallet/votingpool?status.png)] +(http://godoc.org/github.com/conformal/btcwallet/votingpool) + +Full `go doc` style documentation for the project can be viewed online without +installing this package by using the GoDoc site here: +http://godoc.org/github.com/conformal/btcwallet/votingpool + +You can also view the documentation locally once the package is installed with +the `godoc` tool by running `godoc -http=":6060"` and pointing your browser to +http://localhost:6060/pkg/github.com/conformal/btcwallet/votingpool + +Package votingpool is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/votingpool/cov_report.sh b/votingpool/cov_report.sh new file mode 100644 index 0000000..307f05b --- /dev/null +++ b/votingpool/cov_report.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# This script uses gocov to generate a test coverage report. +# The gocov tool my be obtained with the following command: +# go get github.com/axw/gocov/gocov +# +# It will be installed to $GOPATH/bin, so ensure that location is in your $PATH. + +# Check for gocov. +type gocov >/dev/null 2>&1 +if [ $? -ne 0 ]; then + echo >&2 "This script requires the gocov tool." + echo >&2 "You may obtain it with the following command:" + echo >&2 "go get github.com/axw/gocov/gocov" + exit 1 +fi +gocov test | gocov report diff --git a/votingpool/db.go b/votingpool/db.go new file mode 100644 index 0000000..e41c900 --- /dev/null +++ b/votingpool/db.go @@ -0,0 +1,283 @@ +/* + * 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 ( + "bytes" + "encoding/binary" + "fmt" + + "github.com/conformal/btcwallet/snacl" + "github.com/conformal/btcwallet/waddrmgr" + "github.com/conformal/btcwallet/walletdb" +) + +// These constants define the serialized length for a given encrypted extended +// public or private key. +const ( + // We can calculate the encrypted extended key length this way: + // snacl.Overhead == overhead for encrypting (16) + // actual base58 extended key length = (111) + // snacl.NonceSize == nonce size used for encryption (24) + seriesKeyLength = snacl.Overhead + 111 + snacl.NonceSize + // 4 bytes version + 1 byte active + 4 bytes nKeys + 4 bytes reqSigs + seriesMinSerial = 4 + 1 + 4 + 4 + // 15 is the max number of keys in a voting pool, 1 each for + // pubkey and privkey + seriesMaxSerial = seriesMinSerial + 15*seriesKeyLength*2 + // version of serialized Series that we support + seriesMaxVersion = 1 +) + +var ( + // string representing a non-existent private key + seriesNullPrivKey = [seriesKeyLength]byte{} +) + +type dbSeriesRow struct { + version uint32 + active bool + reqSigs uint32 + pubKeysEncrypted [][]byte + 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) + if err != nil { + str := fmt.Sprintf("cannot create voting pool %v", votingPoolID) + return managerError(waddrmgr.ErrDatabase, str, 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) + 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) + } + allSeries[seriesID] = series + return nil + }) + if err != nil { + return nil, err + } + return allSeries, nil +} + +// existsPool checks the existence of a bucket named after the given +// voting pool id. +func existsPool(tx walletdb.Tx, votingPoolID []byte) bool { + bucket := tx.RootBucket().Bucket(votingPoolID) + 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 { + row := &dbSeriesRow{ + version: version, + active: active, + reqSigs: reqSigs, + pubKeysEncrypted: pubKeysEncrypted, + privKeysEncrypted: privKeysEncrypted, + } + return putSeriesRow(tx, votingPoolID, 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 +// beforehand. +func putSeriesRow(tx walletdb.Tx, votingPoolID []byte, ID uint32, row *dbSeriesRow) error { + bucket, err := tx.RootBucket().CreateBucketIfNotExists(votingPoolID) + if err != nil { + str := fmt.Sprintf("cannot create bucket %v", votingPoolID) + return managerError(waddrmgr.ErrDatabase, str, err) + } + serialized, err := serializeSeriesRow(row) + if err != nil { + str := fmt.Sprintf("cannot serialize series %v", row) + return managerError(waddrmgr.ErrSeriesStorage, str, 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) + } + return nil +} + +// deserializeSeriesRow deserializes a series storage into a dbSeriesRow struct. +func deserializeSeriesRow(serializedSeries []byte) (*dbSeriesRow, error) { + // The serialized series format is: + // ... + // + // 4 bytes version + 1 byte active + 4 bytes reqSigs + 4 bytes nKeys + // + seriesKeyLength * 2 * nKeys (1 for priv, 1 for pub) + + // Given the above, the length of the serialized series should be + // at minimum the length of the constants. + if len(serializedSeries) < seriesMinSerial { + str := fmt.Sprintf("serialized series is too short: %v", + serializedSeries) + return nil, managerError(waddrmgr.ErrSeriesStorage, 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) + } + + // Keeps track of the position of the next set of bytes to deserialize. + current := 0 + row := dbSeriesRow{} + + row.version = bytesToUint32(serializedSeries[current : current+4]) + if row.version > seriesMaxVersion { + str := fmt.Sprintf("deserialization supports up to version %v not %v", + seriesMaxVersion, row.version) + return nil, managerError(waddrmgr.ErrSeriesVersion, str, nil) + } + current += 4 + + row.active = serializedSeries[current] == 0x01 + current++ + + row.reqSigs = bytesToUint32(serializedSeries[current : current+4]) + current += 4 + + nKeys := bytesToUint32(serializedSeries[current : current+4]) + current += 4 + + // Check to see if we have the right number of bytes to consume. + if len(serializedSeries) < current+int(nKeys)*seriesKeyLength*2 { + str := fmt.Sprintf("serialized series has not enough data: %v", + serializedSeries) + return nil, managerError(waddrmgr.ErrSeriesStorage, 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) + } + + // Deserialize the pubkey/privkey pairs. + row.pubKeysEncrypted = make([][]byte, nKeys) + row.privKeysEncrypted = make([][]byte, nKeys) + for i := 0; i < int(nKeys); i++ { + pubKeyStart := current + seriesKeyLength*i*2 + pubKeyEnd := current + seriesKeyLength*i*2 + seriesKeyLength + privKeyEnd := current + seriesKeyLength*(i+1)*2 + row.pubKeysEncrypted[i] = serializedSeries[pubKeyStart:pubKeyEnd] + privKeyEncrypted := serializedSeries[pubKeyEnd:privKeyEnd] + if bytes.Equal(privKeyEncrypted, seriesNullPrivKey[:]) { + row.privKeysEncrypted[i] = nil + } else { + row.privKeysEncrypted[i] = privKeyEncrypted + } + } + + return &row, nil +} + +// serializeSeriesRow serializes a dbSeriesRow struct into storage format. +func serializeSeriesRow(row *dbSeriesRow) ([]byte, error) { + // The serialized series format is: + // ... + // + // 4 bytes version + 1 byte active + 4 bytes reqSigs + 4 bytes nKeys + // + seriesKeyLength * 2 * nKeys (1 for priv, 1 for pub) + serializedLen := 4 + 1 + 4 + 4 + (seriesKeyLength * 2 * len(row.pubKeysEncrypted)) + + if len(row.privKeysEncrypted) != 0 && + len(row.pubKeysEncrypted) != len(row.privKeysEncrypted) { + str := fmt.Sprintf("different # of pub (%v) and priv (%v) keys", + len(row.pubKeysEncrypted), len(row.privKeysEncrypted)) + return nil, managerError(waddrmgr.ErrSeriesStorage, 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) + } + + serialized := make([]byte, 0, serializedLen) + serialized = append(serialized, uint32ToBytes(row.version)...) + if row.active { + serialized = append(serialized, 0x01) + } else { + serialized = append(serialized, 0x00) + } + serialized = append(serialized, uint32ToBytes(row.reqSigs)...) + nKeys := uint32(len(row.pubKeysEncrypted)) + serialized = append(serialized, uint32ToBytes(nKeys)...) + + var privKeyEncrypted []byte + for i, pubKeyEncrypted := range row.pubKeysEncrypted { + // check that the encrypted length is correct + if len(pubKeyEncrypted) != seriesKeyLength { + str := fmt.Sprintf("wrong length of Encrypted Public Key: %v", + pubKeyEncrypted) + return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil) + } + serialized = append(serialized, pubKeyEncrypted...) + + if len(row.privKeysEncrypted) == 0 { + privKeyEncrypted = seriesNullPrivKey[:] + } else { + privKeyEncrypted = row.privKeysEncrypted[i] + } + + if privKeyEncrypted == nil { + serialized = append(serialized, seriesNullPrivKey[:]...) + } else if len(privKeyEncrypted) != seriesKeyLength { + str := fmt.Sprintf("wrong length of Encrypted Private Key: %v", + len(privKeyEncrypted)) + return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil) + } else { + serialized = append(serialized, privKeyEncrypted...) + } + } + return serialized, nil +} + +// uint32ToBytes converts a 32 bit unsigned integer into a 4-byte slice in +// little-endian order: 1 -> [1 0 0 0]. +func uint32ToBytes(number uint32) []byte { + buf := make([]byte, 4) + binary.LittleEndian.PutUint32(buf, number) + return buf +} + +// bytesToUint32 converts a 4-byte slice in little-endian order into a 32 bit +// unsigned integer: [1 0 0 0] -> 1. +func bytesToUint32(encoded []byte) uint32 { + return binary.LittleEndian.Uint32(encoded) +} diff --git a/votingpool/doc.go b/votingpool/doc.go new file mode 100644 index 0000000..71c5944 --- /dev/null +++ b/votingpool/doc.go @@ -0,0 +1,70 @@ +/* + * 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 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. + +This package depends on the waddrmgr package, and in particular +instances of the waddrgmgr.Manager structure. + +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. + +Loading an existing pool + +An existing voting pool is loaded via the Load function, which accepts +the database name used when creating the pool as well as the poolID. + +Creating a series + +A series can be created via the CreateSeries method, which accepts a +version number, a series identifier, a number of required signatures +(m in m-of-n multisig, and a set of public keys. + +Deposit Addresses + +A deposit address can be created via the DepositScriptAddress +method, which 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. + +Replacing a series + +A series can be replaced via the ReplaceSeries method. It accepts +the same parameters as the CreateSeries method. + + +Documentation + +[1] https://github.com/justusranvier/bips/blob/master/bip-draft-Hierarchy%20for%20Non-Colored%20Voting%20Pool%20Deterministic%20Multisig%20Wallets.mediawiki + + +*/ +package votingpool diff --git a/votingpool/example_test.go b/votingpool/example_test.go new file mode 100644 index 0000000..c9f9fe8 --- /dev/null +++ b/votingpool/example_test.go @@ -0,0 +1,125 @@ +/* + * 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 ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/conformal/btcnet" + "github.com/conformal/btcwallet/votingpool" + "github.com/conformal/btcwallet/waddrmgr" + "github.com/conformal/btcwallet/walletdb" + _ "github.com/conformal/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. + + // Create a new wallet DB. + dir, err := ioutil.TempDir("", "pool_test") + if err != nil { + fmt.Printf("Failed to create db dir: %v\n", 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() + + // 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) + return + } + + // Create the address manager + mgr, err := waddrmgr.Create(mgrNamespace, seed, pubPassphrase, privPassphrase, + &btcnet.MainNetParams, nil) + if err != nil { + fmt.Printf("Failed to create addr manager: %v\n", err) + return + } + defer mgr.Close() + + // Create a walletdb for votingpools. + vpNamespace, err := db.Namespace([]byte("votingpool")) + if err != nil { + fmt.Printf("Failed to create VotingPool DB namespace: %v\n", err) + return + } + + // Create the voting pool. + pool, err := votingpool.Create(vpNamespace, mgr, []byte{0x00}) + if err != nil { + fmt.Printf("Voting Pool creation failed: %v\n", err) + return + } + + // Create a 2-of-3 series. + apiVersion := uint32(1) + seriesID := uint32(1) + requiredSignatures := uint32(2) + pubKeys := []string{ + "xpub661MyMwAqRbcFDDrR5jY7LqsRioFDwg3cLjc7tML3RRcfYyhXqqgCH5SqMSQdpQ1Xh8EtVwcfm8psD8zXKPcRaCVSY4GCqbb3aMEs27GitE", + "xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9", + "xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh", + } + err = pool.CreateSeries(apiVersion, seriesID, requiredSignatures, pubKeys) + if err != nil { + fmt.Printf("Cannot create series: %v\n", err) + return + } + + // Create a deposit address. + branch := uint32(0) // The change branch + index := uint32(1) + addr, err := pool.DepositScriptAddress(seriesID, branch, index) + if err != nil { + fmt.Printf("DepositScriptAddress failed for series: %d, branch: %d, index: %d\n", + seriesID, branch, index) + 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) + if err != nil { + fmt.Printf("Cannot replace series: %v\n", err) + return + } + + // Output: + // Generated deposit address: 3QTzpc9d3tTbNLJLB7xwt87nWM38boAhAw +} diff --git a/votingpool/internal_test.go b/votingpool/internal_test.go new file mode 100644 index 0000000..07d969d --- /dev/null +++ b/votingpool/internal_test.go @@ -0,0 +1,117 @@ +/* + * 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 ( + "github.com/conformal/btcutil/hdkeychain" + "github.com/conformal/btcwallet/waddrmgr" + "github.com/conformal/btcwallet/walletdb" +) + +// 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) +} + +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) +} + +// TstNamespace exposes the Pool's namespace as it's needed in some tests. +func (vp *Pool) TstNamespace() walletdb.Namespace { + return vp.namespace +} + +// TstGetRawPublicKeys gets a series public keys in string format. +func (s *SeriesData) TstGetRawPublicKeys() []string { + rawKeys := make([]string, len(s.publicKeys)) + for i, key := range s.publicKeys { + rawKeys[i] = key.String() + } + return rawKeys +} + +// TstGetRawPrivateKeys gets a series private keys in string format. +func (s *SeriesData) TstGetRawPrivateKeys() []string { + rawKeys := make([]string, len(s.privateKeys)) + for i, key := range s.privateKeys { + if key != nil { + rawKeys[i] = key.String() + } + } + return rawKeys +} + +// TstGetReqSigs expose the series reqSigs attribute. +func (s *SeriesData) TstGetReqSigs() uint32 { + return s.reqSigs +} + +// TstEmptySeriesLookup empties the voting pool seriesLookup attribute. +func (vp *Pool) TstEmptySeriesLookup() { + vp.seriesLookup = make(map[uint32]*SeriesData) +} + +// TstDecryptExtendedKey expose the decryptExtendedKey method. +func (vp *Pool) TstDecryptExtendedKey(keyType waddrmgr.CryptoKeyType, encrypted []byte) (*hdkeychain.ExtendedKey, error) { + return vp.decryptExtendedKey(keyType, encrypted) +} + +// SeriesRow mimics dbSeriesRow defined in db.go . +type SeriesRow struct { + Version uint32 + Active bool + ReqSigs uint32 + PubKeysEncrypted [][]byte + PrivKeysEncrypted [][]byte +} + +// 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/pool.go b/votingpool/pool.go new file mode 100644 index 0000000..8497fc1 --- /dev/null +++ b/votingpool/pool.go @@ -0,0 +1,620 @@ +/* + * 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" + "sort" + + "github.com/conformal/btcscript" + "github.com/conformal/btcutil" + "github.com/conformal/btcutil/hdkeychain" + "github.com/conformal/btcwallet/waddrmgr" + "github.com/conformal/btcwallet/walletdb" +) + +const ( + minSeriesPubKeys = 3 +) + +// SeriesData represents a Series for a given Pool. +type SeriesData struct { + version uint32 + // Whether or not a series is active. This is serialized/deserialized but + // for now there's no way to deactivate a series. + active bool + // A.k.a. "m" in "m of n signatures needed". + reqSigs uint32 + publicKeys []*hdkeychain.ExtendedKey + privateKeys []*hdkeychain.ExtendedKey +} + +// Pool represents an arrangement of notary servers to securely +// store and account for customer cryptocurrency deposits and to redeem +// valid withdrawals. For details about how the arrangement works, see +// http://opentransactions.org/wiki/index.php?title=Category:Voting_Pools +type Pool struct { + ID []byte + seriesLookup map[uint32]*SeriesData + manager *waddrmgr.Manager + namespace walletdb.Namespace +} + +// 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) { + err := namespace.Update( + func(tx walletdb.Tx) error { + return putPool(tx, poolID) + }) + if err != nil { + str := fmt.Sprintf("unable to add voting pool %v to db", poolID) + return nil, managerError(waddrmgr.ErrVotingPoolAlreadyExists, str, err) + } + return newPool(namespace, m, poolID), nil +} + +// Load fetches the entry in the database with the given ID and returns the Pool +// representing it. +func Load(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID []byte) (*Pool, error) { + err := namespace.View( + 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 nil + }) + if err != nil { + return nil, err + } + vp := newPool(namespace, m, poolID) + if err = vp.LoadAllSeries(); err != nil { + return nil, err + } + return vp, nil +} + +// newPool creates a new Pool instance. +func newPool(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID []byte) *Pool { + return &Pool{ + ID: poolID, + seriesLookup: make(map[uint32]*SeriesData), + manager: m, + namespace: namespace, + } +} + +// 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) { + pid := []byte(poolID) + vp, err := Load(namespace, m, pid) + if err != nil { + return nil, err + } + script, err := vp.DepositScript(seriesID, branch, index) + if err != nil { + return nil, err + } + return script, nil +} + +// LoadAndCreateSeries loads the Pool with the given ID, creating a new one if it doesn't +// yet exist, and then creates and returns a Series with the given seriesID, rawPubKeys +// and reqSigs. See CreateSeries for the constraints enforced on rawPubKeys and reqSigs. +func LoadAndCreateSeries(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) + if err != nil { + managerErr := err.(waddrmgr.ManagerError) + if managerErr.ErrorCode == waddrmgr.ErrVotingPoolNotExists { + vp, err = Create(namespace, m, pid) + if err != nil { + return err + } + } else { + return err + } + } + return vp.CreateSeries(version, seriesID, reqSigs, rawPubKeys) +} + +// LoadAndReplaceSeries loads the voting pool with the given ID and calls ReplaceSeries, +// passing the given series ID, public keys and reqSigs to it. +func LoadAndReplaceSeries(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) + if err != nil { + return err + } + return vp.ReplaceSeries(version, seriesID, reqSigs, rawPubKeys) +} + +// LoadAndEmpowerSeries loads the voting pool with the given ID and calls EmpowerSeries, +// passing the given series ID and private key to it. +func LoadAndEmpowerSeries(namespace walletdb.Namespace, m *waddrmgr.Manager, + poolID string, seriesID uint32, rawPrivKey string) error { + pid := []byte(poolID) + pool, err := Load(namespace, m, pid) + if err != nil { + return err + } + return pool.EmpowerSeries(seriesID, rawPrivKey) +} + +// GetSeries 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] + if !exists { + return nil + } + return series +} + +// 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 { + var err error + encryptedPubKeys := make([][]byte, len(data.publicKeys)) + for i, pubKey := range data.publicKeys { + encryptedPubKeys[i], err = vp.manager.Encrypt( + waddrmgr.CKTPublic, []byte(pubKey.String())) + if err != nil { + str := fmt.Sprintf("key %v failed encryption", pubKey) + return managerError(waddrmgr.ErrCrypto, str, err) + } + } + encryptedPrivKeys := make([][]byte, len(data.privateKeys)) + for i, privKey := range data.privateKeys { + if privKey == nil { + encryptedPrivKeys[i] = nil + } else { + encryptedPrivKeys[i], err = vp.manager.Encrypt( + waddrmgr.CKTPrivate, []byte(privKey.String())) + } + if err != nil { + str := fmt.Sprintf("key %v failed encryption", privKey) + return managerError(waddrmgr.ErrCrypto, str, err) + } + } + + err = vp.namespace.Update(func(tx walletdb.Tx) error { + return putSeries(tx, vp.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 nil +} + +// CanonicalKeyOrder will return a copy of the input canonically +// ordered which is defined to be lexicographical. +func CanonicalKeyOrder(keys []string) []string { + orderedKeys := make([]string, len(keys)) + copy(orderedKeys, keys) + sort.Sort(sort.StringSlice(orderedKeys)) + return orderedKeys +} + +// Convert the given slice of strings into a slice of ExtendedKeys, +// checking that all of them are valid public (and not private) keys, +// and that there are no duplicates. +func convertAndValidatePubKeys(rawPubKeys []string) ([]*hdkeychain.ExtendedKey, error) { + seenKeys := make(map[string]bool) + keys := make([]*hdkeychain.ExtendedKey, len(rawPubKeys)) + for i, rawPubKey := range rawPubKeys { + if _, seen := seenKeys[rawPubKey]; seen { + str := fmt.Sprintf("duplicated public key: %v", rawPubKey) + return nil, managerError(waddrmgr.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) + } + + if key.IsPrivate() { + str := fmt.Sprintf("private keys not accepted: %v", rawPubKey) + return nil, managerError(waddrmgr.ErrKeyIsPrivate, str, nil) + } + keys[i] = key + } + return keys, nil +} + +// putSeries creates a new seriesData with the given arguments, ordering the +// given public keys (using CanonicalKeyOrder), validating and converting them +// to hdkeychain.ExtendedKeys, saves that to disk and adds it to this voting +// pool's seriesLookup map. It also ensures inRawPubKeys has at least +// minSeriesPubKeys items and reqSigs is not greater than the number of items in +// inRawPubKeys. +func (vp *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) + } + + 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) + } + + rawPubKeys := CanonicalKeyOrder(inRawPubKeys) + + keys, err := convertAndValidatePubKeys(rawPubKeys) + if err != nil { + return err + } + + data := &SeriesData{ + version: version, + active: false, + reqSigs: reqSigs, + publicKeys: keys, + privateKeys: make([]*hdkeychain.ExtendedKey, len(keys)), + } + + err = vp.saveSeriesToDisk(seriesID, data) + if err != nil { + return err + } + vp.seriesLookup[seriesID] = data + return nil +} + +// CreateSeries will create and return a new non-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) 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) + } + + return vp.putSeries(version, seriesID, reqSigs, rawPubKeys) +} + +// 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) + if series == nil { + str := fmt.Sprintf("series #%d does not exist, cannot replace it", seriesID) + return managerError(waddrmgr.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 vp.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) + if err != nil { + str := fmt.Sprintf("cannot decrypt key %v", encrypted) + return nil, managerError(waddrmgr.ErrCrypto, str, err) + } + result, err := hdkeychain.NewKeyFromString(string(decrypted)) + zero(decrypted) + if err != nil { + str := fmt.Sprintf("cannot get key from string %v", decrypted) + return nil, managerError(waddrmgr.ErrKeyChain, str, err) + } + return result, nil +} + +// validateAndDecryptSeriesKeys checks that the length of the public and private key +// slices is the same, decrypts them, ensures the non-nil private keys have a matching +// public key and returns them. +func validateAndDecryptKeys(rawPubKeys, rawPrivKeys [][]byte, vp *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, + "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) + if err != nil { + return nil, nil, err + } + pubKeys[i] = pubKey + + encryptedPriv := rawPrivKeys[i] + var privKey *hdkeychain.ExtendedKey + if encryptedPriv == nil { + privKey = nil + } else { + privKey, err = vp.decryptExtendedKey(waddrmgr.CKTPrivate, encryptedPriv) + if err != nil { + return nil, nil, err + } + } + privKeys[i] = privKey + + if privKey != nil { + checkPubKey, err := privKey.Neuter() + if err != nil { + str := fmt.Sprintf("cannot neuter key %v", privKey) + return nil, nil, managerError(waddrmgr.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 pubKeys, privKeys, nil +} + +// LoadAllSeries fetches all series (decrypting their public and private +// extended keys) for this Pool from the database and populates the +// seriesLookup map with them. If there are any private extended keys for +// a series, it will also ensure they have a matching extended public key +// in that series. +func (vp *Pool) LoadAllSeries() error { + var series map[uint32]*dbSeriesRow + err := vp.namespace.View(func(tx walletdb.Tx) error { + var err error + series, err = loadAllSeries(tx, vp.ID) + return err + }) + if err != nil { + return err + } + for id, series := range series { + pubKeys, privKeys, err := validateAndDecryptKeys( + series.pubKeysEncrypted, series.privKeysEncrypted, vp) + if err != nil { + return err + } + vp.seriesLookup[id] = &SeriesData{ + publicKeys: pubKeys, + privateKeys: privKeys, + reqSigs: series.reqSigs, + } + } + 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) { + 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) + } + + if branch > uint32(len(pks)) { + return nil, managerError(waddrmgr.ErrInvalidBranch, "branch number is bigger than number of public keys", nil) + } + + if branch == 0 { + numKeys := len(pks) + res := make([]*hdkeychain.ExtendedKey, numKeys) + copy(res, pks) + // reverse pk + for i, j := 0, numKeys-1; i < j; i, j = i+1, j-1 { + res[i], res[j] = res[j], res[i] + } + return res, nil + } + + tmp := make([]*hdkeychain.ExtendedKey, len(pks)) + tmp[0] = pks[branch-1] + j := 1 + for i := 0; i < len(pks); i++ { + if i != int(branch-1) { + tmp[j] = pks[i] + j++ + } + } + return tmp, nil +} + +// DepositScriptAddress 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) + if err != nil { + return nil, err + } + scriptHash := btcutil.Hash160(script) + + return btcutil.NewAddressScriptHashFromHash(scriptHash, vp.manager.Net()) +} + +// 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) + if series == nil { + str := fmt.Sprintf("series #%d does not exist", seriesID) + return nil, managerError(waddrmgr.ErrSeriesNotExists, str, nil) + } + + pubKeys, err := branchOrder(series.publicKeys, branch) + if err != nil { + return nil, err + } + + pks := make([]*btcutil.AddressPubKey, len(pubKeys)) + for i, key := range pubKeys { + child, err := key.Child(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) + } + 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) + } + pks[i], err = btcutil.NewAddressPubKey(pubkey.SerializeCompressed(), vp.manager.Net()) + 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) + } + } + + script, err := btcscript.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 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 { + // make sure this series exists + series := vp.GetSeries(seriesID) + if series == nil { + str := fmt.Sprintf("series %d does not exist for this voting pool", + seriesID) + return managerError(waddrmgr.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) + } + 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) + } + + 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) + } + + lookingFor := pubKey.String() + found := false + + // Make sure the private key has the corresponding public key in the series, + // to be able to empower it. + for i, publicKey := range series.publicKeys { + if publicKey.String() == lookingFor { + found = true + series.privateKeys[i] = privKey + } + } + + if !found { + str := fmt.Sprintf( + "private Key does not have a corresponding public key in this series") + return managerError(waddrmgr.ErrKeysPrivatePublicMismatch, str, nil) + } + + err = vp.saveSeriesToDisk(seriesID, series) + + if err != nil { + return err + } + + return nil +} + +// IsEmpowered returns true if this series is empowered (i.e. if it has +// at least one private key loaded). +func (s *SeriesData) IsEmpowered() bool { + for _, key := range s.privateKeys { + if key != nil { + return true + } + } + return false +} + +// 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} +} + +// zero sets all bytes in the passed slice to zero. This is used to +// explicitly clear private key material from memory. +// +// XXX(lars) there exists currently around 4-5 other zero functions +// with at least 3 different implementations. We should try to +// consolidate these. +func zero(b []byte) { + for i := range b { + b[i] ^= b[i] + } +} diff --git a/votingpool/pool_test.go b/votingpool/pool_test.go new file mode 100644 index 0000000..8374086 --- /dev/null +++ b/votingpool/pool_test.go @@ -0,0 +1,1385 @@ +/* + * 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" + "encoding/hex" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "runtime" + "testing" + + "github.com/conformal/btcnet" + "github.com/conformal/btcutil/hdkeychain" + "github.com/conformal/btcwallet/votingpool" + "github.com/conformal/btcwallet/waddrmgr" + "github.com/conformal/btcwallet/walletdb" + _ "github.com/conformal/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, + &btcnet.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) + defer tearDown() + // setup + poolID := "test" + pubKeys := []string{pubKey0, pubKey1, pubKey2} + err := votingpool.LoadAndCreateSeries(pool.TstNamespace(), manager, 1, poolID, 0, 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) + if err != nil { + t.Fatalf("Failed to get deposit script: %v", err) + } + + // validate + strScript := hex.EncodeToString(script) + want := "5221035e94da75731a2153b20909017f62fcd49474c45f3b46282c0dafa8b40a3a312b2102e983a53dd20b7746dd100dfd2925b777436fc1ab1dd319433798924a5ce143e32102908d52a548ee9ef6b2d0ea67a3781a0381bc3570ad623564451e63757ff9393253ae" + if want != strScript { + t.Fatalf("Failed to get the right deposit script. Got %v, want %v", + strScript, want) + } +} + +func TestLoadVotingPoolAndCreateSeries(t *testing.T) { + tearDown, manager, pool := setUp(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) + 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) + + if err != nil { + t.Fatalf("Loading voting pool and Creating series failed: %v", err) + } +} + +func TestLoadVotingPoolAndReplaceSeries(t *testing.T) { + tearDown, manager, pool := setUp(t) + defer tearDown() + + // setup + poolID := "test" + pubKeys := []string{pubKey0, pubKey1, pubKey2} + err := votingpool.LoadAndCreateSeries(pool.TstNamespace(), manager, 1, poolID, 0, 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) + if err != nil { + t.Fatalf("Failed to replace series: %v", err) + } +} + +func TestLoadVotingPoolAndEmpowerSeries(t *testing.T) { + tearDown, manager, pool := setUp(t) + defer tearDown() + + // setup + poolID := "test" + pubKeys := []string{pubKey0, pubKey1, pubKey2} + err := votingpool.LoadAndCreateSeries(pool.TstNamespace(), manager, 1, poolID, 0, 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) + if err != nil { + t.Fatalf("Load voting pool and Empower series failed: %v", err) + } +} + +func TestDepositScriptAddress(t *testing.T) { + tearDown, _, pool := setUp(t) + defer tearDown() + + tests := []struct { + version uint32 + series uint32 + reqSigs uint32 + pubKeys []string + // map of branch:address (we only check the branch index at 0) + addresses map[uint32]string + }{ + { + version: 1, + series: 0, + reqSigs: 2, + pubKeys: []string{pubKey0, pubKey1, pubKey2}, + addresses: map[uint32]string{ + 0: "3Hb4xcebcKg4DiETJfwjh8sF4uDw9rqtVC", + 1: "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", + 2: "3Qt1EaKRD9g9FeL2DGkLLswhK1AKmmXFSe", + 3: "3PbExiaztsSYgh6zeMswC49hLUwhTQ86XG", + }, + }, + } + + for i, test := range tests { + if err := pool.CreateSeries(test.version, test.series, + test.reqSigs, test.pubKeys); err != nil { + t.Fatalf("Cannot creates series %v", test.series) + } + for branch, expectedAddress := range test.addresses { + addr, err := pool.DepositScriptAddress(test.series, branch, 0) + if err != nil { + t.Fatalf("Failed to get DepositScriptAddress #%d: %v", i, err) + } + address := addr.EncodeAddress() + if expectedAddress != address { + t.Errorf("DepositScript #%d returned the wrong deposit script. Got %v, want %v", + i, address, expectedAddress) + } + } + } +} + +func TestDepositScriptAddressForNonExistentSeries(t *testing.T) { + tearDown, _, pool := setUp(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) + } + } +} + +func TestDepositScriptAddressForHardenedPubKey(t *testing.T) { + tearDown, _, pool := setUp(t) + defer tearDown() + if err := pool.CreateSeries(1, 0, 2, []string{pubKey0, pubKey1, pubKey2}); 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)) + + 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) + } + } +} + +func TestLoadVotingPool(t *testing.T) { + tearDown, mgr, pool := setUp(t) + defer tearDown() + + pool2, err := votingpool.Load(pool.TstNamespace(), mgr, pool.ID) + if err != nil { + t.Errorf("Error loading VotingPool: %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) + defer tearDown() + + pool2, err := votingpool.Create(pool.TstNamespace(), mgr, []byte{0x02}) + if err != nil { + t.Errorf("Error creating VotingPool: %v", err) + } + if !bytes.Equal(pool2.ID, []byte{0x02}) { + t.Errorf("VotingPool ID mismatch: got %v, want %v", pool2.ID, []byte{0x02}) + } +} + +func TestCreateVotingPoolWhenAlreadyExists(t *testing.T) { + tearDown, mgr, pool := setUp(t) + defer tearDown() + + _, err := votingpool.Create(pool.TstNamespace(), mgr, pool.ID) + + checkManagerError(t, "", err, waddrmgr.ErrVotingPoolAlreadyExists) +} + +func TestCreateSeries(t *testing.T) { + tearDown, _, pool := setUp(t) + defer tearDown() + + tests := []struct { + version uint32 + series uint32 + 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}, + }, + { + version: 1, + series: 2, + reqSigs: 4, + pubKeys: []string{pubKey0, pubKey1, pubKey2, pubKey3, pubKey4, + pubKey5, pubKey6}, + }, + { + version: 1, + series: 3, + reqSigs: 5, + pubKeys: []string{pubKey0, pubKey1, pubKey2, pubKey3, pubKey4, + pubKey5, pubKey6, pubKey7, pubKey8}, + }, + } + + for testNum, test := range tests { + err := pool.CreateSeries(test.version, test.series, test.reqSigs, test.pubKeys[:]) + if err != nil { + t.Fatalf("%d: Cannot create series %d", testNum, test.series) + } + exists, err := pool.TstExistsSeries(test.series) + if err != nil { + t.Fatal(err) + } + if !exists { + t.Errorf("%d: Series %d not in database", testNum, test.series) + } + } +} + +func TestCreateSeriesWhenAlreadyExists(t *testing.T) { + tearDown, _, pool := setUp(t) + defer tearDown() + pubKeys := []string{pubKey0, pubKey1, pubKey2} + if err := pool.CreateSeries(1, 0, 1, pubKeys); err != nil { + t.Fatalf("Cannot create series: %v", err) + } + + err := pool.CreateSeries(1, 0, 1, pubKeys) + + checkManagerError(t, "", err, waddrmgr.ErrSeriesAlreadyExists) +} + +func TestPutSeriesErrors(t *testing.T) { + tearDown, _, pool := setUp(t) + defer tearDown() + + tests := []struct { + version uint32 + reqSigs uint32 + pubKeys []string + err waddrmgr.ManagerError + msg string + }{ + { + pubKeys: []string{pubKey0}, + err: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrTooFewPublicKeys}, + msg: "Should return error when passed too few pubkeys", + }, + { + reqSigs: 5, + pubKeys: []string{pubKey0, pubKey1, pubKey2}, + err: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrTooManyReqSignatures}, + msg: "Should return error when reqSigs > len(pubKeys)", + }, + { + pubKeys: []string{pubKey0, pubKey1, pubKey2, pubKey0}, + err: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrKeyDuplicate}, + msg: "Should return error when passed duplicate pubkeys", + }, + { + pubKeys: []string{"invalidxpub1", "invalidxpub2", "invalidxpub3"}, + err: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrKeyChain}, + msg: "Should return error when passed invalid pubkey", + }, + { + pubKeys: []string{privKey0, privKey1, privKey2}, + err: waddrmgr.ManagerError{ErrorCode: waddrmgr.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) + } + } + } +} + +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) + defer tearDown() + + var seriesID uint32 = 1 + + if err := pool.CreateSeries(1, seriesID, 3, []string{pubKey0, pubKey1, pubKey2, pubKey3}); err != nil { + t.Fatalf("Failed to create series", 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", 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", err) + } else { + gotErr := err.(waddrmgr.ManagerError) + wantErrCode := waddrmgr.ErrorCode(waddrmgr.ErrSeriesAlreadyEmpowered) + if wantErrCode != gotErr.ErrorCode { + t.Errorf("Got %s, want %s", gotErr.ErrorCode, wantErrCode) + } + } +} + +func TestReplaceNonExistingSeries(t *testing.T) { + tearDown, _, pool := setUp(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) + } + } +} + +type replaceSeriesTestEntry struct { + testID int + orig seriesRaw + replaceWith seriesRaw +} + +var replaceSeriesTestData = []replaceSeriesTestEntry{ + { + testID: 0, + orig: seriesRaw{ + id: 0, + version: 1, + reqSigs: 2, + pubKeys: votingpool.CanonicalKeyOrder( + []string{pubKey0, pubKey1, pubKey2, pubKey4}), + }, + replaceWith: seriesRaw{ + id: 0, + version: 1, + reqSigs: 1, + pubKeys: votingpool.CanonicalKeyOrder( + []string{pubKey3, pubKey4, pubKey5}), + }, + }, + { + testID: 1, + orig: seriesRaw{ + id: 2, + version: 1, + reqSigs: 2, + pubKeys: votingpool.CanonicalKeyOrder( + []string{pubKey0, pubKey1, pubKey2}), + }, + replaceWith: seriesRaw{ + id: 2, + version: 1, + reqSigs: 2, + pubKeys: votingpool.CanonicalKeyOrder( + []string{pubKey3, pubKey4, pubKey5, pubKey6}), + }, + }, + { + testID: 2, + orig: seriesRaw{ + id: 4, + version: 1, + reqSigs: 8, + pubKeys: votingpool.CanonicalKeyOrder([]string{pubKey0, pubKey1, pubKey2, pubKey3, pubKey4, pubKey5, pubKey6, pubKey7, pubKey8}), + }, + replaceWith: seriesRaw{ + id: 4, + version: 1, + reqSigs: 7, + pubKeys: votingpool.CanonicalKeyOrder([]string{pubKey0, pubKey1, pubKey2, pubKey3, pubKey4, pubKey5, pubKey6, pubKey7}), + }, + }, +} + +func TestReplaceExistingSeries(t *testing.T) { + tearDown, _, pool := setUp(t) + defer tearDown() + + for _, data := range replaceSeriesTestData { + seriesID := data.orig.id + testID := data.testID + + if err := pool.CreateSeries(data.orig.version, seriesID, data.orig.reqSigs, data.orig.pubKeys); err != nil { + t.Fatalf("Test #%d: failed to create series in replace series setup", + testID, err) + } + + if err := pool.ReplaceSeries(data.replaceWith.version, seriesID, + data.replaceWith.reqSigs, data.replaceWith.pubKeys); err != nil { + t.Errorf("Test #%d: replaceSeries failed", testID, err) + } + + validateReplaceSeries(t, pool, testID, data.replaceWith) + } +} + +// validateReplaceSeries validate the created series stored in the system +// corresponds to the series we replaced the original with. +func validateReplaceSeries(t *testing.T, pool *votingpool.Pool, testID int, replacedWith seriesRaw) { + seriesID := replacedWith.id + series := pool.GetSeries(seriesID) + if series == nil { + t.Fatalf("Test #%d Series #%d: series not found", testID, seriesID) + } + + pubKeys := series.TstGetRawPublicKeys() + // Check that the public keys match what we expect. + if !reflect.DeepEqual(replacedWith.pubKeys, pubKeys) { + t.Errorf("Test #%d, series #%d: pubkeys mismatch. Got %v, want %v", + testID, seriesID, pubKeys, replacedWith.pubKeys) + } + + // Check number of required sigs. + if replacedWith.reqSigs != series.TstGetReqSigs() { + t.Errorf("Test #%d, series #%d: required signatures mismatch. Got %d, want %d", + testID, seriesID, series.TstGetReqSigs(), replacedWith.reqSigs) + } + + // Check that the series is not empowered. + if series.IsEmpowered() { + t.Errorf("Test #%d, series #%d: series is empowered but should not be", + testID, seriesID) + } +} + +func TestEmpowerSeries(t *testing.T) { + tearDown, manager, pool := setUp(t) + defer tearDown() + + seriesID := uint32(0) + err := pool.CreateSeries(1, seriesID, 2, []string{pubKey0, pubKey1, pubKey2}) + if err != nil { + t.Fatalf("Failed to create series: %v", err) + } + + tests := []struct { + seriesID uint32 + key string + err error + }{ + { + seriesID: 0, + key: privKey0, + }, + { + seriesID: 0, + key: privKey1, + }, + { + seriesID: 1, + key: privKey0, + // invalid series + err: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrSeriesNotExists}, + }, + { + seriesID: 0, + key: "NONSENSE", + // invalid private key + err: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrKeyChain}, + }, + { + seriesID: 0, + key: pubKey5, + // wrong type of key + err: waddrmgr.ManagerError{ErrorCode: waddrmgr.ErrKeyIsPublic}, + }, + { + seriesID: 0, + key: privKey5, + // key not corresponding to pub key + err: waddrmgr.ManagerError{ErrorCode: waddrmgr.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. + 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 + } + } + +} + +func TestGetSeries(t *testing.T) { + tearDown, _, pool := setUp(t) + defer tearDown() + expectedPubKeys := votingpool.CanonicalKeyOrder([]string{pubKey0, pubKey1, pubKey2}) + if err := pool.CreateSeries(1, 0, 2, expectedPubKeys); err != nil { + t.Fatalf("Failed to create series: %v", err) + } + + series := pool.GetSeries(0) + + if series == nil { + t.Fatal("GetSeries() returned nil") + } + pubKeys := series.TstGetRawPublicKeys() + if !reflect.DeepEqual(pubKeys, expectedPubKeys) { + t.Errorf("Series pubKeys mismatch. Got %v, want %v", pubKeys, expectedPubKeys) + } +} + +type seriesRaw struct { + id uint32 + version uint32 + reqSigs uint32 + pubKeys []string + privKeys []string +} + +type testLoadAllSeriesTest struct { + id int + series []seriesRaw +} + +var testLoadAllSeriesTests = []testLoadAllSeriesTest{ + { + id: 1, + series: []seriesRaw{ + { + id: 0, + version: 1, + reqSigs: 2, + pubKeys: []string{pubKey0, pubKey1, pubKey2}, + }, + { + id: 1, + version: 1, + reqSigs: 2, + pubKeys: []string{pubKey3, pubKey4, pubKey5}, + privKeys: []string{privKey4}, + }, + { + id: 2, + version: 1, + reqSigs: 3, + pubKeys: []string{pubKey0, pubKey1, pubKey2, pubKey3, pubKey4}, + privKeys: []string{privKey0, privKey2}, + }, + }, + }, + { + id: 2, + series: []seriesRaw{ + { + id: 0, + version: 1, + reqSigs: 2, + pubKeys: []string{pubKey0, pubKey1, pubKey2}, + }, + }, + }, +} + +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)}) + if err != nil { + t.Fatalf("Voting Pool creation failed: %v", err) + } + + for _, series := range test.series { + err := pool.CreateSeries(series.version, series.id, + series.reqSigs, series.pubKeys) + if err != nil { + t.Fatalf("Test #%d Series #%d: failed to create series: %v", + test.id, series.id, err) + } + + for _, privKey := range series.privKeys { + 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) + } + } + } + return pool +} + +func TestLoadAllSeries(t *testing.T) { + tearDown, manager, pool := setUp(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) + } + 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) + + // Check that the series exists. + if series == nil { + t.Errorf("Test #%d, series #%d: series not found", testID, seriesData.id) + } + + // Check that reqSigs is what we inserted. + if seriesData.reqSigs != series.TstGetReqSigs() { + t.Errorf("Test #%d, series #%d: required sigs are different. Got %d, want %d", + testID, seriesData.id, series.TstGetReqSigs(), seriesData.reqSigs) + } + + // Check that pubkeys and privkeys have the same length. + publicKeys := series.TstGetRawPublicKeys() + privateKeys := series.TstGetRawPrivateKeys() + if len(privateKeys) != len(publicKeys) { + t.Errorf("Test #%d, series #%d: wrong number of private keys. Got %d, want %d", + testID, seriesData.id, len(privateKeys), len(publicKeys)) + } + + sortedKeys := votingpool.CanonicalKeyOrder(seriesData.pubKeys) + if !reflect.DeepEqual(publicKeys, sortedKeys) { + t.Errorf("Test #%d, series #%d: public keys mismatch. Got %d, want %d", + testID, seriesData.id, sortedKeys, publicKeys) + } + + // Check that privkeys are what we inserted (length and content). + foundPrivKeys := make([]string, 0, len(seriesData.pubKeys)) + for _, privateKey := range privateKeys { + if privateKey != "" { + foundPrivKeys = append(foundPrivKeys, privateKey) + } + } + foundPrivKeys = votingpool.CanonicalKeyOrder(foundPrivKeys) + privKeys := votingpool.CanonicalKeyOrder(seriesData.privKeys) + if !reflect.DeepEqual(privKeys, foundPrivKeys) { + t.Errorf("Test #%d, series #%d: private keys mismatch. Got %d, want %d", + testID, seriesData.id, foundPrivKeys, privKeys) + } +} + +func reverse(inKeys []*hdkeychain.ExtendedKey) []*hdkeychain.ExtendedKey { + revKeys := make([]*hdkeychain.ExtendedKey, len(inKeys)) + max := len(inKeys) + for i := range inKeys { + revKeys[i] = inKeys[max-i-1] + } + return revKeys +} + +func TestBranchOrderZero(t *testing.T) { + // test change address branch (0) for 0-10 keys + for i := 0; i < 10; i++ { + inKeys := createTestPubKeys(t, i, 0) + wantKeys := reverse(inKeys) + resKeys, err := votingpool.TstBranchOrder(inKeys, 0) + if err != nil { + t.Fatalf("Error ordering keys: %v", err) + } + + if len(resKeys) != len(wantKeys) { + t.Errorf("BranchOrder: wrong no. of keys. Got: %d, want %d", + len(resKeys), len(inKeys)) + return + } + + for keyIdx := 0; i < len(inKeys); i++ { + if resKeys[keyIdx] != wantKeys[keyIdx] { + fmt.Printf("%p, %p\n", resKeys[i], wantKeys[i]) + t.Errorf("BranchOrder(keys, 0): got %v, want %v", + resKeys[i], wantKeys[i]) + } + } + } +} + +func TestBranchOrderNonZero(t *testing.T) { + maxBranch := 5 + maxTail := 4 + // Test branch reordering for branch no. > 0. We test all branch values + // within [1, 5] in a slice of up to 9 (maxBranch-1 + branch-pivot + + // maxTail) keys. Hopefully that covers all combinations and edge-cases. + // We test the case where branch no. is 0 elsewhere. + for branch := 1; branch <= maxBranch; branch++ { + for j := 0; j <= maxTail; j++ { + first := createTestPubKeys(t, branch-1, 0) + pivot := createTestPubKeys(t, 1, branch) + last := createTestPubKeys(t, j, branch+1) + + inKeys := append(append(first, pivot...), last...) + wantKeys := append(append(pivot, first...), last...) + resKeys, err := votingpool.TstBranchOrder(inKeys, uint32(branch)) + if err != nil { + t.Fatalf("Error ordering keys: %v", err) + } + + if len(resKeys) != len(inKeys) { + t.Errorf("BranchOrder: wrong no. of keys. Got: %d, want %d", + len(resKeys), len(inKeys)) + } + + for idx := 0; idx < len(inKeys); idx++ { + if resKeys[idx] != wantKeys[idx] { + o, w, g := branchErrorFormat(inKeys, wantKeys, resKeys) + t.Errorf("Branch: %d\nOrig: %v\nGot: %v\nWant: %v", branch, o, g, w) + } + } + } + } +} + +func TestBranchOrderNilKeys(t *testing.T) { + _, err := votingpool.TstBranchOrder(nil, 1) + + checkManagerError(t, "", err, waddrmgr.ErrInvalidValue) +} + +func TestBranchOrderInvalidBranch(t *testing.T) { + _, err := votingpool.TstBranchOrder(createTestPubKeys(t, 3, 0), 4) + + checkManagerError(t, "", err, waddrmgr.ErrInvalidBranch) +} + +func branchErrorFormat(orig, want, got []*hdkeychain.ExtendedKey) (origOrder, wantOrder, gotOrder []int) { + origOrder = []int{} + origMap := make(map[*hdkeychain.ExtendedKey]int) + for i, key := range orig { + origMap[key] = i + 1 + origOrder = append(origOrder, i+1) + } + + wantOrder = []int{} + for _, key := range want { + wantOrder = append(wantOrder, origMap[key]) + } + + gotOrder = []int{} + for _, key := range got { + gotOrder = append(gotOrder, origMap[key]) + } + + return origOrder, wantOrder, gotOrder +} + +func createTestPubKeys(t *testing.T, number, offset int) []*hdkeychain.ExtendedKey { + xpubRaw := "xpub661MyMwAqRbcFwdnYF5mvCBY54vaLdJf8c5ugJTp5p7PqF9J1USgBx12qYMnZ9yUiswV7smbQ1DSweMqu8wn7Jociz4PWkuJ6EPvoVEgMw7" + xpubKey, err := hdkeychain.NewKeyFromString(xpubRaw) + if err != nil { + t.Fatalf("Failed to generate new key", err) + } + + keys := make([]*hdkeychain.ExtendedKey, number) + for i := uint32(0); i < uint32(len(keys)); i++ { + chPubKey, err := xpubKey.Child(i + uint32(offset)) + if err != nil { + t.Fatalf("Failed to generate child key", err) + } + keys[i] = chPubKey + } + return keys +} + +func TestReverse(t *testing.T) { + // Test the utility function that reverses a list of public keys. + // 11 is arbitrary. + for numKeys := 0; numKeys < 11; numKeys++ { + keys := createTestPubKeys(t, numKeys, 0) + revRevKeys := reverse(reverse(keys)) + if len(keys) != len(revRevKeys) { + t.Errorf("Reverse(Reverse(x)): the no. pubkeys changed. Got %d, want %d", + len(revRevKeys), len(keys)) + } + + for i := 0; i < len(keys); i++ { + if keys[i] != revRevKeys[i] { + t.Errorf("Reverse(Reverse(x)) != x. Got %v, want %v", + revRevKeys[i], keys[i]) + } + } + } +} + +func TestEmpowerSeriesNeuterFailed(t *testing.T) { + tearDown, _, pool := setUp(t) + defer tearDown() + + seriesID := uint32(0) + err := pool.CreateSeries(1, seriesID, 2, []string{pubKey0, pubKey1, pubKey2}) + if err != nil { + t.Fatalf("Failed to create series: %v", err) + } + + // A private key with bad version (0xffffffff) will trigger an + // error in (k *ExtendedKey).Neuter and the associated error path + // in EmpowerSeries. + badKey := "wM5uZBNTYmaYGiK8VaGi7zPGbZGLuQgDiR2Zk4nGfbRFLXwHGcMUdVdazRpNHFSR7X7WLmzzbAq8dA1ViN6eWKgKqPye1rJTDQTvBiXvZ7E3nmdx" + err = pool.EmpowerSeries(seriesID, badKey) + + checkManagerError(t, "", err, waddrmgr.ErrKeyNeuter) +} + +func TestDecryptExtendedKeyCannotCreateResultKey(t *testing.T) { + tearDown, mgr, pool := setUp(t) + defer tearDown() + + // the plaintext not being base58 encoded triggers the error + cipherText, err := mgr.Encrypt(waddrmgr.CKTPublic, []byte("not-base58-encoded")) + if err != nil { + 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) + } + } +} + +func TestDecryptExtendedKeyCannotDecrypt(t *testing.T) { + tearDown, _, pool := setUp(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) + } + } +} + +func TestSerializationErrors(t *testing.T) { + tearDown, mgr, _ := setUp(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, + }, + } + + // We need to unlock the manager in order to encrypt with the + // private key. + mgr.Unlock(privPassphrase) + + 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) + } +} + +func TestSerialization(t *testing.T) { + tearDown, mgr, _ := setUp(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, + }, + } + + // 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 %d want %d", + 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, _, _ := setUp(t) + defer tearDown() + + 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, + }, + } + + for testNum, test := range tests { + _, err := votingpool.DeserializeSeries(test.serialized) + + checkManagerError(t, fmt.Sprintf("Test #%d", testNum), err, test.err) + } +} diff --git a/votingpool/test_coverage.txt b/votingpool/test_coverage.txt new file mode 100644 index 0000000..34ed6db --- /dev/null +++ b/votingpool/test_coverage.txt @@ -0,0 +1,92 @@ + +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) + diff --git a/waddrmgr/address.go b/waddrmgr/address.go index 349cc03..6fb6d13 100644 --- a/waddrmgr/address.go +++ b/waddrmgr/address.go @@ -125,7 +125,7 @@ type managedAddress struct { privKeyMutex sync.Mutex } -// Enforce mangedAddress satisfies the ManagedPubKeyAddress interface. +// Enforce managedAddress satisfies the ManagedPubKeyAddress interface. var _ ManagedPubKeyAddress = (*managedAddress)(nil) // unlock decrypts and stores a pointer to the associated private key. It will diff --git a/waddrmgr/db.go b/waddrmgr/db.go index 49b3f0e..e2202e8 100644 --- a/waddrmgr/db.go +++ b/waddrmgr/db.go @@ -302,17 +302,17 @@ func putWatchingOnly(tx walletdb.Tx, watchingOnly bool) error { } if err := bucket.Put(watchingOnlyName, []byte{encoded}); err != nil { - str := "failed to store wathcing only flag" + str := "failed to store watching only flag" return managerError(ErrDatabase, str, err) } return nil } -// accountKey returns the account key to use in the database for a given account -// number. -func accountKey(account uint32) []byte { +// uint32ToBytes converts a 32 bit unsigned integer into a 4-byte slice in +// little-endian order: 1 -> [1 0 0 0]. +func uint32ToBytes(number uint32) []byte { buf := make([]byte, 4) - binary.LittleEndian.PutUint32(buf, account) + binary.LittleEndian.PutUint32(buf, number) return buf } @@ -404,7 +404,6 @@ func deserializeBIP0044AccountRow(accountID []byte, row *dbAccountRow) (*dbBIP00 func serializeBIP0044AccountRow(encryptedPubKey, encryptedPrivKey []byte, nextExternalIndex, nextInternalIndex uint32, name string) []byte { - // The serialized BIP0044 account raw data format is: // // @@ -438,7 +437,7 @@ func serializeBIP0044AccountRow(encryptedPubKey, func fetchAccountInfo(tx walletdb.Tx, account uint32) (interface{}, error) { bucket := tx.RootBucket().Bucket(acctBucketName) - accountID := accountKey(account) + accountID := uint32ToBytes(account) serializedRow := bucket.Get(accountID) if serializedRow == nil { str := fmt.Sprintf("account %d not found", account) @@ -465,7 +464,7 @@ func putAccountRow(tx walletdb.Tx, account uint32, row *dbAccountRow) error { bucket := tx.RootBucket().Bucket(acctBucketName) // Write the serialized value keyed by the account number. - err := bucket.Put(accountKey(account), serializeAccountRow(row)) + err := bucket.Put(uint32ToBytes(account), serializeAccountRow(row)) if err != nil { str := fmt.Sprintf("failed to store account %d", account) return managerError(ErrDatabase, str, err) @@ -781,12 +780,13 @@ func putChainedAddress(tx walletdb.Tx, addressID []byte, account uint32, // Update the next index for the appropriate internal or external // branch. - accountID := accountKey(account) + accountID := uint32ToBytes(account) bucket := tx.RootBucket().Bucket(acctBucketName) serializedAccount := bucket.Get(accountID) // Deserialize the account row. row, err := deserializeAccountRow(accountID, serializedAccount) + if err != nil { return err } @@ -1228,7 +1228,7 @@ func upgradeManager(namespace walletdb.Namespace) error { return managerError(ErrDatabase, str, err) } - // Save the most recent manager version if it isn't already + // Save the most recent database version if it isn't already // there, otherwise keep track of it for potential upgrades. verBytes := mainBucket.Get(mgrVersionName) if verBytes == nil { diff --git a/waddrmgr/doc.go b/waddrmgr/doc.go index 7e56b9e..e078b4a 100644 --- a/waddrmgr/doc.go +++ b/waddrmgr/doc.go @@ -117,7 +117,7 @@ Requesting Existing Addresses In addition to generating new addresses, access to old addresses is often required. Most notably, to sign transactions in order to redeem them. The -Address function provides this capability and returns a ManagedAddress +Address function provides this capability and returns a ManagedAddress. Importing Addresses diff --git a/waddrmgr/error.go b/waddrmgr/error.go index 3f6efa9..aa93693 100644 --- a/waddrmgr/error.go +++ b/waddrmgr/error.go @@ -58,7 +58,7 @@ const ( ErrDatabase ErrorCode = iota // ErrKeyChain indicates an error with the key chain typically either - // due to the inability to create and extended key or deriving a child + // due to the inability to create an extended key or deriving a child // extended key. When this error code is set, the Err field of the // ManagerError will be set to the underlying error. ErrKeyChain @@ -74,54 +74,54 @@ const ( // key type has been selected. ErrInvalidKeyType - // ErrNoExist indicates the manager does not exist. + // ErrNoExist indicates that the specified database does not exist. ErrNoExist - // ErrAlreadyExists indicates the specified manager already exists. + // ErrAlreadyExists indicates that the specified database already exists. ErrAlreadyExists - // ErrCoinTypeTooHigh indicates the coin type specified in the provided + // ErrCoinTypeTooHigh indicates that the coin type specified in the provided // network parameters is higher than the max allowed value as defined // by the maxCoinType constant. ErrCoinTypeTooHigh - // ErrAccountNumTooHigh indicates the specified account number is higher + // ErrAccountNumTooHigh indicates that the specified account number is higher // than the max allowed value as defined by the MaxAccountNum constant. ErrAccountNumTooHigh - // ErrLocked indicates the an operation which requires the address - // manager to be unlocked was requested on a locked address manager. + // ErrLocked indicates that an operation, which requires the account + // manager to be unlocked, was requested on a locked account manager. ErrLocked - // ErrWatchingOnly indicates the an operation which requires the address - // manager to have access to private data was requested on a - // watching-only address manager. + // ErrWatchingOnly indicates that an operation, which requires the + // account manager to have access to private data, was requested on + // a watching-only account manager. ErrWatchingOnly - // ErrInvalidAccount indicates the requested account is not valid. + // ErrInvalidAccount indicates that the requested account is not valid. ErrInvalidAccount - // ErrAddressNotFound indicates the requested address is not known to - // the address manager. + // ErrAddressNotFound indicates that the requested address is not known to + // the account manager. ErrAddressNotFound - // ErrAccountNotFound indicates the requested account is not known to - // the address manager. + // ErrAccountNotFound indicates that the requested account is not known to + // the account manager. ErrAccountNotFound - // ErrDuplicate indicates an address already exists. + // ErrDuplicate indicates that an address already exists. ErrDuplicate - // ErrTooManyAddresses indicates more than the maximum allowed number of + // ErrTooManyAddresses indicates that more than the maximum allowed number of // addresses per account have been requested. ErrTooManyAddresses - // ErrWrongPassphrase inidicates the specified password is incorrect. - // This could be for either the public and private master keys. + // ErrWrongPassphrase indicates that the specified passphrase is incorrect. + // This could be for either public or private master keys. ErrWrongPassphrase - // ErrWrongNet indicates the private key to be imported is not for the - // the same network the account mangaer is configured for. + // ErrWrongNet indicates that the private key to be imported is not for the + // the same network the account manager is configured for. ErrWrongNet ) @@ -144,6 +144,26 @@ 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 b82c2ef..6175610 100644 --- a/waddrmgr/error_test.go +++ b/waddrmgr/error_test.go @@ -46,6 +46,22 @@ 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/internal_test.go b/waddrmgr/internal_test.go index 1a2a88b..fb5c4ed 100644 --- a/waddrmgr/internal_test.go +++ b/waddrmgr/internal_test.go @@ -47,7 +47,7 @@ func TstRunWithReplacedNewSecretKey(callback func()) { callback() } -// TstCheckPublicPassphrase return true if the provided public passphrase is +// TstCheckPublicPassphrase returns true if the provided public passphrase is // correct for the manager. func (m *Manager) TstCheckPublicPassphrase(pubPassphrase []byte) bool { secretKey := snacl.SecretKey{Key: &snacl.CryptoKey{}} diff --git a/waddrmgr/pool_error.go b/waddrmgr/pool_error.go new file mode 100644 index 0000000..db5a884 --- /dev/null +++ b/waddrmgr/pool_error.go @@ -0,0 +1,92 @@ +/* + * 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 +)