votingpool: implement Pool.StartWithdrawal()

<http://opentransactions.org/wiki/index.php/Voting_Pool_Withdrawal_Process>

Also includes some refactorings and other improvements, including better docs
and a new error type (votingpool.Error) used for all votingpool-specific
errors.
This commit is contained in:
Guilherme Salgado 2014-11-04 15:22:13 -02:00
parent 68a9168d9e
commit 2181f4859d
24 changed files with 5550 additions and 1179 deletions

View file

@ -628,7 +628,7 @@ func (t *TxRecord) AddDebits() (Debits, error) {
defer t.s.mtx.Unlock()
if t.debits == nil {
spent, err := t.s.findPreviousCredits(t.Tx())
spent, err := t.s.FindPreviousCredits(t.Tx())
if err != nil {
return Debits{}, err
}
@ -654,9 +654,9 @@ func (t *TxRecord) AddDebits() (Debits, error) {
return d, nil
}
// findPreviousCredits searches for all unspent credits that make up the inputs
// FindPreviousCredits searches for all unspent credits that make up the inputs
// for tx.
func (s *Store) findPreviousCredits(tx *btcutil.Tx) ([]Credit, error) {
func (s *Store) FindPreviousCredits(tx *btcutil.Tx) ([]Credit, error) {
type createdCredit struct {
credit Credit
err error

88
votingpool/common_test.go Normal file
View file

@ -0,0 +1,88 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool
import (
"fmt"
"os"
"runtime"
"testing"
"github.com/btcsuite/btclog"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/waddrmgr"
)
func init() {
runtime.GOMAXPROCS(runtime.NumCPU())
// Enable logging (Debug level) to aid debugging failing tests.
logger, err := btclog.NewLoggerFromWriter(os.Stdout, btclog.DebugLvl)
if err != nil {
fmt.Printf("Failed to initialize stdout logger: %v\n", err)
os.Exit(1)
}
UseLogger(logger)
}
// TstCheckError ensures the passed error is a votingpool.Error with an error
// code that matches the passed error code.
func TstCheckError(t *testing.T, testName string, gotErr error, wantErrCode ErrorCode) {
vpErr, ok := gotErr.(Error)
if !ok {
t.Errorf("%s: unexpected error type - got %T (%s), want %T",
testName, gotErr, gotErr, Error{})
}
if vpErr.ErrorCode != wantErrCode {
t.Errorf("%s: unexpected error code - got %s (%s), want %s",
testName, vpErr.ErrorCode, vpErr, wantErrCode)
}
}
// TstRunWithManagerUnlocked calls the given callback with the manager unlocked,
// and locks it again before returning.
func TstRunWithManagerUnlocked(t *testing.T, mgr *waddrmgr.Manager, callback func()) {
if err := mgr.Unlock(privPassphrase); err != nil {
t.Fatal(err)
}
defer mgr.Lock()
callback()
}
// replaceCalculateTxFee replaces the calculateTxFee func with the given one
// and returns a function that restores it to the original one.
func replaceCalculateTxFee(f func(*withdrawalTx) btcutil.Amount) func() {
orig := calculateTxFee
calculateTxFee = f
return func() { calculateTxFee = orig }
}
// replaceIsTxTooBig replaces the isTxTooBig func with the given one
// and returns a function that restores it to the original one.
func replaceIsTxTooBig(f func(*withdrawalTx) bool) func() {
orig := isTxTooBig
isTxTooBig = f
return func() { isTxTooBig = orig }
}
// replaceCalculateTxSize replaces the calculateTxSize func with the given one
// and returns a function that restores it to the original one.
func replaceCalculateTxSize(f func(*withdrawalTx) int) func() {
orig := calculateTxSize
calculateTxSize = f
return func() { calculateTxSize = orig }
}

View file

@ -22,12 +22,11 @@ import (
"fmt"
"github.com/btcsuite/btcwallet/snacl"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
)
// These constants define the serialized length for a given encrypted extended
// public or private key.
// public or private key.
const (
// We can calculate the encrypted extended key length this way:
// snacl.Overhead == overhead for encrypting (16)
@ -44,6 +43,8 @@ const (
)
var (
usedAddrsBucketName = []byte("usedaddrs")
seriesBucketName = []byte("series")
// string representing a non-existent private key
seriesNullPrivKey = [seriesKeyLength]byte{}
)
@ -56,29 +57,103 @@ type dbSeriesRow struct {
privKeysEncrypted [][]byte
}
// putPool stores a voting pool in the database, creating a bucket named
// after the voting pool id.
func putPool(tx walletdb.Tx, votingPoolID []byte) error {
_, err := tx.RootBucket().CreateBucket(votingPoolID)
// getUsedAddrBucketID returns the used addresses bucket ID for the given series
// and branch. It has the form seriesID:branch.
func getUsedAddrBucketID(seriesID uint32, branch Branch) []byte {
var bucketID [9]byte
binary.LittleEndian.PutUint32(bucketID[0:4], seriesID)
bucketID[4] = ':'
binary.LittleEndian.PutUint32(bucketID[5:9], uint32(branch))
return bucketID[:]
}
// putUsedAddrHash adds an entry (key==index, value==encryptedHash) to the used
// addresses bucket of the given pool, series and branch.
func putUsedAddrHash(tx walletdb.Tx, poolID []byte, seriesID uint32, branch Branch,
index Index, encryptedHash []byte) error {
usedAddrs := tx.RootBucket().Bucket(poolID).Bucket(usedAddrsBucketName)
bucket, err := usedAddrs.CreateBucketIfNotExists(getUsedAddrBucketID(seriesID, branch))
if err != nil {
str := fmt.Sprintf("cannot create voting pool %v", votingPoolID)
return managerError(waddrmgr.ErrDatabase, str, err)
return newError(ErrDatabase, "failed to store used address hash", err)
}
return bucket.Put(uint32ToBytes(uint32(index)), encryptedHash)
}
// getUsedAddrHash returns the addr hash with the given index from the used
// addresses bucket of the given pool, series and branch.
func getUsedAddrHash(tx walletdb.Tx, poolID []byte, seriesID uint32, branch Branch,
index Index) []byte {
usedAddrs := tx.RootBucket().Bucket(poolID).Bucket(usedAddrsBucketName)
bucket := usedAddrs.Bucket(getUsedAddrBucketID(seriesID, branch))
if bucket == nil {
return nil
}
return bucket.Get(uint32ToBytes(uint32(index)))
}
// getMaxUsedIdx returns the highest used index from the used addresses bucket
// of the given pool, series and branch.
func getMaxUsedIdx(tx walletdb.Tx, poolID []byte, seriesID uint32, branch Branch) (Index, error) {
maxIdx := Index(0)
usedAddrs := tx.RootBucket().Bucket(poolID).Bucket(usedAddrsBucketName)
bucket := usedAddrs.Bucket(getUsedAddrBucketID(seriesID, branch))
if bucket == nil {
return maxIdx, nil
}
// FIXME: This is far from optimal and should be optimized either by storing
// a separate key in the DB with the highest used idx for every
// series/branch or perhaps by doing a large gap linear forward search +
// binary backwards search (e.g. check for 1000000, 2000000, .... until it
// doesn't exist, and then use a binary search to find the max using the
// discovered bounds).
err := bucket.ForEach(
func(k, v []byte) error {
idx := Index(bytesToUint32(k))
if idx > maxIdx {
maxIdx = idx
}
return nil
})
if err != nil {
return Index(0), newError(ErrDatabase, "failed to get highest idx of used addresses", err)
}
return maxIdx, nil
}
// putPool stores a voting pool in the database, creating a bucket named
// after the voting pool id and two other buckets inside it to store series and
// used addresses for that pool.
func putPool(tx walletdb.Tx, poolID []byte) error {
poolBucket, err := tx.RootBucket().CreateBucket(poolID)
if err != nil {
return newError(ErrDatabase, fmt.Sprintf("cannot create pool %v", poolID), err)
}
_, err = poolBucket.CreateBucket(seriesBucketName)
if err != nil {
return newError(ErrDatabase, fmt.Sprintf("cannot create series bucket for pool %v",
poolID), err)
}
_, err = poolBucket.CreateBucket(usedAddrsBucketName)
if err != nil {
return newError(ErrDatabase, fmt.Sprintf("cannot create used addrs bucket for pool %v",
poolID), err)
}
return nil
}
// loadAllSeries returns a map of all the series stored inside a voting pool
// bucket, keyed by id.
func loadAllSeries(tx walletdb.Tx, votingPoolID []byte) (map[uint32]*dbSeriesRow, error) {
bucket := tx.RootBucket().Bucket(votingPoolID)
func loadAllSeries(tx walletdb.Tx, poolID []byte) (map[uint32]*dbSeriesRow, error) {
bucket := tx.RootBucket().Bucket(poolID).Bucket(seriesBucketName)
allSeries := make(map[uint32]*dbSeriesRow)
err := bucket.ForEach(
func(k, v []byte) error {
seriesID := bytesToUint32(k)
series, err := deserializeSeriesRow(v)
if err != nil {
str := fmt.Sprintf("cannot deserialize series %v", v)
return managerError(waddrmgr.ErrSeriesStorage, str, err)
return err
}
allSeries[seriesID] = series
return nil
@ -91,14 +166,14 @@ func loadAllSeries(tx walletdb.Tx, votingPoolID []byte) (map[uint32]*dbSeriesRow
// existsPool checks the existence of a bucket named after the given
// voting pool id.
func existsPool(tx walletdb.Tx, votingPoolID []byte) bool {
bucket := tx.RootBucket().Bucket(votingPoolID)
func existsPool(tx walletdb.Tx, poolID []byte) bool {
bucket := tx.RootBucket().Bucket(poolID)
return bucket != nil
}
// putSeries stores the given series inside a voting pool bucket named after
// votingPoolID. The voting pool bucket does not need to be created beforehand.
func putSeries(tx walletdb.Tx, votingPoolID []byte, version, ID uint32, active bool, reqSigs uint32, pubKeysEncrypted, privKeysEncrypted [][]byte) error {
// poolID. The voting pool bucket does not need to be created beforehand.
func putSeries(tx walletdb.Tx, poolID []byte, version, ID uint32, active bool, reqSigs uint32, pubKeysEncrypted, privKeysEncrypted [][]byte) error {
row := &dbSeriesRow{
version: version,
active: active,
@ -106,27 +181,27 @@ func putSeries(tx walletdb.Tx, votingPoolID []byte, version, ID uint32, active b
pubKeysEncrypted: pubKeysEncrypted,
privKeysEncrypted: privKeysEncrypted,
}
return putSeriesRow(tx, votingPoolID, ID, row)
return putSeriesRow(tx, poolID, ID, row)
}
// putSeriesRow stores the given series row inside a voting pool bucket named
// after votingPoolID. The voting pool bucket does not need to be created
// after poolID. The voting pool bucket does not need to be created
// beforehand.
func putSeriesRow(tx walletdb.Tx, votingPoolID []byte, ID uint32, row *dbSeriesRow) error {
bucket, err := tx.RootBucket().CreateBucketIfNotExists(votingPoolID)
func putSeriesRow(tx walletdb.Tx, poolID []byte, ID uint32, row *dbSeriesRow) error {
bucket, err := tx.RootBucket().CreateBucketIfNotExists(poolID)
if err != nil {
str := fmt.Sprintf("cannot create bucket %v", votingPoolID)
return managerError(waddrmgr.ErrDatabase, str, err)
str := fmt.Sprintf("cannot create bucket %v", poolID)
return newError(ErrDatabase, str, err)
}
bucket = bucket.Bucket(seriesBucketName)
serialized, err := serializeSeriesRow(row)
if err != nil {
str := fmt.Sprintf("cannot serialize series %v", row)
return managerError(waddrmgr.ErrSeriesStorage, str, err)
return err
}
err = bucket.Put(uint32ToBytes(ID), serialized)
if err != nil {
str := fmt.Sprintf("cannot put series %v into bucket %v", serialized, votingPoolID)
return managerError(waddrmgr.ErrSeriesStorage, str, err)
str := fmt.Sprintf("cannot put series %v into bucket %v", serialized, poolID)
return newError(ErrDatabase, str, err)
}
return nil
}
@ -142,17 +217,15 @@ func deserializeSeriesRow(serializedSeries []byte) (*dbSeriesRow, error) {
// Given the above, the length of the serialized series should be
// at minimum the length of the constants.
if len(serializedSeries) < seriesMinSerial {
str := fmt.Sprintf("serialized series is too short: %v",
serializedSeries)
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
str := fmt.Sprintf("serialized series is too short: %v", serializedSeries)
return nil, newError(ErrSeriesSerialization, str, nil)
}
// Maximum number of public keys is 15 and the same for public keys
// this gives us an upper bound.
if len(serializedSeries) > seriesMaxSerial {
str := fmt.Sprintf("serialized series is too long: %v",
serializedSeries)
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
str := fmt.Sprintf("serialized series is too long: %v", serializedSeries)
return nil, newError(ErrSeriesSerialization, str, nil)
}
// Keeps track of the position of the next set of bytes to deserialize.
@ -163,7 +236,7 @@ func deserializeSeriesRow(serializedSeries []byte) (*dbSeriesRow, error) {
if row.version > seriesMaxVersion {
str := fmt.Sprintf("deserialization supports up to version %v not %v",
seriesMaxVersion, row.version)
return nil, managerError(waddrmgr.ErrSeriesVersion, str, nil)
return nil, newError(ErrSeriesVersion, str, nil)
}
current += 4
@ -178,13 +251,11 @@ func deserializeSeriesRow(serializedSeries []byte) (*dbSeriesRow, error) {
// Check to see if we have the right number of bytes to consume.
if len(serializedSeries) < current+int(nKeys)*seriesKeyLength*2 {
str := fmt.Sprintf("serialized series has not enough data: %v",
serializedSeries)
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
str := fmt.Sprintf("serialized series has not enough data: %v", serializedSeries)
return nil, newError(ErrSeriesSerialization, str, nil)
} else if len(serializedSeries) > current+int(nKeys)*seriesKeyLength*2 {
str := fmt.Sprintf("serialized series has too much data: %v",
serializedSeries)
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
str := fmt.Sprintf("serialized series has too much data: %v", serializedSeries)
return nil, newError(ErrSeriesSerialization, str, nil)
}
// Deserialize the pubkey/privkey pairs.
@ -219,13 +290,13 @@ func serializeSeriesRow(row *dbSeriesRow) ([]byte, error) {
len(row.pubKeysEncrypted) != len(row.privKeysEncrypted) {
str := fmt.Sprintf("different # of pub (%v) and priv (%v) keys",
len(row.pubKeysEncrypted), len(row.privKeysEncrypted))
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
return nil, newError(ErrSeriesSerialization, str, nil)
}
if row.version > seriesMaxVersion {
str := fmt.Sprintf("serialization supports up to version %v, not %v",
seriesMaxVersion, row.version)
return nil, managerError(waddrmgr.ErrSeriesVersion, str, nil)
return nil, newError(ErrSeriesVersion, str, nil)
}
serialized := make([]byte, 0, serializedLen)
@ -245,7 +316,7 @@ func serializeSeriesRow(row *dbSeriesRow) ([]byte, error) {
if len(pubKeyEncrypted) != seriesKeyLength {
str := fmt.Sprintf("wrong length of Encrypted Public Key: %v",
pubKeyEncrypted)
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
return nil, newError(ErrSeriesSerialization, str, nil)
}
serialized = append(serialized, pubKeyEncrypted...)
@ -260,7 +331,7 @@ func serializeSeriesRow(row *dbSeriesRow) ([]byte, error) {
} else if len(privKeyEncrypted) != seriesKeyLength {
str := fmt.Sprintf("wrong length of Encrypted Private Key: %v",
len(privKeyEncrypted))
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
return nil, newError(ErrSeriesSerialization, str, nil)
} else {
serialized = append(serialized, privKeyEncrypted...)
}

82
votingpool/db_wb_test.go Normal file
View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2015 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool
import (
"bytes"
"testing"
"github.com/btcsuite/btcwallet/walletdb"
)
func TestPutUsedAddrHash(t *testing.T) {
tearDown, _, pool := TstCreatePool(t)
defer tearDown()
dummyHash := bytes.Repeat([]byte{0x09}, 10)
err := pool.namespace.Update(
func(tx walletdb.Tx) error {
return putUsedAddrHash(tx, pool.ID, 0, 0, 0, dummyHash)
})
if err != nil {
t.Fatal(err)
}
var storedHash []byte
err = pool.namespace.View(
func(tx walletdb.Tx) error {
storedHash = getUsedAddrHash(tx, pool.ID, 0, 0, 0)
return nil
})
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(storedHash, dummyHash) {
t.Fatalf("Wrong stored hash; got %x, want %x", storedHash, dummyHash)
}
}
func TestGetMaxUsedIdx(t *testing.T) {
tearDown, _, pool := TstCreatePool(t)
defer tearDown()
var err error
pool.namespace.Update(
func(tx walletdb.Tx) error {
for i, idx := range []int{0, 7, 9, 3001, 41, 500, 6} {
dummyHash := bytes.Repeat([]byte{byte(i)}, 10)
err = putUsedAddrHash(tx, pool.ID, 0, 0, Index(idx), dummyHash)
if err != nil {
t.Fatal(err)
}
}
return nil
})
var maxIdx Index
pool.namespace.View(
func(tx walletdb.Tx) error {
maxIdx, err = getMaxUsedIdx(tx, pool.ID, 0, 0)
if err != nil {
t.Fatal(err)
}
return nil
})
if maxIdx != Index(3001) {
t.Fatalf("Wrong max idx; got %d, want %d", maxIdx, Index(3001))
}
}

View file

@ -20,19 +20,24 @@ Package votingpool provides voting pool functionality for btcwallet.
Overview
The purpose of the voting pool package is to make it possible to store
bitcoins using m-of-n multisig transactions. Each member of the pool
holds one of the n private keys needed to create a transaction and can
only create transactions that can spend the bitcoins if m - 1 other
members of the pool agree to it.
bitcoins using m-of-n multisig transactions. A pool can have multiple
series, each of them with a set of pubkeys (one for each of the members
in that pool's series) and the minimum number of required signatures (m)
needed to spend the pool's coins. Each member will hold a private key
matching one of the series' public keys, and at least m members will
need to be in agreement when spending the pool's coins.
This package depends on the waddrmgr package, and in particular
instances of the waddrgmgr.Manager structure.
More details about voting pools as well as some of its use cases can
be found at http://opentransactions.org/wiki/index.php?title=Category:Voting_Pools
This package depends on the waddrmgr and walletdb packages.
Creating a voting pool
A voting pool is created via the Create function. This function
accepts a database namespace which will be used to store all
information about the pool as well as a poolID.
information related to that pool under a bucket whose key is the
pool's ID.
Loading an existing pool
@ -43,28 +48,52 @@ Creating a series
A series can be created via the CreateSeries method, which accepts a
version number, a series identifier, a number of required signatures
(m in m-of-n multisig, and a set of public keys.
(m in m-of-n multisig), and a set of public keys.
Deposit Addresses
A deposit address can be created via the DepositScriptAddress
method, which based on a seriesID a branch number and an index
creates a pay-to-script-hash address, where the script is a multisig
script. The public keys used as inputs for generating the address are
generated from the public keys passed to CreateSeries. In [1] the
generated public keys correspend to the lowest level or the
'address_index' in the hierarchy.
method, which returns a series-specific P2SH address from the multi-sig
script constructed with the index-th child of the series' public keys and
sorted according to the given branch. The procedure to construct multi-sig
deposit addresses is described in detail at
http://opentransactions.org/wiki/index.php/Deposit_Address_(voting_pools)
Replacing a series
A series can be replaced via the ReplaceSeries method. It accepts
the same parameters as the CreateSeries method.
Empowering a series
Documentation
For security reasons, most private keys will be maintained offline and
only brought online when they're needed. In order to bring a key online,
one must use the EmpowerSeries method, which takes just the series ID
and a raw private key matching one of the series' public keys.
[1] https://github.com/justusranvier/bips/blob/master/bip-draft-Hierarchy%20for%20Non-Colored%20Voting%20Pool%20Deterministic%20Multisig%20Wallets.mediawiki
Starting withdrawals
When withdrawing coins from the pool, we employ a deterministic process
in order to minimise the cost of coordinating transaction signing. For
this to work, members of the pool have to perform an out-of-band consensus
process (<http://opentransactions.org/wiki/index.php/Consensus_Process_(voting_pools)>)
to define the following parameters, that should be passed to the
StartWithdrawal method:
roundID: the unique identifier of a given consensus round
requests: a list with outputs requested by users of the voting pool
startAddress: the seriesID, branch and indes where we should start looking for inputs
lastSeriesID: the ID of the last series where we should take inputs from
changeStart: the first change address to use
dustThreshold: the minimum amount of satoshis an input needs to be considered eligible
StartWithdrawal will then select all eligible inputs in the given address
range (following the algorithim at <http://opentransactions.org/wiki/index.php/Input_Selection_Algorithm_(voting_pools)>)
and use them to construct transactions (<http://opentransactions.org/wiki/index.php/Category:Transaction_Construction_Algorithm_(voting_pools)>)
that fulfill the output requests. It returns a WithdrawalStatus containing
the state of every requested output, the raw signatures for the constructed
transactions, the network fees included in those transactions and the input
range to use in the next withdrawal.
*/
package votingpool

219
votingpool/error.go Normal file
View file

@ -0,0 +1,219 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool
import "fmt"
// ErrorCode identifies a kind of error
type ErrorCode int
const (
// ErrInputSelection indicates an error in the input selection
// algorithm.
ErrInputSelection ErrorCode = iota
// ErrWithdrawalProcessing indicates an internal error when processing a
// withdrawal request.
ErrWithdrawalProcessing
// ErrUnknownPubKey indicates a pubkey that does not belong to a given
// series.
ErrUnknownPubKey
// ErrSeriesSerialization indicates that an error occurred while
// serializing or deserializing one or more series for storing into
// the database.
ErrSeriesSerialization
// ErrSeriesVersion indicates that we've been asked to deal with a series
// whose version is unsupported
ErrSeriesVersion
// ErrSeriesNotExists indicates that an attempt has been made to access
// a series that does not exist.
ErrSeriesNotExists
// ErrSeriesAlreadyExists indicates that an attempt has been made to
// create a series that already exists.
ErrSeriesAlreadyExists
// ErrSeriesAlreadyEmpowered indicates that an already empowered series
// was used where a not empowered one was expected.
ErrSeriesAlreadyEmpowered
// ErrSeriesNotActive indicates that an active series was needed but the
// selected one is not.
ErrSeriesNotActive
// ErrKeyIsPrivate indicates that a private key was used where a public
// one was expected.
ErrKeyIsPrivate
// ErrKeyIsPublic indicates that a public key was used where a private
// one was expected.
ErrKeyIsPublic
// ErrKeyNeuter indicates a problem when trying to neuter a private key.
ErrKeyNeuter
// ErrKeyMismatch indicates that the key is not the expected one.
ErrKeyMismatch
// ErrKeysPrivatePublicMismatch indicates that the number of private and
// public keys is not the same.
ErrKeysPrivatePublicMismatch
// ErrKeyDuplicate indicates that a key is duplicated.
ErrKeyDuplicate
// ErrTooFewPublicKeys indicates that a required minimum of public
// keys was not met.
ErrTooFewPublicKeys
// ErrPoolAlreadyExists indicates that an attempt has been made to
// create a voting pool that already exists.
ErrPoolAlreadyExists
// ErrPoolNotExists indicates that an attempt has been made to access
// a voting pool that does not exist.
ErrPoolNotExists
// ErrScriptCreation indicates that the creation of a deposit script
// failed.
ErrScriptCreation
// ErrTooManyReqSignatures indicates that too many required
// signatures are requested.
ErrTooManyReqSignatures
// ErrInvalidBranch indicates that the given branch number is not valid
// for a given set of public keys.
ErrInvalidBranch
// ErrInvalidValue indicates that the value of a given function argument
// is invalid.
ErrInvalidValue
// ErrDatabase indicates an error with the underlying database.
ErrDatabase
// ErrKeyChain indicates an error with the key chain typically either
// due to the inability to create an extended key or deriving a child
// extended key.
ErrKeyChain
// ErrCrypto indicates an error with the cryptography related operations
// such as decrypting or encrypting data, parsing an EC public key,
// or deriving a secret key from a password.
ErrCrypto
// ErrRawSigning indicates an error in the process of generating raw
// signatures for a transaction input.
ErrRawSigning
// ErrPreconditionNotMet indicates a programming error since a
// preconditon has not been met.
ErrPreconditionNotMet
// ErrTxSigning indicates an error when signing a transaction.
ErrTxSigning
// ErrSeriesIDNotSequential indicates an attempt to create a series with
// an ID that is not sequantial.
ErrSeriesIDNotSequential
// ErrInvalidScriptHash indicates an invalid P2SH.
ErrInvalidScriptHash
// ErrWithdrawFromUnusedAddr indicates an attempt to withdraw funds from
// an address which has not been used before.
ErrWithdrawFromUnusedAddr
// ErrSeriesIDInvalid indicates an attempt to create a series with an
// invalid ID.
ErrSeriesIDInvalid
// lastErr is used for testing, making it possible to iterate over
// the error codes in order to check that they all have proper
// translations in errorCodeStrings.
lastErr
)
// Map of ErrorCode values back to their constant names for pretty printing.
var errorCodeStrings = map[ErrorCode]string{
ErrInputSelection: "ErrInputSelection",
ErrWithdrawalProcessing: "ErrWithdrawalProcessing",
ErrUnknownPubKey: "ErrUnknownPubKey",
ErrSeriesSerialization: "ErrSeriesSerialization",
ErrSeriesVersion: "ErrSeriesVersion",
ErrSeriesNotExists: "ErrSeriesNotExists",
ErrSeriesAlreadyExists: "ErrSeriesAlreadyExists",
ErrSeriesAlreadyEmpowered: "ErrSeriesAlreadyEmpowered",
ErrSeriesIDNotSequential: "ErrSeriesIDNotSequential",
ErrSeriesIDInvalid: "ErrSeriesIDInvalid",
ErrSeriesNotActive: "ErrSeriesNotActive",
ErrKeyIsPrivate: "ErrKeyIsPrivate",
ErrKeyIsPublic: "ErrKeyIsPublic",
ErrKeyNeuter: "ErrKeyNeuter",
ErrKeyMismatch: "ErrKeyMismatch",
ErrKeysPrivatePublicMismatch: "ErrKeysPrivatePublicMismatch",
ErrKeyDuplicate: "ErrKeyDuplicate",
ErrTooFewPublicKeys: "ErrTooFewPublicKeys",
ErrPoolAlreadyExists: "ErrPoolAlreadyExists",
ErrPoolNotExists: "ErrPoolNotExists",
ErrScriptCreation: "ErrScriptCreation",
ErrTooManyReqSignatures: "ErrTooManyReqSignatures",
ErrInvalidBranch: "ErrInvalidBranch",
ErrInvalidValue: "ErrInvalidValue",
ErrDatabase: "ErrDatabase",
ErrKeyChain: "ErrKeyChain",
ErrCrypto: "ErrCrypto",
ErrRawSigning: "ErrRawSigning",
ErrPreconditionNotMet: "ErrPreconditionNotMet",
ErrTxSigning: "ErrTxSigning",
ErrInvalidScriptHash: "ErrInvalidScriptHash",
ErrWithdrawFromUnusedAddr: "ErrWithdrawFromUnusedAddr",
}
// String returns the ErrorCode as a human-readable name.
func (e ErrorCode) String() string {
if s := errorCodeStrings[e]; s != "" {
return s
}
return fmt.Sprintf("Unknown ErrorCode (%d)", int(e))
}
// Error is a typed error for all errors arising during the
// operation of the voting pool.
type Error struct {
ErrorCode ErrorCode // Describes the kind of error
Description string // Human readable description of the issue
Err error // Underlying error
}
// Error satisfies the error interface and prints human-readable errors.
func (e Error) Error() string {
if e.Err != nil {
return e.Description + ": " + e.Err.Error()
}
return e.Description
}
// newError creates a new Error.
func newError(c ErrorCode, desc string, err error) Error {
return Error{ErrorCode: c, Description: desc, Err: err}
}

82
votingpool/error_test.go Normal file
View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool_test
import (
"testing"
vp "github.com/btcsuite/btcwallet/votingpool"
)
// TestErrorCodeStringer tests that all error codes has a text
// representation and that text representation is still correct,
// ie. that a refactoring and renaming of the error code has not
// drifted from the textual representation.
func TestErrorCodeStringer(t *testing.T) {
// All the errors in ths
tests := []struct {
in vp.ErrorCode
want string
}{
{vp.ErrInputSelection, "ErrInputSelection"},
{vp.ErrWithdrawalProcessing, "ErrWithdrawalProcessing"},
{vp.ErrUnknownPubKey, "ErrUnknownPubKey"},
{vp.ErrSeriesSerialization, "ErrSeriesSerialization"},
{vp.ErrSeriesVersion, "ErrSeriesVersion"},
{vp.ErrSeriesNotExists, "ErrSeriesNotExists"},
{vp.ErrSeriesAlreadyExists, "ErrSeriesAlreadyExists"},
{vp.ErrSeriesAlreadyEmpowered, "ErrSeriesAlreadyEmpowered"},
{vp.ErrSeriesIDNotSequential, "ErrSeriesIDNotSequential"},
{vp.ErrSeriesIDInvalid, "ErrSeriesIDInvalid"},
{vp.ErrSeriesNotActive, "ErrSeriesNotActive"},
{vp.ErrKeyIsPrivate, "ErrKeyIsPrivate"},
{vp.ErrKeyIsPublic, "ErrKeyIsPublic"},
{vp.ErrKeyNeuter, "ErrKeyNeuter"},
{vp.ErrKeyMismatch, "ErrKeyMismatch"},
{vp.ErrKeysPrivatePublicMismatch, "ErrKeysPrivatePublicMismatch"},
{vp.ErrKeyDuplicate, "ErrKeyDuplicate"},
{vp.ErrTooFewPublicKeys, "ErrTooFewPublicKeys"},
{vp.ErrPoolAlreadyExists, "ErrPoolAlreadyExists"},
{vp.ErrPoolNotExists, "ErrPoolNotExists"},
{vp.ErrScriptCreation, "ErrScriptCreation"},
{vp.ErrTooManyReqSignatures, "ErrTooManyReqSignatures"},
{vp.ErrInvalidBranch, "ErrInvalidBranch"},
{vp.ErrInvalidValue, "ErrInvalidValue"},
{vp.ErrDatabase, "ErrDatabase"},
{vp.ErrKeyChain, "ErrKeyChain"},
{vp.ErrCrypto, "ErrCrypto"},
{vp.ErrRawSigning, "ErrRawSigning"},
{vp.ErrPreconditionNotMet, "ErrPreconditionNotMet"},
{vp.ErrTxSigning, "ErrTxSigning"},
{vp.ErrInvalidScriptHash, "ErrInvalidScriptHash"},
{vp.ErrWithdrawFromUnusedAddr, "ErrWithdrawFromUnusedAddr"},
{0xffff, "Unknown ErrorCode (65535)"},
}
if int(vp.TstLastErr) != len(tests)-1 {
t.Errorf("Wrong number of errorCodeStrings. Got: %d, want: %d",
int(vp.TstLastErr), len(tests))
}
for i, test := range tests {
result := test.in.String()
if result != test.want {
t.Errorf("String #%d\ngot: %s\nwant: %s", i, result,
test.want)
}
}
}

View file

@ -17,69 +17,92 @@
package votingpool_test
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/txstore"
"github.com/btcsuite/btcwallet/votingpool"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
)
func Example_basic() {
// This example demonstrates how to create a voting pool, create a
// series, get a deposit address from a series and lastly how to
// replace a series.
var (
pubPassphrase = []byte("pubPassphrase")
privPassphrase = []byte("privPassphrase")
)
// Create a new wallet DB.
dir, err := ioutil.TempDir("", "pool_test")
func ExampleCreate() {
// Create a new walletdb.DB. See the walletdb docs for instructions on how
// to do that.
db, dbTearDown, err := createWalletDB()
if err != nil {
fmt.Printf("Failed to create db dir: %v\n", err)
fmt.Println(err)
return
}
db, err := walletdb.Create("bdb", filepath.Join(dir, "wallet.db"))
if err != nil {
fmt.Printf("Failed to create wallet DB: %v\n", err)
return
}
defer os.RemoveAll(dir)
defer db.Close()
defer dbTearDown()
// Create a new walletdb namespace for the address manager.
mgrNamespace, err := db.Namespace([]byte("waddrmgr"))
if err != nil {
fmt.Printf("Failed to create addr manager DB namespace: %v\n", err)
fmt.Println(err)
return
}
// Create the address manager
mgr, err := waddrmgr.Create(mgrNamespace, seed, pubPassphrase, privPassphrase,
&chaincfg.MainNetParams, nil)
// Create the address manager.
seed := bytes.Repeat([]byte{0x2a, 0x64, 0xdf, 0x08}, 8)
var fastScrypt = &waddrmgr.Options{ScryptN: 16, ScryptR: 8, ScryptP: 1}
mgr, err := waddrmgr.Create(
mgrNamespace, seed, pubPassphrase, privPassphrase, &chaincfg.MainNetParams, fastScrypt)
if err != nil {
fmt.Printf("Failed to create addr manager: %v\n", err)
fmt.Println(err)
return
}
defer mgr.Close()
// Create a walletdb for votingpools.
// Create a walletdb namespace for votingpools.
vpNamespace, err := db.Namespace([]byte("votingpool"))
if err != nil {
fmt.Printf("Failed to create VotingPool DB namespace: %v\n", err)
fmt.Println(err)
return
}
// Create a voting pool.
_, err = votingpool.Create(vpNamespace, mgr, []byte{0x00})
if err != nil {
fmt.Println(err)
return
}
// Output:
//
}
// This example demonstrates how to create a voting pool with one
// series and get a deposit address for that series.
func Example_depositAddress() {
// Create the address manager and votingpool DB namespace. See the example
// for the Create() function for more info on how this is done.
mgr, vpNamespace, tearDownFunc, err := exampleCreateMgrAndDBNamespace()
if err != nil {
fmt.Println(err)
return
}
defer tearDownFunc()
// Create the voting pool.
pool, err := votingpool.Create(vpNamespace, mgr, []byte{0x00})
if err != nil {
fmt.Printf("Voting Pool creation failed: %v\n", err)
fmt.Println(err)
return
}
// Create a 2-of-3 series.
apiVersion := uint32(1)
seriesID := uint32(1)
requiredSignatures := uint32(2)
pubKeys := []string{
@ -87,39 +110,220 @@ func Example_basic() {
"xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9",
"xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh",
}
err = pool.CreateSeries(apiVersion, seriesID, requiredSignatures, pubKeys)
err = pool.CreateSeries(votingpool.CurrentVersion, seriesID, requiredSignatures, pubKeys)
if err != nil {
fmt.Printf("Cannot create series: %v\n", err)
fmt.Println(err)
return
}
// Create a deposit address.
branch := uint32(0) // The change branch
index := uint32(1)
addr, err := pool.DepositScriptAddress(seriesID, branch, index)
addr, err := pool.DepositScriptAddress(seriesID, votingpool.Branch(0), votingpool.Index(1))
if err != nil {
fmt.Printf("DepositScriptAddress failed for series: %d, branch: %d, index: %d\n",
seriesID, branch, index)
fmt.Println(err)
return
}
fmt.Println("Generated deposit address:", addr.EncodeAddress())
// Replace the existing series with a 3-of-5 series.
pubKeys = []string{
"xpub661MyMwAqRbcFQfXKHwz8ZbTtePwAKu8pmGYyVrWEM96DYUTWDYipMnHrFcemZHn13jcRMfsNU3UWQUudiaE7mhkWCHGFRMavF167DQM4Va",
"xpub661MyMwAqRbcGnTEXx3ehjx8EiqQGnL4uhwZw3ZxvZAa2E6E4YVAp63UoVtvm2vMDDF8BdPpcarcf7PWcEKvzHhxzAYw1zG23C2egeh82AR",
"xpub661MyMwAqRbcG83KwFyr1RVrNUmqVwYxV6nzxbqoRTNc8fRnWxq1yQiTBifTHhevcEM9ucZ1TqFS7Kv17Gd81cesv6RDrrvYS9SLPjPXhV5",
"xpub661MyMwAqRbcFGJbLPhMjtpC1XntFpg6jjQWjr6yXN8b9wfS1RiU5EhJt5L7qoFuidYawc3XJoLjT2PcjVpXryS3hn1WmSPCyvQDNuKsfgM",
"xpub661MyMwAqRbcGJDX4GYocn7qCzvMJwNisxpzkYZAakcvXtWV6CanXuz9xdfe5kTptFMJ4hDt2iTiT11zyN14u8R5zLvoZ1gnEVqNLxp1r3v",
"xpub661MyMwAqRbcG13FtwvZVaA15pTerP4JdAGvytPykqDr2fKXePqw3wLhCALPAixsE176jFkc2ac9K3tnF4KwaTRKUqFF5apWD6XL9LHCu7E",
}
requiredSignatures = 3
err = pool.ReplaceSeries(apiVersion, seriesID, requiredSignatures, pubKeys)
// Output:
// Generated deposit address: 3QTzpc9d3tTbNLJLB7xwt87nWM38boAhAw
}
// This example demonstrates how to empower a series by loading the private
// key for one of the series' public keys.
func Example_empowerSeries() {
// Create the address manager and votingpool DB namespace. See the example
// for the Create() function for more info on how this is done.
mgr, vpNamespace, tearDownFunc, err := exampleCreateMgrAndDBNamespace()
if err != nil {
fmt.Printf("Cannot replace series: %v\n", err)
fmt.Println(err)
return
}
defer tearDownFunc()
// Create a pool and a series. See the DepositAddress example for more info
// on how this is done.
pool, seriesID, err := exampleCreatePoolAndSeries(mgr, vpNamespace)
if err != nil {
fmt.Println(err)
return
}
// Now empower the series with one of its private keys. Notice that in order
// to do that we need to unlock the address manager.
if err := mgr.Unlock(privPassphrase); err != nil {
fmt.Println(err)
return
}
defer mgr.Lock()
privKey := "xprv9s21ZrQH143K2j9PK4CXkCu8sgxkpUxCF7p1KVwiV5tdnkeYzJXReUkxz5iB2FUzTXC1L15abCDG4RMxSYT5zhm67uvsnLYxuDhZfoFcB6a"
err = pool.EmpowerSeries(seriesID, privKey)
if err != nil {
fmt.Println(err)
return
}
// Output:
// Generated deposit address: 3QTzpc9d3tTbNLJLB7xwt87nWM38boAhAw
//
}
// This example demonstrates how to empower a series by loading the private
// key for one of the series' public keys.
func Example_startWithdrawal() {
// Create the address manager and votingpool DB namespace. See the example
// for the Create() function for more info on how this is done.
mgr, vpNamespace, tearDownFunc, err := exampleCreateMgrAndDBNamespace()
if err != nil {
fmt.Println(err)
return
}
defer tearDownFunc()
// Create a pool and a series. See the DepositAddress example for more info
// on how this is done.
pool, seriesID, err := exampleCreatePoolAndSeries(mgr, vpNamespace)
if err != nil {
fmt.Println(err)
return
}
// Unlock the manager
if err := mgr.Unlock(privPassphrase); err != nil {
fmt.Println(err)
return
}
defer mgr.Lock()
addr, _ := btcutil.DecodeAddress("1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX", mgr.ChainParams())
pkScript, _ := txscript.PayToAddrScript(addr)
requests := []votingpool.OutputRequest{
votingpool.OutputRequest{
PkScript: pkScript,
Address: addr,
Amount: 1e6,
Server: "server-id",
Transaction: 123},
}
changeStart, err := pool.ChangeAddress(seriesID, votingpool.Index(0))
if err != nil {
fmt.Println(err)
return
}
// This is only needed because we have not used any deposit addresses from
// the series, and we cannot create a WithdrawalAddress for an unused
// branch/idx pair.
if err = pool.EnsureUsedAddr(seriesID, votingpool.Branch(1), votingpool.Index(0)); err != nil {
fmt.Println(err)
return
}
startAddr, err := pool.WithdrawalAddress(seriesID, votingpool.Branch(1), votingpool.Index(0))
if err != nil {
fmt.Println(err)
return
}
lastSeriesID := seriesID
dustThreshold := btcutil.Amount(1e4)
currentBlock := int32(19432)
roundID := uint32(0)
txstore, tearDownFunc, err := exampleCreateTxStore()
if err != nil {
fmt.Println(err)
return
}
_, err = pool.StartWithdrawal(
roundID, requests, *startAddr, lastSeriesID, *changeStart, txstore, currentBlock,
dustThreshold)
if err != nil {
fmt.Println(err)
}
// Output:
//
}
func createWalletDB() (walletdb.DB, func(), error) {
dir, err := ioutil.TempDir("", "votingpool_example")
if err != nil {
return nil, nil, err
}
db, err := walletdb.Create("bdb", filepath.Join(dir, "wallet.db"))
if err != nil {
return nil, nil, err
}
dbTearDown := func() {
db.Close()
os.RemoveAll(dir)
}
return db, dbTearDown, nil
}
func exampleCreateMgrAndDBNamespace() (*waddrmgr.Manager, walletdb.Namespace, func(), error) {
db, dbTearDown, err := createWalletDB()
if err != nil {
return nil, nil, nil, err
}
// Create a new walletdb namespace for the address manager.
mgrNamespace, err := db.Namespace([]byte("waddrmgr"))
if err != nil {
dbTearDown()
return nil, nil, nil, err
}
// Create the address manager
seed := bytes.Repeat([]byte{0x2a, 0x64, 0xdf, 0x08}, 8)
var fastScrypt = &waddrmgr.Options{ScryptN: 16, ScryptR: 8, ScryptP: 1}
mgr, err := waddrmgr.Create(
mgrNamespace, seed, pubPassphrase, privPassphrase, &chaincfg.MainNetParams, fastScrypt)
if err != nil {
dbTearDown()
return nil, nil, nil, err
}
tearDownFunc := func() {
mgr.Close()
dbTearDown()
}
// Create a walletdb namespace for votingpools.
vpNamespace, err := db.Namespace([]byte("votingpool"))
if err != nil {
tearDownFunc()
return nil, nil, nil, err
}
return mgr, vpNamespace, tearDownFunc, nil
}
func exampleCreatePoolAndSeries(mgr *waddrmgr.Manager, vpNamespace walletdb.Namespace) (
*votingpool.Pool, uint32, error) {
pool, err := votingpool.Create(vpNamespace, mgr, []byte{0x00})
if err != nil {
return nil, 0, err
}
// Create a 2-of-3 series.
seriesID := uint32(1)
requiredSignatures := uint32(2)
pubKeys := []string{
"xpub661MyMwAqRbcFDDrR5jY7LqsRioFDwg3cLjc7tML3RRcfYyhXqqgCH5SqMSQdpQ1Xh8EtVwcfm8psD8zXKPcRaCVSY4GCqbb3aMEs27GitE",
"xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9",
"xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh",
}
err = pool.CreateSeries(votingpool.CurrentVersion, seriesID, requiredSignatures, pubKeys)
if err != nil {
return nil, 0, err
}
err = pool.ActivateSeries(seriesID)
if err != nil {
return nil, 0, err
}
return pool, seriesID, nil
}
func exampleCreateTxStore() (*txstore.Store, func(), error) {
dir, err := ioutil.TempDir("", "tx.bin")
if err != nil {
return nil, nil, err
}
s := txstore.New(dir)
return s, func() { os.RemoveAll(dir) }, nil
}

388
votingpool/factory_test.go Normal file
View file

@ -0,0 +1,388 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
// Helpers to create parameterized objects to use in tests.
package votingpool
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"sync/atomic"
"testing"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/txstore"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
)
var (
// seed is the master seed used to create extended keys.
seed = bytes.Repeat([]byte{0x2a, 0x64, 0xdf, 0x08}, 8)
pubPassphrase = []byte("_DJr{fL4H0O}*-0\n:V1izc)(6BomK")
privPassphrase = []byte("81lUHXnOMZ@?XXd7O9xyDIWIbXX-lj")
uniqueCounter = uint32(0)
// The block height where all our test inputs are created.
TstInputsBlock = int32(10)
)
func getUniqueID() uint32 {
return atomic.AddUint32(&uniqueCounter, 1)
}
// createWithdrawalTx creates a withdrawalTx with the given input and output amounts.
func createWithdrawalTx(t *testing.T, pool *Pool, store *txstore.Store, inputAmounts []int64,
outputAmounts []int64) *withdrawalTx {
net := pool.Manager().ChainParams()
tx := newWithdrawalTx()
_, credits := TstCreateCredits(t, pool, inputAmounts, store)
for _, c := range credits {
tx.addInput(c)
}
for i, amount := range outputAmounts {
request := TstNewOutputRequest(
t, uint32(i), "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", btcutil.Amount(amount), net)
tx.addOutput(request)
}
return tx
}
func createMsgTx(pkScript []byte, amts []int64) *wire.MsgTx {
msgtx := &wire.MsgTx{
Version: 1,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: wire.OutPoint{
Hash: wire.ShaHash{},
Index: 0xffffffff,
},
SignatureScript: []byte{txscript.OP_NOP},
Sequence: 0xffffffff,
},
},
LockTime: 0,
}
for _, amt := range amts {
msgtx.AddTxOut(wire.NewTxOut(amt, pkScript))
}
return msgtx
}
func TstNewDepositScript(t *testing.T, p *Pool, seriesID uint32, branch Branch, idx Index) []byte {
script, err := p.DepositScript(seriesID, branch, idx)
if err != nil {
t.Fatalf("Failed to create deposit script for series %d, branch %d, index %d: %v",
seriesID, branch, idx, err)
}
return script
}
// TstEnsureUsedAddr ensures the addresses defined by the given series/branch and
// index==0..idx are present in the set of used addresses for the given Pool.
func TstEnsureUsedAddr(t *testing.T, p *Pool, seriesID uint32, branch Branch, idx Index) []byte {
addr, err := p.getUsedAddr(seriesID, branch, idx)
if err != nil {
t.Fatal(err)
} else if addr != nil {
var script []byte
TstRunWithManagerUnlocked(t, p.Manager(), func() {
script, err = addr.Script()
})
if err != nil {
t.Fatal(err)
}
return script
}
TstRunWithManagerUnlocked(t, p.Manager(), func() {
err = p.EnsureUsedAddr(seriesID, branch, idx)
})
if err != nil {
t.Fatal(err)
}
return TstNewDepositScript(t, p, seriesID, branch, idx)
}
func TstCreatePkScript(t *testing.T, p *Pool, seriesID uint32, branch Branch, idx Index) []byte {
script := TstEnsureUsedAddr(t, p, seriesID, branch, idx)
addr, err := p.addressFor(script)
if err != nil {
t.Fatal(err)
}
pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
t.Fatal(err)
}
return pkScript
}
func TstCreateTxStore(t *testing.T) (store *txstore.Store, tearDown func()) {
dir, err := ioutil.TempDir("", "tx.bin")
if err != nil {
t.Fatalf("Failed to create db file: %v", err)
}
s := txstore.New(dir)
return s, func() { os.RemoveAll(dir) }
}
type TstSeriesDef struct {
ReqSigs uint32
PubKeys []string
PrivKeys []string
SeriesID uint32
Inactive bool
}
// TstCreateSeries creates a new Series for every definition in the given slice
// of TstSeriesDef. If the definition includes any private keys, the Series is
// empowered with them.
func TstCreateSeries(t *testing.T, pool *Pool, definitions []TstSeriesDef) {
for _, def := range definitions {
err := pool.CreateSeries(CurrentVersion, def.SeriesID, def.ReqSigs, def.PubKeys)
if err != nil {
t.Fatalf("Cannot creates series %d: %v", def.SeriesID, err)
}
TstRunWithManagerUnlocked(t, pool.Manager(), func() {
for _, key := range def.PrivKeys {
if err := pool.EmpowerSeries(def.SeriesID, key); err != nil {
t.Fatal(err)
}
}
})
pool.Series(def.SeriesID).active = !def.Inactive
}
}
func TstCreateMasterKey(t *testing.T, seed []byte) *hdkeychain.ExtendedKey {
key, err := hdkeychain.NewMaster(seed)
if err != nil {
t.Fatal(err)
}
return key
}
// createMasterKeys creates count master ExtendedKeys with unique seeds.
func createMasterKeys(t *testing.T, count int) []*hdkeychain.ExtendedKey {
keys := make([]*hdkeychain.ExtendedKey, count)
for i := range keys {
keys[i] = TstCreateMasterKey(t, bytes.Repeat(uint32ToBytes(getUniqueID()), 4))
}
return keys
}
// TstCreateSeriesDef creates a TstSeriesDef with a unique SeriesID, the given
// reqSigs and the raw public/private keys extracted from the list of private
// keys. The new series will be empowered with all private keys.
func TstCreateSeriesDef(t *testing.T, pool *Pool, reqSigs uint32, keys []*hdkeychain.ExtendedKey) TstSeriesDef {
pubKeys := make([]string, len(keys))
privKeys := make([]string, len(keys))
for i, key := range keys {
privKeys[i] = key.String()
pubkey, _ := key.Neuter()
pubKeys[i] = pubkey.String()
}
seriesID := uint32(len(pool.seriesLookup))
if seriesID == 0 {
seriesID++
}
return TstSeriesDef{
ReqSigs: reqSigs, SeriesID: seriesID, PubKeys: pubKeys, PrivKeys: privKeys}
}
func TstCreatePoolAndTxStore(t *testing.T) (tearDown func(), pool *Pool, store *txstore.Store) {
mgrTearDown, _, pool := TstCreatePool(t)
store, storeTearDown := TstCreateTxStore(t)
tearDown = func() {
mgrTearDown()
storeTearDown()
}
return tearDown, pool, store
}
// TstCreateCredits creates a new Series (with a unique ID) and a slice of
// credits locked to the series' address with branch==1 and index==0. The new
// Series will use a 2-of-3 configuration and will be empowered with all of its
// private keys.
func TstCreateCredits(t *testing.T, pool *Pool, amounts []int64, store *txstore.Store) (
uint32, []Credit) {
masters := []*hdkeychain.ExtendedKey{
TstCreateMasterKey(t, bytes.Repeat(uint32ToBytes(getUniqueID()), 4)),
TstCreateMasterKey(t, bytes.Repeat(uint32ToBytes(getUniqueID()), 4)),
TstCreateMasterKey(t, bytes.Repeat(uint32ToBytes(getUniqueID()), 4)),
}
def := TstCreateSeriesDef(t, pool, 2, masters)
TstCreateSeries(t, pool, []TstSeriesDef{def})
return def.SeriesID, TstCreateCreditsOnSeries(t, pool, def.SeriesID, amounts, store)
}
// TstCreateCreditsOnSeries creates a slice of credits locked to the given
// series' address with branch==1 and index==0.
func TstCreateCreditsOnSeries(t *testing.T, pool *Pool, seriesID uint32, amounts []int64,
store *txstore.Store) []Credit {
branch := Branch(1)
idx := Index(0)
pkScript := TstCreatePkScript(t, pool, seriesID, branch, idx)
eligible := make([]Credit, len(amounts))
for i, credit := range TstCreateInputs(t, store, pkScript, amounts) {
eligible[i] = newCredit(credit, *TstNewWithdrawalAddress(t, pool, seriesID, branch, idx))
}
return eligible
}
// TstCreateInputs is a convenience function. See TstCreateInputsOnBlock
// for a more flexible version.
func TstCreateInputs(t *testing.T, store *txstore.Store, pkScript []byte, amounts []int64) []txstore.Credit {
return TstCreateInputsOnBlock(t, store, 1, pkScript, amounts)
}
// TstCreateInputsOnBlock creates a number of inputs by creating a transaction
// with a number of outputs corresponding to the elements of the amounts slice.
//
// The transaction is added to a block and the index and blockheight must be
// specified.
func TstCreateInputsOnBlock(t *testing.T, s *txstore.Store,
blockTxIndex int, pkScript []byte, amounts []int64) []txstore.Credit {
msgTx := createMsgTx(pkScript, amounts)
block := &txstore.Block{
Height: TstInputsBlock,
}
tx := btcutil.NewTx(msgTx)
tx.SetIndex(blockTxIndex)
r, err := s.InsertTx(tx, block)
if err != nil {
t.Fatal("Failed to create inputs: ", err)
}
credits := make([]txstore.Credit, len(msgTx.TxOut))
for i := range msgTx.TxOut {
credit, err := r.AddCredit(uint32(i), false)
if err != nil {
t.Fatal("Failed to create inputs: ", err)
}
credits[i] = credit
}
return credits
}
// TstCreatePool creates a Pool on a fresh walletdb and returns it. It also
// returns the pool's waddrmgr.Manager (which uses the same walletdb, but with a
// different namespace) as a convenience, and a teardown function that closes
// the Manager and removes the directory used to store the database.
func TstCreatePool(t *testing.T) (tearDownFunc func(), mgr *waddrmgr.Manager, pool *Pool) {
// This should be moved somewhere else eventually as not all of our tests
// call this function, but right now the only option would be to have the
// t.Parallel() call in each of our tests.
t.Parallel()
// Create a new wallet DB and addr manager.
dir, err := ioutil.TempDir("", "pool_test")
if err != nil {
t.Fatalf("Failed to create db dir: %v", err)
}
db, err := walletdb.Create("bdb", filepath.Join(dir, "wallet.db"))
if err != nil {
t.Fatalf("Failed to create wallet DB: %v", err)
}
mgrNamespace, err := db.Namespace([]byte("waddrmgr"))
if err != nil {
t.Fatalf("Failed to create addr manager DB namespace: %v", err)
}
var fastScrypt = &waddrmgr.Options{ScryptN: 16, ScryptR: 8, ScryptP: 1}
mgr, err = waddrmgr.Create(mgrNamespace, seed, pubPassphrase, privPassphrase,
&chaincfg.MainNetParams, fastScrypt)
if err != nil {
t.Fatalf("Failed to create addr manager: %v", err)
}
// Create a walletdb for votingpools.
vpNamespace, err := db.Namespace([]byte("votingpool"))
if err != nil {
t.Fatalf("Failed to create VotingPool DB namespace: %v", err)
}
pool, err = Create(vpNamespace, mgr, []byte{0x00})
if err != nil {
t.Fatalf("Voting Pool creation failed: %v", err)
}
tearDownFunc = func() {
db.Close()
mgr.Close()
os.RemoveAll(dir)
}
return tearDownFunc, mgr, pool
}
func TstNewOutputRequest(t *testing.T, transaction uint32, address string, amount btcutil.Amount,
net *chaincfg.Params) OutputRequest {
addr, err := btcutil.DecodeAddress(address, net)
if err != nil {
t.Fatalf("Unable to decode address %s", address)
}
pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
t.Fatalf("Unable to generate pkScript for %v", addr)
}
return OutputRequest{
PkScript: pkScript,
Address: addr,
Amount: amount,
Server: "server",
Transaction: transaction,
}
}
func TstNewWithdrawalOutput(r OutputRequest, status outputStatus,
outpoints []OutBailmentOutpoint) *WithdrawalOutput {
output := &WithdrawalOutput{
request: r,
status: status,
outpoints: outpoints,
}
return output
}
func TstNewWithdrawalAddress(t *testing.T, p *Pool, seriesID uint32, branch Branch,
index Index) (addr *WithdrawalAddress) {
TstEnsureUsedAddr(t, p, seriesID, branch, index)
var err error
TstRunWithManagerUnlocked(t, p.Manager(), func() {
addr, err = p.WithdrawalAddress(seriesID, branch, index)
})
if err != nil {
t.Fatalf("Failed to get WithdrawalAddress: %v", err)
}
return addr
}
func TstNewChangeAddress(t *testing.T, p *Pool, seriesID uint32, idx Index) (addr *ChangeAddress) {
addr, err := p.ChangeAddress(seriesID, idx)
if err != nil {
t.Fatalf("Failed to get ChangeAddress: %v", err)
}
return addr
}
func TstConstantFee(fee btcutil.Amount) func(tx *withdrawalTx) btcutil.Amount {
return func(tx *withdrawalTx) btcutil.Amount { return fee }
}

View file

@ -0,0 +1,287 @@
/*
* Copyright (c) 2015 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool
import (
"bytes"
"fmt"
"sort"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/txstore"
)
const eligibleInputMinConfirmations = 100
// Credit is an abstraction over txstore.Credit used in the construction of
// voting pool withdrawal transactions.
type Credit interface {
TxSha() *wire.ShaHash
OutputIndex() uint32
Address() WithdrawalAddress
Amount() btcutil.Amount
OutPoint() *wire.OutPoint
TxOut() *wire.TxOut
}
// credit implements the Credit interface.
type credit struct {
txstore.Credit
addr WithdrawalAddress
}
// newCredit initialises a new credit.
func newCredit(c txstore.Credit, addr WithdrawalAddress) *credit {
return &credit{Credit: c, addr: addr}
}
func (c *credit) String() string {
return fmt.Sprintf("credit of %v to %v", c.Amount(), c.Address())
}
// TxSha returns the sha hash of the underlying transaction.
func (c *credit) TxSha() *wire.ShaHash {
return c.Credit.TxRecord.Tx().Sha()
}
// OutputIndex returns the outputindex of the ouput in the underlying
// transaction.
func (c *credit) OutputIndex() uint32 {
return c.Credit.OutputIndex
}
// Address returns the voting pool address.
func (c *credit) Address() WithdrawalAddress {
return c.addr
}
// Compile time check that credit implements Credit interface.
var _ Credit = (*credit)(nil)
// byAddress defines the methods needed to satisify sort.Interface to sort a
// slice of Credits by their address.
type byAddress []Credit
func (c byAddress) Len() int { return len(c) }
func (c byAddress) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
// Less returns true if the element at positions i is smaller than the
// element at position j. The 'smaller-than' relation is defined to be
// the lexicographic ordering defined on the tuple (SeriesID, Index,
// Branch, TxSha, OutputIndex).
func (c byAddress) Less(i, j int) bool {
iAddr := c[i].Address()
jAddr := c[j].Address()
if iAddr.seriesID < jAddr.seriesID {
return true
}
if iAddr.seriesID > jAddr.seriesID {
return false
}
// The seriesID are equal, so compare index.
if iAddr.index < jAddr.index {
return true
}
if iAddr.index > jAddr.index {
return false
}
// The seriesID and index are equal, so compare branch.
if iAddr.branch < jAddr.branch {
return true
}
if iAddr.branch > jAddr.branch {
return false
}
// The seriesID, index, and branch are equal, so compare hash.
txidComparison := bytes.Compare(c[i].TxSha().Bytes(), c[j].TxSha().Bytes())
if txidComparison < 0 {
return true
}
if txidComparison > 0 {
return false
}
// The seriesID, index, branch, and hash are equal, so compare output
// index.
return c[i].OutputIndex() < c[j].OutputIndex()
}
// getEligibleInputs returns eligible inputs with addresses between startAddress
// and the last used address of lastSeriesID.
func (p *Pool) getEligibleInputs(store *txstore.Store, startAddress WithdrawalAddress,
lastSeriesID uint32, dustThreshold btcutil.Amount, chainHeight int32,
minConf int) ([]Credit, error) {
if p.Series(lastSeriesID) == nil {
str := fmt.Sprintf("lastSeriesID (%d) does not exist", lastSeriesID)
return nil, newError(ErrSeriesNotExists, str, nil)
}
unspents, err := store.UnspentOutputs()
if err != nil {
return nil, newError(ErrInputSelection, "failed to get unspent outputs", err)
}
addrMap, err := groupCreditsByAddr(unspents, p.manager.ChainParams())
if err != nil {
return nil, err
}
var inputs []Credit
address := startAddress
for {
log.Debugf("Looking for eligible inputs at address %v", address.addrIdentifier())
if candidates, ok := addrMap[address.addr.EncodeAddress()]; ok {
var eligibles []Credit
for _, c := range candidates {
if p.isCreditEligible(c, minConf, chainHeight, dustThreshold) {
eligibles = append(eligibles, newCredit(c, address))
}
}
// Make sure the eligibles are correctly sorted.
sort.Sort(byAddress(eligibles))
inputs = append(inputs, eligibles...)
}
nAddr, err := nextAddr(p, address.seriesID, address.branch, address.index, lastSeriesID+1)
if err != nil {
return nil, newError(ErrInputSelection, "failed to get next withdrawal address", err)
} else if nAddr == nil {
log.Debugf("getEligibleInputs: reached last addr, stopping")
break
}
address = *nAddr
}
return inputs, nil
}
// nextAddr returns the next WithdrawalAddress according to the input selection
// rules: http://opentransactions.org/wiki/index.php/Input_Selection_Algorithm_(voting_pools)
// It returns nil if the new address' seriesID is >= stopSeriesID.
func nextAddr(p *Pool, seriesID uint32, branch Branch, index Index, stopSeriesID uint32) (
*WithdrawalAddress, error) {
series := p.Series(seriesID)
if series == nil {
return nil, newError(ErrSeriesNotExists, fmt.Sprintf("unknown seriesID: %d", seriesID), nil)
}
branch++
if int(branch) > len(series.publicKeys) {
highestIdx, err := p.highestUsedSeriesIndex(seriesID)
if err != nil {
return nil, err
}
if index > highestIdx {
seriesID++
log.Debugf("nextAddr(): reached last branch (%d) and highest used index (%d), "+
"moving on to next series (%d)", branch, index, seriesID)
index = 0
} else {
index++
}
branch = 0
}
if seriesID >= stopSeriesID {
return nil, nil
}
addr, err := p.WithdrawalAddress(seriesID, branch, index)
if err != nil && err.(Error).ErrorCode == ErrWithdrawFromUnusedAddr {
// The used indices will vary between branches so sometimes we'll try to
// get a WithdrawalAddress that hasn't been used before, and in such
// cases we just need to move on to the next one.
log.Debugf("nextAddr(): skipping addr (series #%d, branch #%d, index #%d) as it hasn't "+
"been used before", seriesID, branch, index)
return nextAddr(p, seriesID, branch, index, stopSeriesID)
}
return addr, err
}
// highestUsedSeriesIndex returns the highest index among all of this Pool's
// used addresses for the given seriesID. It returns 0 if there are no used
// addresses with the given seriesID.
func (p *Pool) highestUsedSeriesIndex(seriesID uint32) (Index, error) {
maxIdx := Index(0)
series := p.Series(seriesID)
if series == nil {
return maxIdx,
newError(ErrSeriesNotExists, fmt.Sprintf("unknown seriesID: %d", seriesID), nil)
}
for i := range series.publicKeys {
idx, err := p.highestUsedIndexFor(seriesID, Branch(i))
if err != nil {
return Index(0), err
}
if idx > maxIdx {
maxIdx = idx
}
}
return maxIdx, nil
}
// groupCreditsByAddr converts a slice of credits to a map from the string
// representation of an encoded address to the unspent outputs associated with
// that address.
func groupCreditsByAddr(credits []txstore.Credit, chainParams *chaincfg.Params) (
map[string][]txstore.Credit, error) {
addrMap := make(map[string][]txstore.Credit)
for _, c := range credits {
_, addrs, _, err := c.Addresses(chainParams)
if err != nil {
return nil, newError(ErrInputSelection, "failed to obtain input address", err)
}
// As our credits are all P2SH we should never have more than one
// address per credit, so let's error out if that assumption is
// violated.
if len(addrs) != 1 {
return nil, newError(ErrInputSelection, "input doesn't have exactly one address", nil)
}
encAddr := addrs[0].EncodeAddress()
if v, ok := addrMap[encAddr]; ok {
addrMap[encAddr] = append(v, c)
} else {
addrMap[encAddr] = []txstore.Credit{c}
}
}
return addrMap, nil
}
// isCreditEligible tests a given credit for eligibilty with respect
// to number of confirmations, the dust threshold and that it is not
// the charter output.
func (p *Pool) isCreditEligible(c txstore.Credit, minConf int, chainHeight int32,
dustThreshold btcutil.Amount) bool {
if c.Amount() < dustThreshold {
return false
}
if !c.Confirmed(minConf, chainHeight) {
return false
}
if p.isCharterOutput(c) {
return false
}
return true
}
// isCharterOutput - TODO: In order to determine this, we need the txid
// and the output index of the current charter output, which we don't have yet.
func (p *Pool) isCharterOutput(c txstore.Credit) bool {
return false
}

View file

@ -0,0 +1,410 @@
/*
* Copyright (c) 2015 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool
import (
"bytes"
"reflect"
"sort"
"testing"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/txstore"
)
var (
// random small number of satoshis used as dustThreshold
dustThreshold btcutil.Amount = 1e4
)
func TestGetEligibleInputs(t *testing.T) {
tearDown, pool, store := TstCreatePoolAndTxStore(t)
defer tearDown()
series := []TstSeriesDef{
{ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1},
{ReqSigs: 2, PubKeys: TstPubKeys[3:6], SeriesID: 2},
}
TstCreateSeries(t, pool, series)
scripts := append(
getPKScriptsForAddressRange(t, pool, 1, 0, 2, 0, 4),
getPKScriptsForAddressRange(t, pool, 2, 0, 2, 0, 6)...)
// Create two eligible inputs locked to each of the PKScripts above.
expNoEligibleInputs := 2 * len(scripts)
eligibleAmounts := []int64{int64(dustThreshold + 1), int64(dustThreshold + 1)}
var inputs []txstore.Credit
for i := 0; i < len(scripts); i++ {
txIndex := int(i) + 1
created := TstCreateInputsOnBlock(t, store, txIndex, scripts[i], eligibleAmounts)
inputs = append(inputs, created...)
}
startAddr := TstNewWithdrawalAddress(t, pool, 1, 0, 0)
lastSeriesID := uint32(2)
currentBlock := int32(TstInputsBlock + eligibleInputMinConfirmations + 1)
var eligibles []Credit
var err error
TstRunWithManagerUnlocked(t, pool.Manager(), func() {
eligibles, err = pool.getEligibleInputs(
store, *startAddr, lastSeriesID, dustThreshold, int32(currentBlock),
eligibleInputMinConfirmations)
})
if err != nil {
t.Fatal("InputSelection failed:", err)
}
// Check we got the expected number of eligible inputs.
if len(eligibles) != expNoEligibleInputs {
t.Fatalf("Wrong number of eligible inputs returned. Got: %d, want: %d.",
len(eligibles), expNoEligibleInputs)
}
// Check that the returned eligibles are sorted by address.
if !sort.IsSorted(byAddress(eligibles)) {
t.Fatal("Eligible inputs are not sorted.")
}
// Check that all credits are unique
checkUniqueness(t, eligibles)
}
func TestNextAddrWithVaryingHighestIndices(t *testing.T) {
tearDown, mgr, pool := TstCreatePool(t)
defer tearDown()
series := []TstSeriesDef{
{ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1},
}
TstCreateSeries(t, pool, series)
stopSeriesID := uint32(2)
// Populate the used addr DB for branch 0 and indices ranging from 0 to 2.
TstEnsureUsedAddr(t, pool, 1, Branch(0), 2)
// Populate the used addr DB for branch 1 and indices ranging from 0 to 1.
TstEnsureUsedAddr(t, pool, 1, Branch(1), 1)
// Start with the address for branch==0, index==1.
addr := TstNewWithdrawalAddress(t, pool, 1, 0, 1)
var err error
// The first call to nextAddr() should give us the address for branch==1
// and index==1.
TstRunWithManagerUnlocked(t, mgr, func() {
addr, err = nextAddr(pool, addr.seriesID, addr.branch, addr.index, stopSeriesID)
})
if err != nil {
t.Fatalf("Failed to get next address: %v", err)
}
checkWithdrawalAddressMatches(t, addr, 1, Branch(1), 1)
// The next call should give us the address for branch==0, index==2 since
// there are no used addresses for branch==2.
TstRunWithManagerUnlocked(t, mgr, func() {
addr, err = nextAddr(pool, addr.seriesID, addr.branch, addr.index, stopSeriesID)
})
if err != nil {
t.Fatalf("Failed to get next address: %v", err)
}
checkWithdrawalAddressMatches(t, addr, 1, Branch(0), 2)
// Since the last addr for branch==1 was the one with index==1, a subsequent
// call will return nil.
TstRunWithManagerUnlocked(t, mgr, func() {
addr, err = nextAddr(pool, addr.seriesID, addr.branch, addr.index, stopSeriesID)
})
if err != nil {
t.Fatalf("Failed to get next address: %v", err)
}
if addr != nil {
t.Fatalf("Wrong next addr; got '%s', want 'nil'", addr.addrIdentifier())
}
}
func TestNextAddr(t *testing.T) {
tearDown, mgr, pool := TstCreatePool(t)
defer tearDown()
series := []TstSeriesDef{
{ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1},
{ReqSigs: 2, PubKeys: TstPubKeys[3:6], SeriesID: 2},
}
TstCreateSeries(t, pool, series)
stopSeriesID := uint32(3)
lastIdx := Index(10)
// Populate used addresses DB with entries for seriesID==1, branch==0..3,
// idx==0..10.
for _, i := range []int{0, 1, 2, 3} {
TstEnsureUsedAddr(t, pool, 1, Branch(i), lastIdx)
}
addr := TstNewWithdrawalAddress(t, pool, 1, 0, lastIdx-1)
var err error
// nextAddr() first increments just the branch, which ranges from 0 to 3
// here (because our series has 3 public keys).
for _, i := range []int{1, 2, 3} {
TstRunWithManagerUnlocked(t, mgr, func() {
addr, err = nextAddr(pool, addr.seriesID, addr.branch, addr.index, stopSeriesID)
})
if err != nil {
t.Fatalf("Failed to get next address: %v", err)
}
checkWithdrawalAddressMatches(t, addr, 1, Branch(i), lastIdx-1)
}
// The last nextAddr() above gave us the addr with branch=3,
// idx=lastIdx-1, so the next 4 calls should give us the addresses with
// branch=[0-3] and idx=lastIdx.
for _, i := range []int{0, 1, 2, 3} {
TstRunWithManagerUnlocked(t, mgr, func() {
addr, err = nextAddr(pool, addr.seriesID, addr.branch, addr.index, stopSeriesID)
})
if err != nil {
t.Fatalf("Failed to get next address: %v", err)
}
checkWithdrawalAddressMatches(t, addr, 1, Branch(i), lastIdx)
}
// Populate used addresses DB with entries for seriesID==2, branch==0..3,
// idx==0..10.
for _, i := range []int{0, 1, 2, 3} {
TstEnsureUsedAddr(t, pool, 2, Branch(i), lastIdx)
}
// Now we've gone through all the available branch/idx combinations, so
// we should move to the next series and start again with branch=0, idx=0.
for _, i := range []int{0, 1, 2, 3} {
TstRunWithManagerUnlocked(t, mgr, func() {
addr, err = nextAddr(pool, addr.seriesID, addr.branch, addr.index, stopSeriesID)
})
if err != nil {
t.Fatalf("Failed to get next address: %v", err)
}
checkWithdrawalAddressMatches(t, addr, 2, Branch(i), 0)
}
// Finally check that nextAddr() returns nil when we've reached the last
// available address before stopSeriesID.
addr = TstNewWithdrawalAddress(t, pool, 2, 3, lastIdx)
TstRunWithManagerUnlocked(t, mgr, func() {
addr, err = nextAddr(pool, addr.seriesID, addr.branch, addr.index, stopSeriesID)
})
if err != nil {
t.Fatalf("Failed to get next address: %v", err)
}
if addr != nil {
t.Fatalf("Wrong WithdrawalAddress; got %s, want nil", addr.addrIdentifier())
}
}
func TestEligibleInputsAreEligible(t *testing.T) {
tearDown, pool, store := TstCreatePoolAndTxStore(t)
defer tearDown()
seriesID := uint32(1)
branch := Branch(0)
index := Index(0)
// create the series
series := []TstSeriesDef{{ReqSigs: 3, PubKeys: TstPubKeys[1:6], SeriesID: seriesID}}
TstCreateSeries(t, pool, series)
// Create the input.
pkScript := TstCreatePkScript(t, pool, seriesID, branch, index)
var chainHeight int32 = 1000
c := TstCreateInputs(t, store, pkScript, []int64{int64(dustThreshold)})[0]
// Make sure credits is old enough to pass the minConf check.
c.BlockHeight = int32(eligibleInputMinConfirmations)
if !pool.isCreditEligible(c, eligibleInputMinConfirmations, chainHeight, dustThreshold) {
t.Errorf("Input is not eligible and it should be.")
}
}
func TestNonEligibleInputsAreNotEligible(t *testing.T) {
tearDown, pool, store1 := TstCreatePoolAndTxStore(t)
store2, storeTearDown2 := TstCreateTxStore(t)
defer tearDown()
defer storeTearDown2()
seriesID := uint32(1)
branch := Branch(0)
index := Index(0)
// create the series
series := []TstSeriesDef{{ReqSigs: 3, PubKeys: TstPubKeys[1:6], SeriesID: seriesID}}
TstCreateSeries(t, pool, series)
pkScript := TstCreatePkScript(t, pool, seriesID, branch, index)
var chainHeight int32 = 1000
// Check that credit below dustThreshold is rejected.
c1 := TstCreateInputs(t, store1, pkScript, []int64{int64(dustThreshold - 1)})[0]
c1.BlockHeight = int32(100) // make sure it has enough confirmations.
if pool.isCreditEligible(c1, eligibleInputMinConfirmations, chainHeight, dustThreshold) {
t.Errorf("Input is eligible and it should not be.")
}
// Check that a credit with not enough confirmations is rejected.
c2 := TstCreateInputs(t, store2, pkScript, []int64{int64(dustThreshold)})[0]
// the calculation of if it has been confirmed does this:
// chainheigt - bh + 1 >= target, which is quite weird, but the
// reason why I need to put 902 as *that* makes 1000 - 902 +1 = 99 >=
// 100 false
c2.BlockHeight = int32(902)
if pool.isCreditEligible(c2, eligibleInputMinConfirmations, chainHeight, dustThreshold) {
t.Errorf("Input is eligible and it should not be.")
}
}
func TestCreditSortingByAddress(t *testing.T) {
teardown, _, pool := TstCreatePool(t)
defer teardown()
series := []TstSeriesDef{
{ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1},
{ReqSigs: 2, PubKeys: TstPubKeys[3:6], SeriesID: 2},
}
TstCreateSeries(t, pool, series)
shaHash0 := bytes.Repeat([]byte{0}, 32)
shaHash1 := bytes.Repeat([]byte{1}, 32)
shaHash2 := bytes.Repeat([]byte{2}, 32)
c0 := TstNewFakeCredit(t, pool, 1, 0, 0, shaHash0, 0)
c1 := TstNewFakeCredit(t, pool, 1, 0, 0, shaHash0, 1)
c2 := TstNewFakeCredit(t, pool, 1, 0, 0, shaHash1, 0)
c3 := TstNewFakeCredit(t, pool, 1, 0, 0, shaHash2, 0)
c4 := TstNewFakeCredit(t, pool, 1, 0, 1, shaHash0, 0)
c5 := TstNewFakeCredit(t, pool, 1, 1, 0, shaHash0, 0)
c6 := TstNewFakeCredit(t, pool, 2, 0, 0, shaHash0, 0)
randomCredits := [][]Credit{
[]Credit{c6, c5, c4, c3, c2, c1, c0},
[]Credit{c2, c1, c0, c6, c5, c4, c3},
[]Credit{c6, c4, c5, c2, c3, c0, c1},
}
want := []Credit{c0, c1, c2, c3, c4, c5, c6}
for _, random := range randomCredits {
sort.Sort(byAddress(random))
got := random
if len(got) != len(want) {
t.Fatalf("Sorted credit slice size wrong: Got: %d, want: %d",
len(got), len(want))
}
for idx := 0; idx < len(want); idx++ {
if !reflect.DeepEqual(got[idx], want[idx]) {
t.Errorf("Wrong output index. Got: %v, want: %v",
got[idx], want[idx])
}
}
}
}
// TstFakeCredit is a structure implementing the Credit interface used to test
// the byAddress sorting. It exists because to test the sorting properly we need
// to be able to set the Credit's TxSha and OutputIndex.
type TstFakeCredit struct {
addr WithdrawalAddress
txSha *wire.ShaHash
outputIndex uint32
amount btcutil.Amount
}
func (c *TstFakeCredit) String() string { return "" }
func (c *TstFakeCredit) TxSha() *wire.ShaHash { return c.txSha }
func (c *TstFakeCredit) OutputIndex() uint32 { return c.outputIndex }
func (c *TstFakeCredit) Address() WithdrawalAddress { return c.addr }
func (c *TstFakeCredit) Amount() btcutil.Amount { return c.amount }
func (c *TstFakeCredit) TxOut() *wire.TxOut { return nil }
func (c *TstFakeCredit) OutPoint() *wire.OutPoint {
return &wire.OutPoint{Hash: *c.txSha, Index: c.outputIndex}
}
func TstNewFakeCredit(t *testing.T, pool *Pool, series uint32, index Index, branch Branch,
txSha []byte, outputIdx int) *TstFakeCredit {
var hash wire.ShaHash
if err := hash.SetBytes(txSha); err != nil {
t.Fatal(err)
}
// Ensure the address defined by the given series/branch/index is present on
// the set of used addresses as that's a requirement of WithdrawalAddress.
TstEnsureUsedAddr(t, pool, series, branch, index)
addr := TstNewWithdrawalAddress(t, pool, series, branch, index)
return &TstFakeCredit{
addr: *addr,
txSha: &hash,
outputIndex: uint32(outputIdx),
}
}
// Compile time check that TstFakeCredit implements the
// Credit interface.
var _ Credit = (*TstFakeCredit)(nil)
func checkUniqueness(t *testing.T, credits byAddress) {
type uniq struct {
series uint32
branch Branch
index Index
hash wire.ShaHash
outputIndex uint32
}
uniqMap := make(map[uniq]bool)
for _, c := range credits {
u := uniq{
series: c.Address().SeriesID(),
branch: c.Address().Branch(),
index: c.Address().Index(),
hash: *c.TxSha(),
outputIndex: c.OutputIndex(),
}
if _, exists := uniqMap[u]; exists {
t.Fatalf("Duplicate found: %v", u)
} else {
uniqMap[u] = true
}
}
}
func getPKScriptsForAddressRange(t *testing.T, pool *Pool, seriesID uint32,
startBranch, stopBranch Branch, startIdx, stopIdx Index) [][]byte {
var pkScripts [][]byte
for idx := startIdx; idx <= stopIdx; idx++ {
for branch := startBranch; branch <= stopBranch; branch++ {
pkScripts = append(pkScripts, TstCreatePkScript(t, pool, seriesID, branch, idx))
}
}
return pkScripts
}
func checkWithdrawalAddressMatches(t *testing.T, addr *WithdrawalAddress, seriesID uint32,
branch Branch, index Index) {
if addr.SeriesID() != seriesID {
t.Fatalf("Wrong seriesID; got %d, want %d", addr.SeriesID(), seriesID)
}
if addr.Branch() != branch {
t.Fatalf("Wrong branch; got %d, want %d", addr.Branch(), branch)
}
if addr.Index() != index {
t.Fatalf("Wrong index; got %d, want %d", addr.Index(), index)
}
}

View file

@ -17,11 +17,16 @@
package votingpool
import (
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
)
var TstLastErr = lastErr
const TstEligibleInputMinConfirmations = eligibleInputMinConfirmations
// TstPutSeries transparently wraps the voting pool putSeries method.
func (vp *Pool) TstPutSeries(version, seriesID, reqSigs uint32, inRawPubKeys []string) error {
return vp.putSeries(version, seriesID, reqSigs, inRawPubKeys)
@ -31,7 +36,24 @@ var TstBranchOrder = branchOrder
// TstExistsSeries checks whether a series is stored in the database.
func (vp *Pool) TstExistsSeries(seriesID uint32) (bool, error) {
return vp.existsSeries(seriesID)
var exists bool
err := vp.namespace.View(
func(tx walletdb.Tx) error {
poolBucket := tx.RootBucket().Bucket(vp.ID)
if poolBucket == nil {
return nil
}
bucket := poolBucket.Bucket(seriesBucketName)
if bucket == nil {
return nil
}
exists = bucket.Get(uint32ToBytes(seriesID)) != nil
return nil
})
if err != nil {
return false, err
}
return exists, nil
}
// TstNamespace exposes the Pool's namespace as it's needed in some tests.
@ -74,44 +96,7 @@ func (vp *Pool) TstDecryptExtendedKey(keyType waddrmgr.CryptoKeyType, encrypted
return vp.decryptExtendedKey(keyType, encrypted)
}
// SeriesRow mimics dbSeriesRow defined in db.go .
type SeriesRow struct {
Version uint32
Active bool
ReqSigs uint32
PubKeysEncrypted [][]byte
PrivKeysEncrypted [][]byte
// TstGetMsgTx returns the withdrawal transaction with the given ntxid.
func (s *WithdrawalStatus) TstGetMsgTx(ntxid Ntxid) *wire.MsgTx {
return s.transactions[ntxid].MsgTx
}
// SerializeSeries wraps serializeSeriesRow by passing it a freshly-built
// dbSeriesRow.
func SerializeSeries(version uint32, active bool, reqSigs uint32, pubKeys, privKeys [][]byte) ([]byte, error) {
row := &dbSeriesRow{
version: version,
active: active,
reqSigs: reqSigs,
pubKeysEncrypted: pubKeys,
privKeysEncrypted: privKeys,
}
return serializeSeriesRow(row)
}
// DeserializeSeries wraps deserializeSeriesRow and returns a freshly-built
// SeriesRow.
func DeserializeSeries(serializedSeries []byte) (*SeriesRow, error) {
row, err := deserializeSeriesRow(serializedSeries)
if err != nil {
return nil, err
}
return &SeriesRow{
Version: row.version,
Active: row.active,
ReqSigs: row.reqSigs,
PubKeysEncrypted: row.pubKeysEncrypted,
PrivKeysEncrypted: row.privKeysEncrypted,
}, nil
}
var TstValidateAndDecryptKeys = validateAndDecryptKeys

42
votingpool/log.go Normal file
View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2015 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool
import "github.com/btcsuite/btclog"
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var log btclog.Logger
// The default amount of logging is none.
func init() {
DisableLog()
}
// DisableLog disables all library log output. Logging output is disabled
// by default until either UseLogger or SetLogWriter are called.
func DisableLog() {
log = btclog.Disabled
}
// UseLogger uses a specified Logger to output package logging info.
// This should be used in preference to SetLogWriter if the caller is also
// using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}

View file

@ -30,8 +30,16 @@ import (
const (
minSeriesPubKeys = 3
// CurrentVersion is the version used for newly created Series.
CurrentVersion = 1
)
// Branch is the type used to represent a branch number in a series.
type Branch uint32
// Index is the type used to represent an index number in a series.
type Index uint32
// SeriesData represents a Series for a given Pool.
type SeriesData struct {
version uint32
@ -55,6 +63,36 @@ type Pool struct {
namespace walletdb.Namespace
}
// PoolAddress represents a voting pool P2SH address, generated by
// deriving public HD keys from the series' master keys using the given
// branch/index and constructing a M-of-N multi-sig script.
type PoolAddress interface {
SeriesID() uint32
Branch() Branch
Index() Index
}
type poolAddress struct {
pool *Pool
addr btcutil.Address
script []byte
seriesID uint32
branch Branch
index Index
}
// ChangeAddress is a votingpool address meant to be used on transaction change
// outputs. All change addresses have branch==0.
type ChangeAddress struct {
*poolAddress
}
// WithdrawalAddress is a votingpool address that may contain unspent outputs
// available for use in a withdrawal.
type WithdrawalAddress struct {
*poolAddress
}
// Create creates a new entry in the database with the given ID
// and returns the Pool representing it.
func Create(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID []byte) (*Pool, error) {
@ -64,7 +102,7 @@ func Create(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID []byte) (*
})
if err != nil {
str := fmt.Sprintf("unable to add voting pool %v to db", poolID)
return nil, managerError(waddrmgr.ErrVotingPoolAlreadyExists, str, err)
return nil, newError(ErrPoolAlreadyExists, str, err)
}
return newPool(namespace, m, poolID), nil
}
@ -76,18 +114,18 @@ func Load(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID []byte) (*Po
func(tx walletdb.Tx) error {
if exists := existsPool(tx, poolID); !exists {
str := fmt.Sprintf("unable to find voting pool %v in db", poolID)
return managerError(waddrmgr.ErrVotingPoolNotExists, str, nil)
return newError(ErrPoolNotExists, str, nil)
}
return nil
})
if err != nil {
return nil, err
}
vp := newPool(namespace, m, poolID)
if err = vp.LoadAllSeries(); err != nil {
p := newPool(namespace, m, poolID)
if err = p.LoadAllSeries(); err != nil {
return nil, err
}
return vp, nil
return p, nil
}
// newPool creates a new Pool instance.
@ -102,13 +140,13 @@ func newPool(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID []byte) *
// LoadAndGetDepositScript generates and returns a deposit script for the given seriesID,
// branch and index of the Pool identified by poolID.
func LoadAndGetDepositScript(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID string, seriesID, branch, index uint32) ([]byte, error) {
func LoadAndGetDepositScript(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID string, seriesID uint32, branch Branch, index Index) ([]byte, error) {
pid := []byte(poolID)
vp, err := Load(namespace, m, pid)
p, err := Load(namespace, m, pid)
if err != nil {
return nil, err
}
script, err := vp.DepositScript(seriesID, branch, index)
script, err := p.DepositScript(seriesID, branch, index)
if err != nil {
return nil, err
}
@ -121,11 +159,11 @@ func LoadAndGetDepositScript(namespace walletdb.Namespace, m *waddrmgr.Manager,
func LoadAndCreateSeries(namespace walletdb.Namespace, m *waddrmgr.Manager, version uint32,
poolID string, seriesID, reqSigs uint32, rawPubKeys []string) error {
pid := []byte(poolID)
vp, err := Load(namespace, m, pid)
p, err := Load(namespace, m, pid)
if err != nil {
managerErr := err.(waddrmgr.ManagerError)
if managerErr.ErrorCode == waddrmgr.ErrVotingPoolNotExists {
vp, err = Create(namespace, m, pid)
vpErr := err.(Error)
if vpErr.ErrorCode == ErrPoolNotExists {
p, err = Create(namespace, m, pid)
if err != nil {
return err
}
@ -133,7 +171,7 @@ func LoadAndCreateSeries(namespace walletdb.Namespace, m *waddrmgr.Manager, vers
return err
}
}
return vp.CreateSeries(version, seriesID, reqSigs, rawPubKeys)
return p.CreateSeries(version, seriesID, reqSigs, rawPubKeys)
}
// LoadAndReplaceSeries loads the voting pool with the given ID and calls ReplaceSeries,
@ -141,11 +179,11 @@ func LoadAndCreateSeries(namespace walletdb.Namespace, m *waddrmgr.Manager, vers
func LoadAndReplaceSeries(namespace walletdb.Namespace, m *waddrmgr.Manager, version uint32,
poolID string, seriesID, reqSigs uint32, rawPubKeys []string) error {
pid := []byte(poolID)
vp, err := Load(namespace, m, pid)
p, err := Load(namespace, m, pid)
if err != nil {
return err
}
return vp.ReplaceSeries(version, seriesID, reqSigs, rawPubKeys)
return p.ReplaceSeries(version, seriesID, reqSigs, rawPubKeys)
}
// LoadAndEmpowerSeries loads the voting pool with the given ID and calls EmpowerSeries,
@ -160,27 +198,34 @@ func LoadAndEmpowerSeries(namespace walletdb.Namespace, m *waddrmgr.Manager,
return pool.EmpowerSeries(seriesID, rawPrivKey)
}
// GetSeries returns the series with the given ID, or nil if it doesn't
// Series returns the series with the given ID, or nil if it doesn't
// exist.
func (vp *Pool) GetSeries(seriesID uint32) *SeriesData {
series, exists := vp.seriesLookup[seriesID]
func (p *Pool) Series(seriesID uint32) *SeriesData {
series, exists := p.seriesLookup[seriesID]
if !exists {
return nil
}
return series
}
// Manager returns the waddrmgr.Manager used by this Pool.
func (p *Pool) Manager() *waddrmgr.Manager {
return p.manager
}
// saveSeriesToDisk stores the given series ID and data in the database,
// first encrypting the public/private extended keys.
func (vp *Pool) saveSeriesToDisk(seriesID uint32, data *SeriesData) error {
//
// This method must be called with the Pool's manager unlocked.
func (p *Pool) saveSeriesToDisk(seriesID uint32, data *SeriesData) error {
var err error
encryptedPubKeys := make([][]byte, len(data.publicKeys))
for i, pubKey := range data.publicKeys {
encryptedPubKeys[i], err = vp.manager.Encrypt(
encryptedPubKeys[i], err = p.manager.Encrypt(
waddrmgr.CKTPublic, []byte(pubKey.String()))
if err != nil {
str := fmt.Sprintf("key %v failed encryption", pubKey)
return managerError(waddrmgr.ErrCrypto, str, err)
return newError(ErrCrypto, str, err)
}
}
encryptedPrivKeys := make([][]byte, len(data.privateKeys))
@ -188,22 +233,22 @@ func (vp *Pool) saveSeriesToDisk(seriesID uint32, data *SeriesData) error {
if privKey == nil {
encryptedPrivKeys[i] = nil
} else {
encryptedPrivKeys[i], err = vp.manager.Encrypt(
encryptedPrivKeys[i], err = p.manager.Encrypt(
waddrmgr.CKTPrivate, []byte(privKey.String()))
}
if err != nil {
str := fmt.Sprintf("key %v failed encryption", privKey)
return managerError(waddrmgr.ErrCrypto, str, err)
return newError(ErrCrypto, str, err)
}
}
err = vp.namespace.Update(func(tx walletdb.Tx) error {
return putSeries(tx, vp.ID, data.version, seriesID, data.active,
err = p.namespace.Update(func(tx walletdb.Tx) error {
return putSeries(tx, p.ID, data.version, seriesID, data.active,
data.reqSigs, encryptedPubKeys, encryptedPrivKeys)
})
if err != nil {
str := fmt.Sprintf("cannot put series #%d into db", seriesID)
return managerError(waddrmgr.ErrSeriesStorage, str, err)
return newError(ErrSeriesSerialization, str, err)
}
return nil
}
@ -226,19 +271,19 @@ func convertAndValidatePubKeys(rawPubKeys []string) ([]*hdkeychain.ExtendedKey,
for i, rawPubKey := range rawPubKeys {
if _, seen := seenKeys[rawPubKey]; seen {
str := fmt.Sprintf("duplicated public key: %v", rawPubKey)
return nil, managerError(waddrmgr.ErrKeyDuplicate, str, nil)
return nil, newError(ErrKeyDuplicate, str, nil)
}
seenKeys[rawPubKey] = true
key, err := hdkeychain.NewKeyFromString(rawPubKey)
if err != nil {
str := fmt.Sprintf("invalid extended public key %v", rawPubKey)
return nil, managerError(waddrmgr.ErrKeyChain, str, err)
return nil, newError(ErrKeyChain, str, err)
}
if key.IsPrivate() {
str := fmt.Sprintf("private keys not accepted: %v", rawPubKey)
return nil, managerError(waddrmgr.ErrKeyIsPrivate, str, nil)
return nil, newError(ErrKeyIsPrivate, str, nil)
}
keys[i] = key
}
@ -251,16 +296,18 @@ func convertAndValidatePubKeys(rawPubKeys []string) ([]*hdkeychain.ExtendedKey,
// pool's seriesLookup map. It also ensures inRawPubKeys has at least
// minSeriesPubKeys items and reqSigs is not greater than the number of items in
// inRawPubKeys.
func (vp *Pool) putSeries(version, seriesID, reqSigs uint32, inRawPubKeys []string) error {
//
// This method must be called with the Pool's manager unlocked.
func (p *Pool) putSeries(version, seriesID, reqSigs uint32, inRawPubKeys []string) error {
if len(inRawPubKeys) < minSeriesPubKeys {
str := fmt.Sprintf("need at least %d public keys to create a series", minSeriesPubKeys)
return managerError(waddrmgr.ErrTooFewPublicKeys, str, nil)
return newError(ErrTooFewPublicKeys, str, nil)
}
if reqSigs > uint32(len(inRawPubKeys)) {
str := fmt.Sprintf(
"the number of required signatures cannot be more than the number of keys")
return managerError(waddrmgr.ErrTooManyReqSignatures, str, nil)
return newError(ErrTooManyReqSignatures, str, nil)
}
rawPubKeys := CanonicalKeyOrder(inRawPubKeys)
@ -278,59 +325,90 @@ func (vp *Pool) putSeries(version, seriesID, reqSigs uint32, inRawPubKeys []stri
privateKeys: make([]*hdkeychain.ExtendedKey, len(keys)),
}
err = vp.saveSeriesToDisk(seriesID, data)
err = p.saveSeriesToDisk(seriesID, data)
if err != nil {
return err
}
vp.seriesLookup[seriesID] = data
p.seriesLookup[seriesID] = data
return nil
}
// CreateSeries will create and return a new non-existing series.
//
// - seriesID must be greater than or equal 1;
// - rawPubKeys has to contain three or more public keys;
// - reqSigs has to be less or equal than the number of public keys in rawPubKeys.
func (vp *Pool) CreateSeries(version, seriesID, reqSigs uint32, rawPubKeys []string) error {
if series := vp.GetSeries(seriesID); series != nil {
str := fmt.Sprintf("series #%d already exists", seriesID)
return managerError(waddrmgr.ErrSeriesAlreadyExists, str, nil)
func (p *Pool) CreateSeries(version, seriesID, reqSigs uint32, rawPubKeys []string) error {
if seriesID == 0 {
return newError(ErrSeriesIDInvalid, "series ID cannot be 0", nil)
}
return vp.putSeries(version, seriesID, reqSigs, rawPubKeys)
if series := p.Series(seriesID); series != nil {
str := fmt.Sprintf("series #%d already exists", seriesID)
return newError(ErrSeriesAlreadyExists, str, nil)
}
if seriesID != 1 {
if _, ok := p.seriesLookup[seriesID-1]; !ok {
str := fmt.Sprintf("series #%d cannot be created because series #%d does not exist",
seriesID, seriesID-1)
return newError(ErrSeriesIDNotSequential, str, nil)
}
}
return p.putSeries(version, seriesID, reqSigs, rawPubKeys)
}
// ActivateSeries marks the series with the given ID as active.
func (p *Pool) ActivateSeries(seriesID uint32) error {
series := p.Series(seriesID)
if series == nil {
str := fmt.Sprintf("series #%d does not exist, cannot activate it", seriesID)
return newError(ErrSeriesNotExists, str, nil)
}
series.active = true
err := p.saveSeriesToDisk(seriesID, series)
if err != nil {
return err
}
p.seriesLookup[seriesID] = series
return nil
}
// ReplaceSeries will replace an already existing series.
//
// - rawPubKeys has to contain three or more public keys
// - reqSigs has to be less or equal than the number of public keys in rawPubKeys.
func (vp *Pool) ReplaceSeries(version, seriesID, reqSigs uint32, rawPubKeys []string) error {
series := vp.GetSeries(seriesID)
func (p *Pool) ReplaceSeries(version, seriesID, reqSigs uint32, rawPubKeys []string) error {
series := p.Series(seriesID)
if series == nil {
str := fmt.Sprintf("series #%d does not exist, cannot replace it", seriesID)
return managerError(waddrmgr.ErrSeriesNotExists, str, nil)
return newError(ErrSeriesNotExists, str, nil)
}
if series.IsEmpowered() {
str := fmt.Sprintf("series #%d has private keys and cannot be replaced", seriesID)
return managerError(waddrmgr.ErrSeriesAlreadyEmpowered, str, nil)
return newError(ErrSeriesAlreadyEmpowered, str, nil)
}
return vp.putSeries(version, seriesID, reqSigs, rawPubKeys)
return p.putSeries(version, seriesID, reqSigs, rawPubKeys)
}
// decryptExtendedKey uses Manager.Decrypt() to decrypt the encrypted byte slice and return
// an extended (public or private) key representing it.
func (vp *Pool) decryptExtendedKey(keyType waddrmgr.CryptoKeyType, encrypted []byte) (*hdkeychain.ExtendedKey, error) {
decrypted, err := vp.manager.Decrypt(keyType, encrypted)
//
// This method must be called with the Pool's manager unlocked.
func (p *Pool) decryptExtendedKey(keyType waddrmgr.CryptoKeyType, encrypted []byte) (*hdkeychain.ExtendedKey, error) {
decrypted, err := p.manager.Decrypt(keyType, encrypted)
if err != nil {
str := fmt.Sprintf("cannot decrypt key %v", encrypted)
return nil, managerError(waddrmgr.ErrCrypto, str, err)
return nil, newError(ErrCrypto, str, err)
}
result, err := hdkeychain.NewKeyFromString(string(decrypted))
zero.Bytes(decrypted)
if err != nil {
str := fmt.Sprintf("cannot get key from string %v", decrypted)
return nil, managerError(waddrmgr.ErrKeyChain, str, err)
return nil, newError(ErrKeyChain, str, err)
}
return result, nil
}
@ -338,17 +416,19 @@ func (vp *Pool) decryptExtendedKey(keyType waddrmgr.CryptoKeyType, encrypted []b
// validateAndDecryptSeriesKeys checks that the length of the public and private key
// slices is the same, decrypts them, ensures the non-nil private keys have a matching
// public key and returns them.
func validateAndDecryptKeys(rawPubKeys, rawPrivKeys [][]byte, vp *Pool) (pubKeys, privKeys []*hdkeychain.ExtendedKey, err error) {
//
// This function must be called with the Pool's manager unlocked.
func validateAndDecryptKeys(rawPubKeys, rawPrivKeys [][]byte, p *Pool) (pubKeys, privKeys []*hdkeychain.ExtendedKey, err error) {
pubKeys = make([]*hdkeychain.ExtendedKey, len(rawPubKeys))
privKeys = make([]*hdkeychain.ExtendedKey, len(rawPrivKeys))
if len(pubKeys) != len(privKeys) {
return nil, nil, managerError(waddrmgr.ErrKeysPrivatePublicMismatch,
return nil, nil, newError(ErrKeysPrivatePublicMismatch,
"the pub key and priv key arrays should have the same number of elements",
nil)
}
for i, encryptedPub := range rawPubKeys {
pubKey, err := vp.decryptExtendedKey(waddrmgr.CKTPublic, encryptedPub)
pubKey, err := p.decryptExtendedKey(waddrmgr.CKTPublic, encryptedPub)
if err != nil {
return nil, nil, err
}
@ -359,7 +439,7 @@ func validateAndDecryptKeys(rawPubKeys, rawPrivKeys [][]byte, vp *Pool) (pubKeys
if encryptedPriv == nil {
privKey = nil
} else {
privKey, err = vp.decryptExtendedKey(waddrmgr.CKTPrivate, encryptedPriv)
privKey, err = p.decryptExtendedKey(waddrmgr.CKTPrivate, encryptedPriv)
if err != nil {
return nil, nil, err
}
@ -370,12 +450,12 @@ func validateAndDecryptKeys(rawPubKeys, rawPrivKeys [][]byte, vp *Pool) (pubKeys
checkPubKey, err := privKey.Neuter()
if err != nil {
str := fmt.Sprintf("cannot neuter key %v", privKey)
return nil, nil, managerError(waddrmgr.ErrKeyNeuter, str, err)
return nil, nil, newError(ErrKeyNeuter, str, err)
}
if pubKey.String() != checkPubKey.String() {
str := fmt.Sprintf("public key %v different than expected %v",
pubKey, checkPubKey)
return nil, nil, managerError(waddrmgr.ErrKeyMismatch, str, nil)
return nil, nil, newError(ErrKeyMismatch, str, nil)
}
}
}
@ -387,11 +467,15 @@ func validateAndDecryptKeys(rawPubKeys, rawPrivKeys [][]byte, vp *Pool) (pubKeys
// seriesLookup map with them. If there are any private extended keys for
// a series, it will also ensure they have a matching extended public key
// in that series.
func (vp *Pool) LoadAllSeries() error {
//
// This method must be called with the Pool's manager unlocked.
// FIXME: We should be able to get rid of this (and loadAllSeries/seriesLookup)
// by making Series() load the series data directly from the DB.
func (p *Pool) LoadAllSeries() error {
var series map[uint32]*dbSeriesRow
err := vp.namespace.View(func(tx walletdb.Tx) error {
err := p.namespace.View(func(tx walletdb.Tx) error {
var err error
series, err = loadAllSeries(tx, vp.ID)
series, err = loadAllSeries(tx, p.ID)
return err
})
if err != nil {
@ -399,11 +483,11 @@ func (vp *Pool) LoadAllSeries() error {
}
for id, series := range series {
pubKeys, privKeys, err := validateAndDecryptKeys(
series.pubKeysEncrypted, series.privKeysEncrypted, vp)
series.pubKeysEncrypted, series.privKeysEncrypted, p)
if err != nil {
return err
}
vp.seriesLookup[id] = &SeriesData{
p.seriesLookup[id] = &SeriesData{
publicKeys: pubKeys,
privateKeys: privKeys,
reqSigs: series.reqSigs,
@ -412,41 +496,22 @@ func (vp *Pool) LoadAllSeries() error {
return nil
}
// existsSeries checks whether a series is stored in the database.
// Used solely by the series creation test.
func (vp *Pool) existsSeries(seriesID uint32) (bool, error) {
var exists bool
err := vp.namespace.View(
func(tx walletdb.Tx) error {
bucket := tx.RootBucket().Bucket(vp.ID)
if bucket == nil {
exists = false
return nil
}
exists = bucket.Get(uint32ToBytes(seriesID)) != nil
return nil
})
if err != nil {
return false, err
}
return exists, nil
}
// Change the order of the pubkeys based on branch number.
// Given the three pubkeys ABC, this would mean:
// - branch 0: CBA (reversed)
// - branch 1: ABC (first key priority)
// - branch 2: BAC (second key priority)
// - branch 3: CAB (third key priority)
func branchOrder(pks []*hdkeychain.ExtendedKey, branch uint32) ([]*hdkeychain.ExtendedKey, error) {
func branchOrder(pks []*hdkeychain.ExtendedKey, branch Branch) ([]*hdkeychain.ExtendedKey, error) {
if pks == nil {
// This really shouldn't happen, but we want to be good citizens, so we
// return an error instead of crashing.
return nil, managerError(waddrmgr.ErrInvalidValue, "pks cannot be nil", nil)
return nil, newError(ErrInvalidValue, "pks cannot be nil", nil)
}
if branch > uint32(len(pks)) {
return nil, managerError(waddrmgr.ErrInvalidBranch, "branch number is bigger than number of public keys", nil)
if branch > Branch(len(pks)) {
return nil, newError(
ErrInvalidBranch, "branch number is bigger than number of public keys", nil)
}
if branch == 0 {
@ -472,27 +537,29 @@ func branchOrder(pks []*hdkeychain.ExtendedKey, branch uint32) ([]*hdkeychain.Ex
return tmp, nil
}
// DepositScriptAddress constructs a multi-signature redemption script using DepositScript
// and returns the pay-to-script-hash-address for that script.
func (vp *Pool) DepositScriptAddress(seriesID, branch, index uint32) (btcutil.Address, error) {
script, err := vp.DepositScript(seriesID, branch, index)
// DepositScriptAddress calls DepositScript to get a multi-signature
// redemption script and returns the pay-to-script-hash-address for that script.
func (p *Pool) DepositScriptAddress(seriesID uint32, branch Branch, index Index) (btcutil.Address, error) {
script, err := p.DepositScript(seriesID, branch, index)
if err != nil {
return nil, err
}
scriptHash := btcutil.Hash160(script)
return p.addressFor(script)
}
return btcutil.NewAddressScriptHashFromHash(scriptHash,
vp.manager.ChainParams())
func (p *Pool) addressFor(script []byte) (btcutil.Address, error) {
scriptHash := btcutil.Hash160(script)
return btcutil.NewAddressScriptHashFromHash(scriptHash, p.manager.ChainParams())
}
// DepositScript constructs and returns a multi-signature redemption script where
// a certain number (Series.reqSigs) of the public keys belonging to the series
// with the given ID are required to sign the transaction for it to be successful.
func (vp *Pool) DepositScript(seriesID, branch, index uint32) ([]byte, error) {
series := vp.GetSeries(seriesID)
func (p *Pool) DepositScript(seriesID uint32, branch Branch, index Index) ([]byte, error) {
series := p.Series(seriesID)
if series == nil {
str := fmt.Sprintf("series #%d does not exist", seriesID)
return nil, managerError(waddrmgr.ErrSeriesNotExists, str, nil)
return nil, newError(ErrSeriesNotExists, str, nil)
}
pubKeys, err := branchOrder(series.publicKeys, branch)
@ -502,68 +569,137 @@ func (vp *Pool) DepositScript(seriesID, branch, index uint32) ([]byte, error) {
pks := make([]*btcutil.AddressPubKey, len(pubKeys))
for i, key := range pubKeys {
child, err := key.Child(index)
child, err := key.Child(uint32(index))
// TODO: implement getting the next index until we find a valid one,
// in case there is a hdkeychain.ErrInvalidChild.
if err != nil {
str := fmt.Sprintf("child #%d for this pubkey %d does not exist", index, i)
return nil, managerError(waddrmgr.ErrKeyChain, str, err)
return nil, newError(ErrKeyChain, str, err)
}
pubkey, err := child.ECPubKey()
if err != nil {
str := fmt.Sprintf("child #%d for this pubkey %d does not exist", index, i)
return nil, managerError(waddrmgr.ErrKeyChain, str, err)
return nil, newError(ErrKeyChain, str, err)
}
pks[i], err = btcutil.NewAddressPubKey(pubkey.SerializeCompressed(),
vp.manager.ChainParams())
p.manager.ChainParams())
if err != nil {
str := fmt.Sprintf(
"child #%d for this pubkey %d could not be converted to an address",
index, i)
return nil, managerError(waddrmgr.ErrKeyChain, str, err)
return nil, newError(ErrKeyChain, str, err)
}
}
script, err := txscript.MultiSigScript(pks, int(series.reqSigs))
if err != nil {
str := fmt.Sprintf("error while making multisig script hash, %d", len(pks))
return nil, managerError(waddrmgr.ErrScriptCreation, str, err)
return nil, newError(ErrScriptCreation, str, err)
}
return script, nil
}
// ChangeAddress returns a new votingpool address for the given seriesID and
// index, on the 0th branch (which is reserved for change addresses). The series
// with the given ID must be active.
func (p *Pool) ChangeAddress(seriesID uint32, index Index) (*ChangeAddress, error) {
series := p.Series(seriesID)
if series == nil {
return nil, newError(ErrSeriesNotExists,
fmt.Sprintf("series %d does not exist", seriesID), nil)
}
if !series.active {
str := fmt.Sprintf("ChangeAddress must be on active series; series #%d is not", seriesID)
return nil, newError(ErrSeriesNotActive, str, nil)
}
script, err := p.DepositScript(seriesID, Branch(0), index)
if err != nil {
return nil, err
}
pAddr, err := p.poolAddress(seriesID, Branch(0), index, script)
if err != nil {
return nil, err
}
return &ChangeAddress{poolAddress: pAddr}, nil
}
// WithdrawalAddress queries the address manager for the P2SH address
// of the redeem script generated with the given series/branch/index and uses
// that to populate the returned WithdrawalAddress. This is done because we
// should only withdraw from previously used addresses but also because when
// processing withdrawals we may iterate over a huge number of addresses and
// it'd be too expensive to re-generate the redeem script for all of them.
// This method must be called with the manager unlocked.
func (p *Pool) WithdrawalAddress(seriesID uint32, branch Branch, index Index) (
*WithdrawalAddress, error) {
// TODO: Ensure the given series is hot.
addr, err := p.getUsedAddr(seriesID, branch, index)
if err != nil {
return nil, err
}
if addr == nil {
str := fmt.Sprintf("cannot withdraw from unused addr (series: %d, branch: %d, index: %d)",
seriesID, branch, index)
return nil, newError(ErrWithdrawFromUnusedAddr, str, nil)
}
script, err := addr.Script()
if err != nil {
return nil, err
}
pAddr, err := p.poolAddress(seriesID, branch, index, script)
if err != nil {
return nil, err
}
return &WithdrawalAddress{poolAddress: pAddr}, nil
}
func (p *Pool) poolAddress(seriesID uint32, branch Branch, index Index, script []byte) (
*poolAddress, error) {
addr, err := p.addressFor(script)
if err != nil {
return nil, err
}
return &poolAddress{
pool: p, seriesID: seriesID, branch: branch, index: index, addr: addr,
script: script},
nil
}
// EmpowerSeries adds the given extended private key (in raw format) to the
// series with the given ID, thus allowing it to sign deposit/withdrawal
// scripts. The series with the given ID must exist, the key must be a valid
// private extended key and must match one of the series' extended public keys.
func (vp *Pool) EmpowerSeries(seriesID uint32, rawPrivKey string) error {
//
// This method must be called with the Pool's manager unlocked.
func (p *Pool) EmpowerSeries(seriesID uint32, rawPrivKey string) error {
// make sure this series exists
series := vp.GetSeries(seriesID)
series := p.Series(seriesID)
if series == nil {
str := fmt.Sprintf("series %d does not exist for this voting pool",
seriesID)
return managerError(waddrmgr.ErrSeriesNotExists, str, nil)
return newError(ErrSeriesNotExists, str, nil)
}
// Check that the private key is valid.
privKey, err := hdkeychain.NewKeyFromString(rawPrivKey)
if err != nil {
str := fmt.Sprintf("invalid extended private key %v", rawPrivKey)
return managerError(waddrmgr.ErrKeyChain, str, err)
return newError(ErrKeyChain, str, err)
}
if !privKey.IsPrivate() {
str := fmt.Sprintf(
"to empower a series you need the extended private key, not an extended public key %v",
privKey)
return managerError(waddrmgr.ErrKeyIsPublic, str, err)
return newError(ErrKeyIsPublic, str, err)
}
pubKey, err := privKey.Neuter()
if err != nil {
str := fmt.Sprintf("invalid extended private key %v, can't convert to public key",
rawPrivKey)
return managerError(waddrmgr.ErrKeyNeuter, str, err)
return newError(ErrKeyNeuter, str, err)
}
lookingFor := pubKey.String()
@ -581,18 +717,153 @@ func (vp *Pool) EmpowerSeries(seriesID uint32, rawPrivKey string) error {
if !found {
str := fmt.Sprintf(
"private Key does not have a corresponding public key in this series")
return managerError(waddrmgr.ErrKeysPrivatePublicMismatch, str, nil)
return newError(ErrKeysPrivatePublicMismatch, str, nil)
}
err = vp.saveSeriesToDisk(seriesID, series)
if err != nil {
if err = p.saveSeriesToDisk(seriesID, series); err != nil {
return err
}
return nil
}
// EnsureUsedAddr ensures we have entries in our used addresses DB for the given
// seriesID, branch and all indices up to the given one. It must be called with
// the manager unlocked.
func (p *Pool) EnsureUsedAddr(seriesID uint32, branch Branch, index Index) error {
lastIdx, err := p.highestUsedIndexFor(seriesID, branch)
if err != nil {
return err
}
if lastIdx == 0 {
// highestUsedIndexFor() returns 0 when there are no used addresses for a
// given seriesID/branch, so we do this to ensure there is an entry with
// index==0.
if err := p.addUsedAddr(seriesID, branch, lastIdx); err != nil {
return err
}
}
lastIdx++
for lastIdx <= index {
if err := p.addUsedAddr(seriesID, branch, lastIdx); err != nil {
return err
}
lastIdx++
}
return nil
}
// addUsedAddr creates a deposit script for the given seriesID/branch/index,
// ensures it is imported into the address manager and finaly adds the script
// hash to our used addresses DB. It must be called with the manager unlocked.
func (p *Pool) addUsedAddr(seriesID uint32, branch Branch, index Index) error {
script, err := p.DepositScript(seriesID, branch, index)
if err != nil {
return err
}
// First ensure the address manager has our script. That way there's no way
// to have it in the used addresses DB but not in the address manager.
// TODO: Decide how far back we want the addr manager to rescan and set the
// BlockStamp height according to that.
_, err = p.manager.ImportScript(script, &waddrmgr.BlockStamp{})
if err != nil && err.(waddrmgr.ManagerError).ErrorCode != waddrmgr.ErrDuplicateAddress {
return err
}
encryptedHash, err := p.manager.Encrypt(waddrmgr.CKTPublic, btcutil.Hash160(script))
if err != nil {
return newError(ErrCrypto, "failed to encrypt script hash", err)
}
err = p.namespace.Update(
func(tx walletdb.Tx) error {
return putUsedAddrHash(tx, p.ID, seriesID, branch, index, encryptedHash)
})
if err != nil {
return newError(ErrDatabase, "failed to store used addr script hash", err)
}
return nil
}
// getUsedAddr gets the script hash for the given series, branch and index from
// the used addresses DB and uses that to look up the ManagedScriptAddress
// from the address manager. It must be called with the manager unlocked.
func (p *Pool) getUsedAddr(seriesID uint32, branch Branch, index Index) (
waddrmgr.ManagedScriptAddress, error) {
mgr := p.manager
var encryptedHash []byte
err := p.namespace.View(
func(tx walletdb.Tx) error {
encryptedHash = getUsedAddrHash(tx, p.ID, seriesID, branch, index)
return nil
})
if err != nil {
return nil, newError(ErrDatabase, "failed to lookup script hash for used addr", err)
}
if encryptedHash == nil {
return nil, nil
}
hash, err := p.manager.Decrypt(waddrmgr.CKTPublic, encryptedHash)
if err != nil {
return nil, newError(ErrCrypto, "failed to decrypt stored script hash", err)
}
addr, err := btcutil.NewAddressScriptHashFromHash(hash, mgr.ChainParams())
if err != nil {
return nil, newError(ErrInvalidScriptHash, "failed to parse script hash", err)
}
mAddr, err := mgr.Address(addr)
if err != nil {
return nil, err
}
return mAddr.(waddrmgr.ManagedScriptAddress), nil
}
// highestUsedIndexFor returns the highest index from this Pool's used addresses
// with the given seriesID and branch. It returns 0 if there are no used
// addresses with the given seriesID and branch.
func (p *Pool) highestUsedIndexFor(seriesID uint32, branch Branch) (Index, error) {
maxIdx := Index(0)
err := p.namespace.View(
func(tx walletdb.Tx) error {
var err error
maxIdx, err = getMaxUsedIdx(tx, p.ID, seriesID, branch)
return err
})
return maxIdx, err
}
// String returns a string encoding of the underlying bitcoin payment address.
func (a *poolAddress) String() string {
return a.addr.EncodeAddress()
}
func (a *poolAddress) addrIdentifier() string {
return fmt.Sprintf("PoolAddress seriesID:%d, branch:%d, index:%d", a.seriesID, a.branch,
a.index)
}
func (a *poolAddress) redeemScript() []byte {
return a.script
}
func (a *poolAddress) series() *SeriesData {
return a.pool.Series(a.seriesID)
}
func (a *poolAddress) SeriesID() uint32 {
return a.seriesID
}
func (a *poolAddress) Branch() Branch {
return a.branch
}
func (a *poolAddress) Index() Index {
return a.index
}
// IsEmpowered returns true if this series is empowered (i.e. if it has
// at least one private key loaded).
func (s *SeriesData) IsEmpowered() bool {
@ -604,8 +875,12 @@ func (s *SeriesData) IsEmpowered() bool {
return false
}
// managerError creates a waddrmgr.ManagerError given a set of arguments.
// XXX(lars): We should probably make our own votingpoolError function.
func managerError(c waddrmgr.ErrorCode, desc string, err error) waddrmgr.ManagerError {
return waddrmgr.ManagerError{ErrorCode: c, Description: desc, Err: err}
func (s *SeriesData) getPrivKeyFor(pubKey *hdkeychain.ExtendedKey) (*hdkeychain.ExtendedKey, error) {
for i, key := range s.publicKeys {
if key.String() == pubKey.String() {
return s.privateKeys[i], nil
}
}
return nil, newError(ErrUnknownPubKey, fmt.Sprintf("unknown public key '%s'",
pubKey.String()), nil)
}

File diff suppressed because it is too large Load diff

463
votingpool/pool_wb_test.go Normal file
View file

@ -0,0 +1,463 @@
/*
* Copyright (c) 2015 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool
import (
"bytes"
"fmt"
"testing"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/waddrmgr"
)
func TestPoolEnsureUsedAddr(t *testing.T) {
tearDown, mgr, pool := TstCreatePool(t)
defer tearDown()
var err error
var script []byte
var addr waddrmgr.ManagedScriptAddress
TstCreateSeries(t, pool, []TstSeriesDef{{ReqSigs: 2, PubKeys: TstPubKeys[0:3], SeriesID: 1}})
idx := Index(0)
TstRunWithManagerUnlocked(t, mgr, func() {
err = pool.EnsureUsedAddr(1, 0, idx)
})
if err != nil {
t.Fatalf("Failed to ensure used addresses: %v", err)
}
addr, err = pool.getUsedAddr(1, 0, 0)
if err != nil {
t.Fatalf("Failed to get addr from used addresses set: %v", err)
}
TstRunWithManagerUnlocked(t, mgr, func() {
script, err = addr.Script()
})
if err != nil {
t.Fatalf("Failed to get script: %v", err)
}
wantScript, _ := pool.DepositScript(1, 0, 0)
if !bytes.Equal(script, wantScript) {
t.Fatalf("Script from looked up addr is not what we expect")
}
idx = Index(3)
TstRunWithManagerUnlocked(t, mgr, func() {
err = pool.EnsureUsedAddr(1, 0, idx)
})
if err != nil {
t.Fatalf("Failed to ensure used addresses: %v", err)
}
for _, i := range []int{0, 1, 2, 3} {
addr, err = pool.getUsedAddr(1, 0, Index(i))
if err != nil {
t.Fatalf("Failed to get addr from used addresses set: %v", err)
}
TstRunWithManagerUnlocked(t, mgr, func() {
script, err = addr.Script()
})
if err != nil {
t.Fatalf("Failed to get script: %v", err)
}
wantScript, _ := pool.DepositScript(1, 0, Index(i))
if !bytes.Equal(script, wantScript) {
t.Fatalf("Script from looked up addr is not what we expect")
}
}
}
func TestPoolGetUsedAddr(t *testing.T) {
tearDown, mgr, pool := TstCreatePool(t)
defer tearDown()
TstCreateSeries(t, pool, []TstSeriesDef{{ReqSigs: 2, PubKeys: TstPubKeys[0:3], SeriesID: 1}})
// Addr with series=1, branch=0, index=10 has never been used, so it should
// return nil.
addr, err := pool.getUsedAddr(1, 0, 10)
if err != nil {
t.Fatalf("Error when looking up used addr: %v", err)
}
if addr != nil {
t.Fatalf("Unused address found in used addresses DB: %v", addr)
}
// Now we add that addr to the used addresses DB and check that the value
// returned by getUsedAddr() is what we expect.
TstRunWithManagerUnlocked(t, mgr, func() {
err = pool.addUsedAddr(1, 0, 10)
})
if err != nil {
t.Fatalf("Error when storing addr in used addresses DB: %v", err)
}
var script []byte
addr, err = pool.getUsedAddr(1, 0, 10)
if err != nil {
t.Fatalf("Error when looking up used addr: %v", err)
}
TstRunWithManagerUnlocked(t, mgr, func() {
script, err = addr.Script()
})
if err != nil {
t.Fatalf("Failed to get script: %v", err)
}
wantScript, _ := pool.DepositScript(1, 0, 10)
if !bytes.Equal(script, wantScript) {
t.Fatalf("Script from looked up addr is not what we expect")
}
}
func TestSerializationErrors(t *testing.T) {
tearDown, mgr, _ := TstCreatePool(t)
defer tearDown()
tests := []struct {
version uint32
pubKeys []string
privKeys []string
reqSigs uint32
err ErrorCode
}{
{
version: 2,
pubKeys: TstPubKeys[0:3],
err: ErrSeriesVersion,
},
{
pubKeys: []string{"NONSENSE"},
// Not a valid length public key.
err: ErrSeriesSerialization,
},
{
pubKeys: TstPubKeys[0:3],
privKeys: TstPrivKeys[0:1],
// The number of public and private keys should be the same.
err: ErrSeriesSerialization,
},
{
pubKeys: TstPubKeys[0:1],
privKeys: []string{"NONSENSE"},
// Not a valid length private key.
err: ErrSeriesSerialization,
},
}
active := true
for testNum, test := range tests {
encryptedPubs, err := encryptKeys(test.pubKeys, mgr, waddrmgr.CKTPublic)
if err != nil {
t.Fatalf("Test #%d - Error encrypting pubkeys: %v", testNum, err)
}
var encryptedPrivs [][]byte
TstRunWithManagerUnlocked(t, mgr, func() {
encryptedPrivs, err = encryptKeys(test.privKeys, mgr, waddrmgr.CKTPrivate)
})
if err != nil {
t.Fatalf("Test #%d - Error encrypting privkeys: %v", testNum, err)
}
row := &dbSeriesRow{
version: test.version,
active: active,
reqSigs: test.reqSigs,
pubKeysEncrypted: encryptedPubs,
privKeysEncrypted: encryptedPrivs}
_, err = serializeSeriesRow(row)
TstCheckError(t, fmt.Sprintf("Test #%d", testNum), err, test.err)
}
}
func TestSerialization(t *testing.T) {
tearDown, mgr, _ := TstCreatePool(t)
defer tearDown()
tests := []struct {
version uint32
active bool
pubKeys []string
privKeys []string
reqSigs uint32
}{
{
version: 1,
active: true,
pubKeys: TstPubKeys[0:1],
reqSigs: 1,
},
{
version: 0,
active: false,
pubKeys: TstPubKeys[0:1],
privKeys: TstPrivKeys[0:1],
reqSigs: 1,
},
{
pubKeys: TstPubKeys[0:3],
privKeys: []string{TstPrivKeys[0], "", ""},
reqSigs: 2,
},
{
pubKeys: TstPubKeys[0:5],
reqSigs: 3,
},
{
pubKeys: TstPubKeys[0:7],
privKeys: []string{"", TstPrivKeys[1], "", TstPrivKeys[3], "", "", ""},
reqSigs: 4,
},
}
var encryptedPrivs [][]byte
for testNum, test := range tests {
encryptedPubs, err := encryptKeys(test.pubKeys, mgr, waddrmgr.CKTPublic)
if err != nil {
t.Fatalf("Test #%d - Error encrypting pubkeys: %v", testNum, err)
}
TstRunWithManagerUnlocked(t, mgr, func() {
encryptedPrivs, err = encryptKeys(test.privKeys, mgr, waddrmgr.CKTPrivate)
})
if err != nil {
t.Fatalf("Test #%d - Error encrypting privkeys: %v", testNum, err)
}
row := &dbSeriesRow{
version: test.version,
active: test.active,
reqSigs: test.reqSigs,
pubKeysEncrypted: encryptedPubs,
privKeysEncrypted: encryptedPrivs,
}
serialized, err := serializeSeriesRow(row)
if err != nil {
t.Fatalf("Test #%d - Error in serialization %v", testNum, err)
}
row, err = deserializeSeriesRow(serialized)
if err != nil {
t.Fatalf("Test #%d - Failed to deserialize %v %v", testNum, serialized, err)
}
if row.version != test.version {
t.Errorf("Serialization #%d - version mismatch: got %d want %d",
testNum, row.version, test.version)
}
if row.active != test.active {
t.Errorf("Serialization #%d - active mismatch: got %v want %v",
testNum, row.active, test.active)
}
if row.reqSigs != test.reqSigs {
t.Errorf("Serialization #%d - row reqSigs off. Got %d, want %d",
testNum, row.reqSigs, test.reqSigs)
}
if len(row.pubKeysEncrypted) != len(test.pubKeys) {
t.Errorf("Serialization #%d - Wrong no. of pubkeys. Got %d, want %d",
testNum, len(row.pubKeysEncrypted), len(test.pubKeys))
}
for i, encryptedPub := range encryptedPubs {
got := string(row.pubKeysEncrypted[i])
if got != string(encryptedPub) {
t.Errorf("Serialization #%d - Pubkey deserialization. Got %v, want %v",
testNum, got, string(encryptedPub))
}
}
if len(row.privKeysEncrypted) != len(row.pubKeysEncrypted) {
t.Errorf("Serialization #%d - no. privkeys (%d) != no. pubkeys (%d)",
testNum, len(row.privKeysEncrypted), len(row.pubKeysEncrypted))
}
for i, encryptedPriv := range encryptedPrivs {
got := string(row.privKeysEncrypted[i])
if got != string(encryptedPriv) {
t.Errorf("Serialization #%d - Privkey deserialization. Got %v, want %v",
testNum, got, string(encryptedPriv))
}
}
}
}
func TestDeserializationErrors(t *testing.T) {
tearDown, _, _ := TstCreatePool(t)
defer tearDown()
tests := []struct {
serialized []byte
err ErrorCode
}{
{
serialized: make([]byte, seriesMaxSerial+1),
// Too many bytes (over seriesMaxSerial).
err: ErrSeriesSerialization,
},
{
serialized: make([]byte, seriesMinSerial-1),
// Not enough bytes (under seriesMinSerial).
err: ErrSeriesSerialization,
},
{
serialized: []byte{
1, 0, 0, 0, // 4 bytes (version)
0, // 1 byte (active)
2, 0, 0, 0, // 4 bytes (reqSigs)
3, 0, 0, 0, // 4 bytes (nKeys)
},
// Here we have the constant data but are missing any public/private keys.
err: ErrSeriesSerialization,
},
{
serialized: []byte{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
// Unsupported version.
err: ErrSeriesVersion,
},
}
for testNum, test := range tests {
_, err := deserializeSeriesRow(test.serialized)
TstCheckError(t, fmt.Sprintf("Test #%d", testNum), err, test.err)
}
}
func TestValidateAndDecryptKeys(t *testing.T) {
tearDown, manager, pool := TstCreatePool(t)
defer tearDown()
rawPubKeys, err := encryptKeys(TstPubKeys[0:2], manager, waddrmgr.CKTPublic)
if err != nil {
t.Fatalf("Failed to encrypt public keys: %v", err)
}
var rawPrivKeys [][]byte
TstRunWithManagerUnlocked(t, manager, func() {
rawPrivKeys, err = encryptKeys([]string{TstPrivKeys[0], ""}, manager, waddrmgr.CKTPrivate)
})
if err != nil {
t.Fatalf("Failed to encrypt private keys: %v", err)
}
var pubKeys, privKeys []*hdkeychain.ExtendedKey
TstRunWithManagerUnlocked(t, manager, func() {
pubKeys, privKeys, err = validateAndDecryptKeys(rawPubKeys, rawPrivKeys, pool)
})
if err != nil {
t.Fatalf("Error when validating/decrypting keys: %v", err)
}
if len(pubKeys) != 2 {
t.Fatalf("Unexpected number of decrypted public keys: got %d, want 2", len(pubKeys))
}
if len(privKeys) != 2 {
t.Fatalf("Unexpected number of decrypted private keys: got %d, want 2", len(privKeys))
}
if pubKeys[0].String() != TstPubKeys[0] || pubKeys[1].String() != TstPubKeys[1] {
t.Fatalf("Public keys don't match: %v!=%v ", TstPubKeys[0:2], pubKeys)
}
if privKeys[0].String() != TstPrivKeys[0] || privKeys[1] != nil {
t.Fatalf("Private keys don't match: %v, %v", []string{TstPrivKeys[0], ""}, privKeys)
}
neuteredKey, err := privKeys[0].Neuter()
if err != nil {
t.Fatalf("Unable to neuter private key: %v", err)
}
if pubKeys[0].String() != neuteredKey.String() {
t.Errorf("Public key (%v) does not match neutered private key (%v)",
pubKeys[0].String(), neuteredKey.String())
}
}
func TestValidateAndDecryptKeysErrors(t *testing.T) {
tearDown, manager, pool := TstCreatePool(t)
defer tearDown()
encryptedPubKeys, err := encryptKeys(TstPubKeys[0:1], manager, waddrmgr.CKTPublic)
if err != nil {
t.Fatalf("Failed to encrypt public key: %v", err)
}
var encryptedPrivKeys [][]byte
TstRunWithManagerUnlocked(t, manager, func() {
encryptedPrivKeys, err = encryptKeys(TstPrivKeys[1:2], manager, waddrmgr.CKTPrivate)
})
if err != nil {
t.Fatalf("Failed to encrypt private key: %v", err)
}
tests := []struct {
rawPubKeys [][]byte
rawPrivKeys [][]byte
err ErrorCode
}{
{
// Number of public keys does not match number of private keys.
rawPubKeys: [][]byte{[]byte(TstPubKeys[0])},
rawPrivKeys: [][]byte{},
err: ErrKeysPrivatePublicMismatch,
},
{
// Failure to decrypt public key.
rawPubKeys: [][]byte{[]byte(TstPubKeys[0])},
rawPrivKeys: [][]byte{[]byte(TstPrivKeys[0])},
err: ErrCrypto,
},
{
// Failure to decrypt private key.
rawPubKeys: encryptedPubKeys,
rawPrivKeys: [][]byte{[]byte(TstPrivKeys[0])},
err: ErrCrypto,
},
{
// One public and one private key, but they don't match.
rawPubKeys: encryptedPubKeys,
rawPrivKeys: encryptedPrivKeys,
err: ErrKeyMismatch,
},
}
for i, test := range tests {
TstRunWithManagerUnlocked(t, manager, func() {
_, _, err = validateAndDecryptKeys(test.rawPubKeys, test.rawPrivKeys, pool)
})
TstCheckError(t, fmt.Sprintf("Test #%d", i), err, test.err)
}
}
func encryptKeys(keys []string, mgr *waddrmgr.Manager, keyType waddrmgr.CryptoKeyType) ([][]byte, error) {
encryptedKeys := make([][]byte, len(keys))
var err error
for i, key := range keys {
if key == "" {
encryptedKeys[i] = nil
} else {
encryptedKeys[i], err = mgr.Encrypt(keyType, []byte(key))
}
if err != nil {
return nil, err
}
}
return encryptedKeys, nil
}

View file

@ -1,92 +1,142 @@
github.com/conformal/btcwallet/votingpool/db.go serializeSeriesRow 100.00% (29/29)
github.com/conformal/btcwallet/votingpool/pool.go branchOrder 100.00% (19/19)
github.com/conformal/btcwallet/votingpool/pool.go convertAndValidatePubKeys 100.00% (16/16)
github.com/conformal/btcwallet/votingpool/input_selection.go Credits.Less 100.00% (12/12)
github.com/conformal/btcwallet/votingpool/pool.go Pool.decryptExtendedKey 100.00% (10/10)
github.com/conformal/btcwallet/votingpool/pool.go Pool.ReplaceSeries 100.00% (8/8)
github.com/conformal/btcwallet/votingpool/input_selection.go AddressRange.NumAddresses 100.00% (7/7)
github.com/conformal/btcwallet/votingpool/pool.go Create 100.00% (5/5)
github.com/conformal/btcwallet/votingpool/db.go putPool 100.00% (5/5)
github.com/conformal/btcwallet/votingpool/pool.go Pool.DepositScriptAddress 100.00% (5/5)
github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.addTxIn 100.00% (4/4)
github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.addTxOut 100.00% (4/4)
github.com/conformal/btcwallet/votingpool/pool.go CanonicalKeyOrder 100.00% (4/4)
github.com/conformal/btcwallet/votingpool/pool.go @81:3 100.00% (4/4)
github.com/conformal/btcwallet/votingpool/pool.go Pool.GetSeries 100.00% (4/4)
github.com/conformal/btcwallet/votingpool/pool.go seriesData.IsEmpowered 100.00% (4/4)
github.com/conformal/btcwallet/votingpool/pool.go Pool.CreateSeries 100.00% (4/4)
github.com/conformal/btcwallet/votingpool/pool.go Pool.existsSeries 100.00% (3/3)
github.com/conformal/btcwallet/votingpool/db.go uint32ToBytes 100.00% (3/3)
github.com/conformal/btcwallet/votingpool/pool.go @398:27 100.00% (3/3)
github.com/conformal/btcwallet/votingpool/pool.go zero 100.00% (2/2)
github.com/conformal/btcwallet/votingpool/db.go putSeries 100.00% (2/2)
github.com/conformal/btcwallet/votingpool/db.go existsPool 100.00% (2/2)
github.com/conformal/btcwallet/votingpool/withdrawal.go Ntxid 100.00% (2/2)
github.com/conformal/btcwallet/votingpool/withdrawal.go NewOutputRequest 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.Index 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go init 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go estimateSize 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go calculateFee 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/db.go bytesToUint32 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.isTooBig 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/error.go newError 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/input_selection.go Credit.TxSha 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/input_selection.go Credit.OutputIndex 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/input_selection.go Credit.Address 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/input_selection.go newCredit 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/input_selection.go Credits.Len 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/input_selection.go Credits.Swap 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Outpoints 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Address 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/input_selection.go Pool.isCharterOutput 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/pool.go @67:3 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/pool.go newPool 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Status 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.addOutpoint 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalStatus.Outputs 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Amount 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go ChangeAddress.Next 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/pool.go @205:28 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.Addr 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.SeriesID 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/pool.go managerError 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.Branch 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/pool.go Pool.EmpowerSeries 96.43% (27/28)
github.com/conformal/btcwallet/votingpool/db.go deserializeSeriesRow 94.87% (37/39)
github.com/conformal/btcwallet/votingpool/pool.go Pool.putSeries 93.75% (15/16)
github.com/conformal/btcwallet/votingpool/pool.go validateAndDecryptKeys 92.31% (24/26)
github.com/conformal/btcwallet/votingpool/input_selection.go Pool.getEligibleInputsFromSeries 86.36% (19/22)
github.com/conformal/btcwallet/votingpool/input_selection.go Pool.getEligibleInputs 85.71% (6/7)
github.com/conformal/btcwallet/votingpool/pool.go Load 85.71% (6/7)
github.com/conformal/btcwallet/votingpool/input_selection.go Pool.isCreditEligible 85.71% (6/7)
github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.finalizeCurrentTx 84.21% (16/19)
github.com/conformal/btcwallet/votingpool/input_selection.go groupCreditsByAddr 83.33% (10/12)
github.com/conformal/btcwallet/votingpool/db.go loadAllSeries 83.33% (5/6)
github.com/conformal/btcwallet/votingpool/withdrawal.go Pool.ChangeAddress 83.33% (5/6)
github.com/conformal/btcwallet/votingpool/pool.go LoadAndCreateSeries 80.00% (8/10)
github.com/conformal/btcwallet/votingpool/pool.go Pool.LoadAllSeries 80.00% (8/10)
github.com/conformal/btcwallet/votingpool/pool.go LoadAndEmpowerSeries 80.00% (4/5)
github.com/conformal/btcwallet/votingpool/pool.go LoadAndReplaceSeries 80.00% (4/5)
github.com/conformal/btcwallet/votingpool/withdrawal.go Pool.WithdrawalAddress 80.00% (4/5)
github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.sign 75.76% (25/33)
github.com/conformal/btcwallet/votingpool/pool.go LoadAndGetDepositScript 75.00% (6/8)
github.com/conformal/btcwallet/votingpool/withdrawal.go OutputRequest.pkScript 75.00% (3/4)
github.com/conformal/btcwallet/votingpool/pool.go Pool.DepositScript 73.08% (19/26)
github.com/conformal/btcwallet/votingpool/withdrawal.go ValidateSigScripts 72.73% (8/11)
github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.fulfilNextOutput 72.41% (21/29)
github.com/conformal/btcwallet/votingpool/withdrawal.go SignMultiSigUTXO 71.43% (15/21)
github.com/conformal/btcwallet/votingpool/db.go @77:3 71.43% (5/7)
github.com/conformal/btcwallet/votingpool/withdrawal.go getRedeemScript 71.43% (5/7)
github.com/conformal/btcwallet/votingpool/pool.go Pool.saveSeriesToDisk 70.00% (14/20)
github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.fulfilOutputs 70.00% (7/10)
github.com/conformal/btcwallet/votingpool/withdrawal.go getPrivKey 70.00% (7/10)
github.com/conformal/btcwallet/votingpool/withdrawal.go Pool.Withdrawal 66.67% (12/18)
github.com/conformal/btcwallet/votingpool/pool.go @426:3 66.67% (4/6)
github.com/conformal/btcwallet/votingpool/error.go ErrorCode.String 66.67% (2/3)
github.com/conformal/btcwallet/votingpool/db.go putSeriesRow 53.85% (7/13)
github.com/conformal/btcwallet/votingpool/error.go Error.Error 0.00% (0/3)
github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.updateStatusFor 0.00% (0/0)
github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.rollBackLastOutput 0.00% (0/0)
github.com/conformal/btcwallet/votingpool -------------------------------- 85.36% (554/649)
github.com/btcsuite/btcwallet/votingpool/db.go serializeSeriesRow 100.00% (31/31)
github.com/btcsuite/btcwallet/votingpool/pool.go branchOrder 100.00% (19/19)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.splitLastOutput 100.00% (16/16)
github.com/btcsuite/btcwallet/votingpool/pool.go convertAndValidatePubKeys 100.00% (16/16)
github.com/btcsuite/btcwallet/votingpool/input_selection.go byAddress.Less 100.00% (12/12)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.maybeDropRequests 100.00% (12/12)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.rollBackLastOutput 100.00% (10/10)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go calculateSize 100.00% (10/10)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.decryptExtendedKey 100.00% (10/10)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go OutputRequest.outBailmentIDHash 100.00% (8/8)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.ReplaceSeries 100.00% (8/8)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.toMsgTx 100.00% (8/8)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.CreateSeries 100.00% (8/8)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.addChange 100.00% (7/7)
github.com/btcsuite/btcwallet/votingpool/db.go putPool 100.00% (5/5)
github.com/btcsuite/btcwallet/votingpool/pool.go Create 100.00% (5/5)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go newWithdrawalTx 100.00% (5/5)
github.com/btcsuite/btcwallet/votingpool/db.go getUsedAddrHash 100.00% (4/4)
github.com/btcsuite/btcwallet/votingpool/pool.go CanonicalKeyOrder 100.00% (4/4)
github.com/btcsuite/btcwallet/votingpool/pool.go SeriesData.IsEmpowered 100.00% (4/4)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.DepositScriptAddress 100.00% (4/4)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go getRedeemScript 100.00% (4/4)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go newWithdrawal 100.00% (4/4)
github.com/btcsuite/btcwallet/votingpool/db.go @102:3 100.00% (4/4)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.removeInput 100.00% (4/4)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.removeOutput 100.00% (4/4)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.GetSeries 100.00% (4/4)
github.com/btcsuite/btcwallet/votingpool/pool.go @119:3 100.00% (4/4)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.outputTotal 100.00% (3/3)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.inputTotal 100.00% (3/3)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.highestUsedIndexFor 100.00% (3/3)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.popRequest 100.00% (3/3)
github.com/btcsuite/btcwallet/votingpool/pool.go @458:26 100.00% (3/3)
github.com/btcsuite/btcwallet/votingpool/db.go uint32ToBytes 100.00% (3/3)
github.com/btcsuite/btcwallet/votingpool/pool.go @811:3 100.00% (3/3)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.popInput 100.00% (3/3)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.updateStatusFor 100.00% (3/3)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.addInput 100.00% (2/2)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.addOutput 100.00% (2/2)
github.com/btcsuite/btcwallet/votingpool/db.go putSeries 100.00% (2/2)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go Ntxid 100.00% (2/2)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.addressFor 100.00% (2/2)
github.com/btcsuite/btcwallet/votingpool/pool.go zero 100.00% (2/2)
github.com/btcsuite/btcwallet/votingpool/pool.go @780:3 100.00% (2/2)
github.com/btcsuite/btcwallet/votingpool/db.go existsPool 100.00% (2/2)
github.com/btcsuite/btcwallet/votingpool/db.go getUsedAddrBucketID 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/db.go bytesToUint32 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/error.go newError 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/input_selection.go newCredit 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/input_selection.go credit.TxSha 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/input_selection.go credit.OutputIndex 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/input_selection.go credit.Address 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/input_selection.go byAddress.Len 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/input_selection.go byAddress.Swap 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/input_selection.go Pool.isCharterOutput 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/log.go init 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/log.go DisableLog 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/log.go UseLogger 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/pool.go @105:3 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/pool.go newPool 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.Manager 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/pool.go @250:27 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/pool.go @761:3 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.Addr 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.AddrIdentifier 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.RedeemScript 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.Series 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.SeriesID 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.Branch 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.Index 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go byAmount.Len 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go byAmount.Less 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go byAmount.Swap 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go byOutBailmentID.Len 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go byOutBailmentID.Swap 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go byOutBailmentID.Less 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalStatus.Outputs 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go OutputRequest.String 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go OutputRequest.outBailmentID 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalOutput.addOutpoint 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Status 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Address 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Outpoints 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go OutBailmentOutpoint.Amount 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTxOut.pkScript 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go @237:20 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go @240:16 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.hasChange 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.pushRequest 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.pushInput 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go @246:21 100.00% (1/1)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.EmpowerSeries 96.30% (26/27)
github.com/btcsuite/btcwallet/votingpool/db.go deserializeSeriesRow 94.59% (35/37)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.fulfillNextRequest 94.44% (17/18)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.putSeries 93.75% (15/16)
github.com/btcsuite/btcwallet/votingpool/pool.go validateAndDecryptKeys 92.31% (24/26)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go getRawSigs 91.43% (32/35)
github.com/btcsuite/btcwallet/votingpool/input_selection.go nextAddr 90.48% (19/21)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.fulfillRequests 87.50% (14/16)
github.com/btcsuite/btcwallet/votingpool/db.go getMaxUsedIdx 87.50% (7/8)
github.com/btcsuite/btcwallet/votingpool/input_selection.go Pool.getEligibleInputs 85.71% (30/35)
github.com/btcsuite/btcwallet/votingpool/input_selection.go Pool.isCreditEligible 85.71% (6/7)
github.com/btcsuite/btcwallet/votingpool/pool.go Load 85.71% (6/7)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go signMultiSigUTXO 85.19% (23/27)
github.com/btcsuite/btcwallet/votingpool/input_selection.go groupCreditsByAddr 83.33% (10/12)
github.com/btcsuite/btcwallet/votingpool/db.go loadAllSeries 83.33% (5/6)
github.com/btcsuite/btcwallet/votingpool/input_selection.go Pool.highestUsedSeriesIndex 81.82% (9/11)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.handleOversizeTx 80.00% (12/15)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.LoadAllSeries 80.00% (8/10)
github.com/btcsuite/btcwallet/votingpool/pool.go LoadAndCreateSeries 80.00% (8/10)
github.com/btcsuite/btcwallet/votingpool/pool.go LoadAndEmpowerSeries 80.00% (4/5)
github.com/btcsuite/btcwallet/votingpool/pool.go LoadAndReplaceSeries 80.00% (4/5)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.finalizeCurrentTx 79.31% (23/29)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.ChangeAddress 76.92% (10/13)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.WithdrawalAddress 76.92% (10/13)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.getUsedAddr 76.47% (13/17)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go Pool.StartWithdrawal 75.00% (12/16)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.ensureUsedAddr 75.00% (9/12)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go validateSigScript 75.00% (6/8)
github.com/btcsuite/btcwallet/votingpool/pool.go LoadAndGetDepositScript 75.00% (6/8)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go getTxOutIndex 75.00% (3/4)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.poolAddress 75.00% (3/4)
github.com/btcsuite/btcwallet/votingpool/db.go putUsedAddrHash 75.00% (3/4)
github.com/btcsuite/btcwallet/votingpool/pool.go SeriesData.getPrivKeyFor 75.00% (3/4)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.DepositScript 73.08% (19/26)
github.com/btcsuite/btcwallet/votingpool/db.go @132:3 71.43% (5/7)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go nextChangeAddr 71.43% (5/7)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go storeTransactions 70.59% (12/17)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.saveSeriesToDisk 70.00% (14/20)
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.addUsedAddr 69.23% (9/13)
github.com/btcsuite/btcwallet/votingpool/error.go ErrorCode.String 66.67% (2/3)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go SignTx 62.50% (5/8)
github.com/btcsuite/btcwallet/votingpool/db.go putSeriesRow 53.85% (7/13)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go nBytesToSerialize 40.00% (2/5)
github.com/btcsuite/btcwallet/votingpool/error.go Error.Error 0.00% (0/3)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalOutput.String 0.00% (0/1)
github.com/btcsuite/btcwallet/votingpool/input_selection.go credit.String 0.00% (0/1)
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTxOut.String 0.00% (0/1)
github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.String 0.00% (0/1)
github.com/btcsuite/btcwallet/votingpool ------------------------------- 87.49% (818/935)

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2015 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool
// Sample data used across our tests.
var TstPrivKeys = []string{
"xprv9s21ZrQH143K2j9PK4CXkCu8sgxkpUxCF7p1KVwiV5tdnkeYzJXReUkxz5iB2FUzTXC1L15abCDG4RMxSYT5zhm67uvsnLYxuDhZfoFcB6a",
"xprv9s21ZrQH143K4PtW77ATQAKAGk7KAFFCzxFuAcWduoMEeQhCgWpuYWQvMGZknqdispUbgLZV1YPqFCbpzMJij8tSZ5xPSaZqPbchojeNuq7",
"xprv9s21ZrQH143K27XboWxXZGU5j7VZ9SqVBnmMQPKTbddiWAhuNzeLynKHaZTAti6N454tVUUcvy6u15DfuW68NCBUxry6ZsHHzqoA8UtzdMn",
"xprv9s21ZrQH143K2vb4DGQymRejLcZSksBHTYLxB7Stg1c7Lk9JxgEUGZTozwUKxoEWJPoGSdGnJY1TW7LNFQCWrpZjDdEXJeqJuDde6BmdD4P",
"xprv9s21ZrQH143K4JNmRvWeLc1PggzusKcDYV1y8fAMNDdb9Rm5X1AvGHizxEdhTVR3sc62XvifC6dLAXMuQesX1y6999xnDwQ3aVno8KviU9d",
"xprv9s21ZrQH143K3dxrqESqeHZ7pSwM6Uq77ssQADSBs7qdFs6dyRWmRcPyLUTQRpgB3EduNhJuWkCGG2LHjuUisw8KKfXJpPqYJ1MSPrZpe1z",
"xprv9s21ZrQH143K2nE8ENAMNksTTVxPrMxFNWUuwThMy2bcH9LHTtQDXSNq2pTNcbuq36n5A3J9pbXVqnq5LDXvqniFRLN299kW7Svnxsx9tQv",
"xprv9s21ZrQH143K3p93xF1oFeB6ey5ruUesWjuPxA9Z2R5wf6BLYfGXz7fg7NavWkQ2cx3Vm8w2HV9uKpSprNNHnenGeW9XhYDPSjwS9hyCs33",
"xprv9s21ZrQH143K3WxnnvPZ8SDGXndASvLTFwMLBVzNCVgs9rzP6rXgW92DLvozdyBm8T9bSQvrFm1jMpTJrRE6w1KY5tshFeDk9Nn3K6V5FYX",
}
var TstPubKeys = []string{
"xpub661MyMwAqRbcFDDrR5jY7LqsRioFDwg3cLjc7tML3RRcfYyhXqqgCH5SqMSQdpQ1Xh8EtVwcfm8psD8zXKPcRaCVSY4GCqbb3aMEs27GitE",
"xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9",
"xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh",
"xpub661MyMwAqRbcFQfXKHwz8ZbTtePwAKu8pmGYyVrWEM96DYUTWDYipMnHrFcemZHn13jcRMfsNU3UWQUudiaE7mhkWCHGFRMavF167DQM4Va",
"xpub661MyMwAqRbcGnTEXx3ehjx8EiqQGnL4uhwZw3ZxvZAa2E6E4YVAp63UoVtvm2vMDDF8BdPpcarcf7PWcEKvzHhxzAYw1zG23C2egeh82AR",
"xpub661MyMwAqRbcG83KwFyr1RVrNUmqVwYxV6nzxbqoRTNc8fRnWxq1yQiTBifTHhevcEM9ucZ1TqFS7Kv17Gd81cesv6RDrrvYS9SLPjPXhV5",
"xpub661MyMwAqRbcFGJbLPhMjtpC1XntFpg6jjQWjr6yXN8b9wfS1RiU5EhJt5L7qoFuidYawc3XJoLjT2PcjVpXryS3hn1WmSPCyvQDNuKsfgM",
"xpub661MyMwAqRbcGJDX4GYocn7qCzvMJwNisxpzkYZAakcvXtWV6CanXuz9xdfe5kTptFMJ4hDt2iTiT11zyN14u8R5zLvoZ1gnEVqNLxp1r3v",
"xpub661MyMwAqRbcG13FtwvZVaA15pTerP4JdAGvytPykqDr2fKXePqw3wLhCALPAixsE176jFkc2ac9K3tnF4KwaTRKUqFF5apWD6XL9LHCu7E",
}

919
votingpool/withdrawal.go Normal file
View file

@ -0,0 +1,919 @@
/*
* Copyright (c) 2015 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool
import (
"bytes"
"fmt"
"math"
"sort"
"strconv"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/txstore"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/fastsha256"
)
// Maximum tx size (in bytes). This should be the same as bitcoind's
// MAX_STANDARD_TX_SIZE.
const txMaxSize = 100000
// feeIncrement is the minimum transation fee (0.00001 BTC, measured in satoshis)
// added to transactions requiring a fee.
const feeIncrement = 1e3
type outputStatus byte
const (
statusSuccess outputStatus = iota
statusPartial
statusSplit
)
// OutBailmentID is the unique ID of a user's outbailment, comprising the
// name of the server the user connected to, and the transaction number,
// internal to that server.
type OutBailmentID string
// Ntxid is the normalized ID of a given bitcoin transaction, which is generated
// by hashing the serialized tx with blank sig scripts on all inputs.
type Ntxid string
// OutputRequest represents one of the outputs (address/amount) requested by a
// withdrawal, and includes information about the user's outbailment request.
type OutputRequest struct {
Address btcutil.Address
Amount btcutil.Amount
PkScript []byte
// The notary server that received the outbailment request.
Server string
// The server-specific transaction number for the outbailment request.
Transaction uint32
// cachedHash is used to cache the hash of the outBailmentID so it
// only has to be calculated once.
cachedHash []byte
}
// WithdrawalOutput represents a possibly fulfilled OutputRequest.
type WithdrawalOutput struct {
request OutputRequest
status outputStatus
// The outpoints that fulfill the OutputRequest. There will be more than one in case we
// need to split the request across multiple transactions.
outpoints []OutBailmentOutpoint
}
// OutBailmentOutpoint represents one of the outpoints created to fulfil an OutputRequest.
type OutBailmentOutpoint struct {
ntxid Ntxid
index uint32
amount btcutil.Amount
}
// changeAwareTx is just a wrapper around wire.MsgTx that knows about its change
// output, if any.
type changeAwareTx struct {
*wire.MsgTx
changeIdx int32 // -1 if there's no change output.
}
// WithdrawalStatus contains the details of a processed withdrawal, including
// the status of each requested output, the total amount of network fees and the
// next input and change addresses to use in a subsequent withdrawal request.
type WithdrawalStatus struct {
nextInputAddr WithdrawalAddress
nextChangeAddr ChangeAddress
fees btcutil.Amount
outputs map[OutBailmentID]*WithdrawalOutput
sigs map[Ntxid]TxSigs
transactions map[Ntxid]changeAwareTx
}
// TxSigs is list of raw signatures (one for every pubkey in the multi-sig
// script) for a given transaction input. They should match the order of pubkeys
// in the script and an empty RawSig should be used when the private key for a
// pubkey is not known.
type TxSigs [][]RawSig
// RawSig represents one of the signatures included in the unlocking script of
// inputs spending from P2SH UTXOs.
type RawSig []byte
// byAmount defines the methods needed to satisify sort.Interface to
// sort a slice of OutputRequests by their amount.
type byAmount []OutputRequest
func (u byAmount) Len() int { return len(u) }
func (u byAmount) Less(i, j int) bool { return u[i].Amount < u[j].Amount }
func (u byAmount) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
// byOutBailmentID defines the methods needed to satisify sort.Interface to sort
// a slice of OutputRequests by their outBailmentIDHash.
type byOutBailmentID []OutputRequest
func (s byOutBailmentID) Len() int { return len(s) }
func (s byOutBailmentID) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byOutBailmentID) Less(i, j int) bool {
return bytes.Compare(s[i].outBailmentIDHash(), s[j].outBailmentIDHash()) < 0
}
func (s outputStatus) String() string {
strings := map[outputStatus]string{
statusSuccess: "success",
statusPartial: "partial-",
statusSplit: "split",
}
return strings[s]
}
// Outputs returns a map of outbailment IDs to WithdrawalOutputs for all outputs
// requested in this withdrawal.
func (s *WithdrawalStatus) Outputs() map[OutBailmentID]*WithdrawalOutput {
return s.outputs
}
// Sigs returns a map of ntxids to signature lists for every input in the tx
// with that ntxid.
func (s *WithdrawalStatus) Sigs() map[Ntxid]TxSigs {
return s.sigs
}
// Fees returns the total amount of network fees included in all transactions
// generated as part of a withdrawal.
func (s *WithdrawalStatus) Fees() btcutil.Amount {
return s.fees
}
// NextInputAddr returns the votingpool address that should be used as the
// startAddress of subsequent withdrawals.
func (s *WithdrawalStatus) NextInputAddr() WithdrawalAddress {
return s.nextInputAddr
}
// NextChangeAddr returns the votingpool address that should be used as the
// changeStart of subsequent withdrawals.
func (s *WithdrawalStatus) NextChangeAddr() ChangeAddress {
return s.nextChangeAddr
}
// String makes OutputRequest satisfy the Stringer interface.
func (r OutputRequest) String() string {
return fmt.Sprintf("OutputRequest %s to send %v to %s", r.outBailmentID(), r.Amount, r.Address)
}
func (r OutputRequest) outBailmentID() OutBailmentID {
return OutBailmentID(fmt.Sprintf("%s:%d", r.Server, r.Transaction))
}
// outBailmentIDHash returns a byte slice which is used when sorting
// OutputRequests.
func (r OutputRequest) outBailmentIDHash() []byte {
if r.cachedHash != nil {
return r.cachedHash
}
str := r.Server + strconv.Itoa(int(r.Transaction))
hasher := fastsha256.New()
// hasher.Write() always returns nil as the error, so it's safe to ignore it here.
_, _ = hasher.Write([]byte(str))
id := hasher.Sum(nil)
r.cachedHash = id
return id
}
func (o *WithdrawalOutput) String() string {
return fmt.Sprintf("WithdrawalOutput for %s", o.request)
}
func (o *WithdrawalOutput) addOutpoint(outpoint OutBailmentOutpoint) {
o.outpoints = append(o.outpoints, outpoint)
}
// Status returns the status of this WithdrawalOutput.
func (o *WithdrawalOutput) Status() string {
return o.status.String()
}
// Address returns the string representation of this WithdrawalOutput's address.
func (o *WithdrawalOutput) Address() string {
return o.request.Address.String()
}
// Outpoints returns a slice containing the OutBailmentOutpoints created to
// fulfill this output.
func (o *WithdrawalOutput) Outpoints() []OutBailmentOutpoint {
return o.outpoints
}
// Amount returns the amount (in satoshis) in this OutBailmentOutpoint.
func (o OutBailmentOutpoint) Amount() btcutil.Amount {
return o.amount
}
// withdrawal holds all the state needed for Pool.Withdrawal() to do its job.
type withdrawal struct {
roundID uint32
status *WithdrawalStatus
transactions []*withdrawalTx
pendingRequests []OutputRequest
eligibleInputs []Credit
current *withdrawalTx
}
// withdrawalTxOut wraps an OutputRequest and provides a separate amount field.
// It is necessary because some requests may be partially fulfilled or split
// across transactions.
type withdrawalTxOut struct {
// Notice that in the case of a split output, the OutputRequest here will
// be a copy of the original one with the amount being the remainder of the
// originally requested amount minus the amounts fulfilled by other
// withdrawalTxOut. The original OutputRequest, if needed, can be obtained
// from WithdrawalStatus.outputs.
request OutputRequest
amount btcutil.Amount
}
// String makes withdrawalTxOut satisfy the Stringer interface.
func (o *withdrawalTxOut) String() string {
return fmt.Sprintf("withdrawalTxOut fulfilling %v of %s", o.amount, o.request)
}
func (o *withdrawalTxOut) pkScript() []byte {
return o.request.PkScript
}
// withdrawalTx represents a transaction constructed by the withdrawal process.
type withdrawalTx struct {
inputs []Credit
outputs []*withdrawalTxOut
fee btcutil.Amount
// changeOutput holds information about the change for this transaction.
changeOutput *wire.TxOut
}
func newWithdrawalTx() *withdrawalTx {
return &withdrawalTx{}
}
// ntxid returns the unique ID for this transaction.
func (tx *withdrawalTx) ntxid() Ntxid {
msgtx := tx.toMsgTx()
var empty []byte
for _, txin := range msgtx.TxIn {
txin.SignatureScript = empty
}
// Ignore the error as TxSha() can't fail.
sha, _ := msgtx.TxSha()
return Ntxid(sha.String())
}
// inputTotal returns the sum amount of all inputs in this tx.
func (tx *withdrawalTx) inputTotal() (total btcutil.Amount) {
for _, input := range tx.inputs {
total += input.Amount()
}
return total
}
// outputTotal returns the sum amount of all outputs in this tx. It does not
// include the amount for the change output, in case the tx has one.
func (tx *withdrawalTx) outputTotal() (total btcutil.Amount) {
for _, output := range tx.outputs {
total += output.amount
}
return total
}
// hasChange returns true if this transaction has a change output.
func (tx *withdrawalTx) hasChange() bool {
return tx.changeOutput != nil
}
// toMsgTx generates a btcwire.MsgTx with this tx's inputs and outputs.
func (tx *withdrawalTx) toMsgTx() *wire.MsgTx {
msgtx := wire.NewMsgTx()
for _, o := range tx.outputs {
msgtx.AddTxOut(wire.NewTxOut(int64(o.amount), o.pkScript()))
}
if tx.hasChange() {
msgtx.AddTxOut(tx.changeOutput)
}
for _, i := range tx.inputs {
msgtx.AddTxIn(wire.NewTxIn(i.OutPoint(), []byte{}))
}
return msgtx
}
// addOutput adds a new output to this transaction.
func (tx *withdrawalTx) addOutput(request OutputRequest) {
log.Debugf("Added tx output sending %s to %s", request.Amount, request.Address)
tx.outputs = append(tx.outputs, &withdrawalTxOut{request: request, amount: request.Amount})
}
// removeOutput removes the last added output and returns it.
func (tx *withdrawalTx) removeOutput() *withdrawalTxOut {
removed := tx.outputs[len(tx.outputs)-1]
tx.outputs = tx.outputs[:len(tx.outputs)-1]
log.Debugf("Removed tx output sending %s to %s", removed.amount, removed.request.Address)
return removed
}
// addInput adds a new input to this transaction.
func (tx *withdrawalTx) addInput(input Credit) {
log.Debugf("Added tx input with amount %v", input.Amount())
tx.inputs = append(tx.inputs, input)
}
// removeInput removes the last added input and returns it.
func (tx *withdrawalTx) removeInput() Credit {
removed := tx.inputs[len(tx.inputs)-1]
tx.inputs = tx.inputs[:len(tx.inputs)-1]
log.Debugf("Removed tx input with amount %v", removed.Amount())
return removed
}
// addChange adds a change output if there are any satoshis left after paying
// all the outputs and network fees. It returns true if a change output was
// added.
//
// This method must be called only once, and no extra inputs/outputs should be
// added after it's called. Also, callsites must make sure adding a change
// output won't cause the tx to exceed the size limit.
func (tx *withdrawalTx) addChange(pkScript []byte) bool {
tx.fee = calculateTxFee(tx)
change := tx.inputTotal() - tx.outputTotal() - tx.fee
log.Debugf("addChange: input total %v, output total %v, fee %v", tx.inputTotal(),
tx.outputTotal(), tx.fee)
if change > 0 {
tx.changeOutput = wire.NewTxOut(int64(change), pkScript)
log.Debugf("Added change output with amount %v", change)
}
return tx.hasChange()
}
// rollBackLastOutput will roll back the last added output and possibly remove
// inputs that are no longer needed to cover the remaining outputs. The method
// returns the removed output and the removed inputs, in the reverse order they
// were added, if any.
//
// The tx needs to have two or more outputs. The case with only one output must
// be handled separately (by the split output procedure).
func (tx *withdrawalTx) rollBackLastOutput() ([]Credit, *withdrawalTxOut, error) {
// Check precondition: At least two outputs are required in the transaction.
if len(tx.outputs) < 2 {
str := fmt.Sprintf("at least two outputs expected; got %d", len(tx.outputs))
return nil, nil, newError(ErrPreconditionNotMet, str, nil)
}
removedOutput := tx.removeOutput()
var removedInputs []Credit
// Continue until sum(in) < sum(out) + fee
for tx.inputTotal() >= tx.outputTotal()+calculateTxFee(tx) {
removedInputs = append(removedInputs, tx.removeInput())
}
// Re-add the last item from removedInputs, which is the last popped input.
tx.addInput(removedInputs[len(removedInputs)-1])
removedInputs = removedInputs[:len(removedInputs)-1]
return removedInputs, removedOutput, nil
}
func newWithdrawal(roundID uint32, requests []OutputRequest, inputs []Credit,
changeStart ChangeAddress) *withdrawal {
outputs := make(map[OutBailmentID]*WithdrawalOutput, len(requests))
for _, request := range requests {
outputs[request.outBailmentID()] = &WithdrawalOutput{request: request}
}
status := &WithdrawalStatus{
outputs: outputs,
nextChangeAddr: changeStart,
}
return &withdrawal{
roundID: roundID,
current: newWithdrawalTx(),
pendingRequests: requests,
eligibleInputs: inputs,
status: status,
}
}
// StartWithdrawal uses a fully deterministic algorithm to construct
// transactions fulfilling as many of the given output requests as possible.
// It returns a WithdrawalStatus containing the outpoints fulfilling the
// requested outputs and a map of normalized transaction IDs (ntxid) to
// signature lists (one for every private key available to this wallet) for each
// of those transaction's inputs. More details about the actual algorithm can be
// found at http://opentransactions.org/wiki/index.php/Startwithdrawal
func (p *Pool) StartWithdrawal(roundID uint32, requests []OutputRequest,
startAddress WithdrawalAddress, lastSeriesID uint32, changeStart ChangeAddress,
txStore *txstore.Store, chainHeight int32, dustThreshold btcutil.Amount) (
*WithdrawalStatus, error) {
eligible, err := p.getEligibleInputs(txStore, startAddress, lastSeriesID, dustThreshold,
chainHeight, eligibleInputMinConfirmations)
if err != nil {
return nil, err
}
w := newWithdrawal(roundID, requests, eligible, changeStart)
if err := w.fulfillRequests(); err != nil {
return nil, err
}
w.status.sigs, err = getRawSigs(w.transactions)
if err != nil {
return nil, err
}
return w.status, nil
}
// popRequest removes and returns the first request from the stack of pending
// requests.
func (w *withdrawal) popRequest() OutputRequest {
request := w.pendingRequests[0]
w.pendingRequests = w.pendingRequests[1:]
return request
}
// pushRequest adds a new request to the top of the stack of pending requests.
func (w *withdrawal) pushRequest(request OutputRequest) {
w.pendingRequests = append([]OutputRequest{request}, w.pendingRequests...)
}
// popInput removes and returns the first input from the stack of eligible
// inputs.
func (w *withdrawal) popInput() Credit {
input := w.eligibleInputs[0]
w.eligibleInputs = w.eligibleInputs[1:]
return input
}
// pushInput adds a new input to the top of the stack of eligible inputs.
// TODO: Reverse the stack semantics here as the current one generates a lot of
// extra garbage since it always creates a new single-element slice and append
// the rest of the items to it.
func (w *withdrawal) pushInput(input Credit) {
w.eligibleInputs = append([]Credit{input}, w.eligibleInputs...)
}
// If this returns it means we have added an output and the necessary inputs to fulfil that
// output plus the required fees. It also means the tx won't reach the size limit even
// after we add a change output and sign all inputs.
func (w *withdrawal) fulfillNextRequest() error {
request := w.popRequest()
output := w.status.outputs[request.outBailmentID()]
// We start with an output status of success and let the methods that deal
// with special cases change it when appropriate.
output.status = statusSuccess
w.current.addOutput(request)
if isTxTooBig(w.current) {
return w.handleOversizeTx()
}
fee := calculateTxFee(w.current)
for w.current.inputTotal() < w.current.outputTotal()+fee {
if len(w.eligibleInputs) == 0 {
log.Debug("Splitting last output because we don't have enough inputs")
if err := w.splitLastOutput(); err != nil {
return err
}
break
}
w.current.addInput(w.popInput())
fee = calculateTxFee(w.current)
if isTxTooBig(w.current) {
return w.handleOversizeTx()
}
}
return nil
}
// handleOversizeTx handles the case when a transaction has become too
// big by either rolling back an output or splitting it.
func (w *withdrawal) handleOversizeTx() error {
if len(w.current.outputs) > 1 {
log.Debug("Rolling back last output because tx got too big")
inputs, output, err := w.current.rollBackLastOutput()
if err != nil {
return newError(ErrWithdrawalProcessing, "failed to rollback last output", err)
}
for _, input := range inputs {
w.pushInput(input)
}
w.pushRequest(output.request)
} else if len(w.current.outputs) == 1 {
log.Debug("Splitting last output because tx got too big...")
w.pushInput(w.current.removeInput())
if err := w.splitLastOutput(); err != nil {
return err
}
} else {
return newError(ErrPreconditionNotMet, "Oversize tx must have at least one output", nil)
}
return w.finalizeCurrentTx()
}
// finalizeCurrentTx finalizes the transaction in w.current, moves it to the
// list of finalized transactions and replaces w.current with a new empty
// transaction.
func (w *withdrawal) finalizeCurrentTx() error {
log.Debug("Finalizing current transaction")
tx := w.current
if len(tx.outputs) == 0 {
log.Debug("Current transaction has no outputs, doing nothing")
return nil
}
pkScript, err := txscript.PayToAddrScript(w.status.nextChangeAddr.addr)
if err != nil {
return newError(ErrWithdrawalProcessing, "failed to generate pkScript for change address", err)
}
if tx.addChange(pkScript) {
var err error
w.status.nextChangeAddr, err = nextChangeAddress(w.status.nextChangeAddr)
if err != nil {
return newError(ErrWithdrawalProcessing, "failed to get next change address", err)
}
}
ntxid := tx.ntxid()
for i, txOut := range tx.outputs {
outputStatus := w.status.outputs[txOut.request.outBailmentID()]
outputStatus.addOutpoint(
OutBailmentOutpoint{ntxid: ntxid, index: uint32(i), amount: txOut.amount})
}
// Check that WithdrawalOutput entries with status==success have the sum of
// their outpoint amounts matching the requested amount.
for _, txOut := range tx.outputs {
// Look up the original request we received because txOut.request may
// represent a split request and thus have a different amount from the
// original one.
outputStatus := w.status.outputs[txOut.request.outBailmentID()]
origRequest := outputStatus.request
amtFulfilled := btcutil.Amount(0)
for _, outpoint := range outputStatus.outpoints {
amtFulfilled += outpoint.amount
}
if outputStatus.status == statusSuccess && amtFulfilled != origRequest.Amount {
msg := fmt.Sprintf("%s was not completely fulfilled; only %v fulfilled", origRequest,
amtFulfilled)
return newError(ErrWithdrawalProcessing, msg, nil)
}
}
w.transactions = append(w.transactions, tx)
w.current = newWithdrawalTx()
return nil
}
// maybeDropRequests will check the total amount we have in eligible inputs and drop
// requested outputs (in descending amount order) if we don't have enough to
// fulfill them all. For every dropped output request we update its entry in
// w.status.outputs with the status string set to statusPartial.
func (w *withdrawal) maybeDropRequests() {
inputAmount := btcutil.Amount(0)
for _, input := range w.eligibleInputs {
inputAmount += input.Amount()
}
outputAmount := btcutil.Amount(0)
for _, request := range w.pendingRequests {
outputAmount += request.Amount
}
sort.Sort(sort.Reverse(byAmount(w.pendingRequests)))
for inputAmount < outputAmount {
request := w.popRequest()
log.Infof("Not fulfilling request to send %v to %v; not enough credits.",
request.Amount, request.Address)
outputAmount -= request.Amount
w.status.outputs[request.outBailmentID()].status = statusPartial
}
}
func (w *withdrawal) fulfillRequests() error {
w.maybeDropRequests()
if len(w.pendingRequests) == 0 {
return nil
}
// Sort outputs by outBailmentID (hash(server ID, tx #))
sort.Sort(byOutBailmentID(w.pendingRequests))
for len(w.pendingRequests) > 0 {
if err := w.fulfillNextRequest(); err != nil {
return err
}
tx := w.current
if len(w.eligibleInputs) == 0 && tx.inputTotal() <= tx.outputTotal()+calculateTxFee(tx) {
// We don't have more eligible inputs and all the inputs in the
// current tx have been spent.
break
}
}
if err := w.finalizeCurrentTx(); err != nil {
return err
}
// TODO: Update w.status.nextInputAddr. Not yet implemented as in some
// conditions we need to know about un-thawed series.
w.status.transactions = make(map[Ntxid]changeAwareTx, len(w.transactions))
for _, tx := range w.transactions {
w.status.updateStatusFor(tx)
w.status.fees += tx.fee
msgtx := tx.toMsgTx()
changeIdx := -1
if tx.hasChange() {
// When withdrawalTx has a change, we know it will be the last entry
// in the generated MsgTx.
changeIdx = len(msgtx.TxOut) - 1
}
w.status.transactions[tx.ntxid()] = changeAwareTx{
MsgTx: msgtx,
changeIdx: int32(changeIdx),
}
}
return nil
}
func (w *withdrawal) splitLastOutput() error {
if len(w.current.outputs) == 0 {
return newError(ErrPreconditionNotMet,
"splitLastOutput requires current tx to have at least 1 output", nil)
}
tx := w.current
output := tx.outputs[len(tx.outputs)-1]
log.Debugf("Splitting tx output for %s", output.request)
origAmount := output.amount
spentAmount := tx.outputTotal() + calculateTxFee(tx) - output.amount
// This is how much we have left after satisfying all outputs except the last
// one. IOW, all we have left for the last output, so we set that as the
// amount of the tx's last output.
unspentAmount := tx.inputTotal() - spentAmount
output.amount = unspentAmount
log.Debugf("Updated output amount to %v", output.amount)
// Create a new OutputRequest with the amount being the difference between
// the original amount and what was left in the tx output above.
request := output.request
newRequest := OutputRequest{
Server: request.Server,
Transaction: request.Transaction,
Address: request.Address,
PkScript: request.PkScript,
Amount: origAmount - output.amount}
w.pushRequest(newRequest)
log.Debugf("Created a new pending output request with amount %v", newRequest.Amount)
w.status.outputs[request.outBailmentID()].status = statusPartial
return nil
}
func (s *WithdrawalStatus) updateStatusFor(tx *withdrawalTx) {
for _, output := range s.outputs {
if len(output.outpoints) > 1 {
output.status = statusSplit
}
// TODO: Update outputs with status=='partial-'. For this we need an API
// that gives us the amount of credits in a given series.
// http://opentransactions.org/wiki/index.php/Update_Status
}
}
// getRawSigs iterates over the inputs of each transaction given, constructing the
// raw signatures for them using the private keys available to us.
// It returns a map of ntxids to signature lists.
func getRawSigs(transactions []*withdrawalTx) (map[Ntxid]TxSigs, error) {
sigs := make(map[Ntxid]TxSigs)
for _, tx := range transactions {
txSigs := make(TxSigs, len(tx.inputs))
msgtx := tx.toMsgTx()
ntxid := tx.ntxid()
for inputIdx, input := range tx.inputs {
creditAddr := input.Address()
redeemScript := creditAddr.redeemScript()
series := creditAddr.series()
// The order of the raw signatures in the signature script must match the
// order of the public keys in the redeem script, so we sort the public keys
// here using the same API used to sort them in the redeem script and use
// series.getPrivKeyFor() to lookup the corresponding private keys.
pubKeys, err := branchOrder(series.publicKeys, creditAddr.Branch())
if err != nil {
return nil, err
}
txInSigs := make([]RawSig, len(pubKeys))
for i, pubKey := range pubKeys {
var sig RawSig
privKey, err := series.getPrivKeyFor(pubKey)
if err != nil {
return nil, err
}
if privKey != nil {
childKey, err := privKey.Child(uint32(creditAddr.Index()))
if err != nil {
return nil, newError(ErrKeyChain, "failed to derive private key", err)
}
ecPrivKey, err := childKey.ECPrivKey()
if err != nil {
return nil, newError(ErrKeyChain, "failed to obtain ECPrivKey", err)
}
log.Debugf("Generating raw sig for input %d of tx %s with privkey of %s",
inputIdx, ntxid, pubKey.String())
sig, err = txscript.RawTxInSignature(
msgtx, inputIdx, redeemScript, txscript.SigHashAll, ecPrivKey)
if err != nil {
return nil, newError(ErrRawSigning, "failed to generate raw signature", err)
}
} else {
log.Debugf("Not generating raw sig for input %d of %s because private key "+
"for %s is not available: %v", inputIdx, ntxid, pubKey.String(), err)
sig = []byte{}
}
txInSigs[i] = sig
}
txSigs[inputIdx] = txInSigs
}
sigs[ntxid] = txSigs
}
return sigs, nil
}
// SignTx signs every input of the given MsgTx by looking up (on the addr
// manager) the redeem script for each of them and constructing the signature
// script using that and the given raw signatures.
// This function must be called with the manager unlocked.
func SignTx(msgtx *wire.MsgTx, sigs TxSigs, mgr *waddrmgr.Manager, store *txstore.Store) error {
credits, err := store.FindPreviousCredits(btcutil.NewTx(msgtx))
for i, credit := range credits {
if err = signMultiSigUTXO(mgr, msgtx, i, credit.TxOut().PkScript, sigs[i]); err != nil {
return err
}
}
return nil
}
// getRedeemScript returns the redeem script for the given P2SH address. It must
// be called with the manager unlocked.
func getRedeemScript(mgr *waddrmgr.Manager, addr *btcutil.AddressScriptHash) ([]byte, error) {
address, err := mgr.Address(addr)
if err != nil {
return nil, err
}
return address.(waddrmgr.ManagedScriptAddress).Script()
}
// signMultiSigUTXO signs the P2SH UTXO with the given index by constructing a
// script containing all given signatures plus the redeem (multi-sig) script. The
// redeem script is obtained by looking up the address of the given P2SH pkScript
// on the address manager.
// The order of the signatures must match that of the public keys in the multi-sig
// script as OP_CHECKMULTISIG expects that.
// This function must be called with the manager unlocked.
func signMultiSigUTXO(mgr *waddrmgr.Manager, tx *wire.MsgTx, idx int, pkScript []byte, sigs []RawSig) error {
class, addresses, _, err := txscript.ExtractPkScriptAddrs(pkScript, mgr.ChainParams())
if err != nil {
return newError(ErrTxSigning, "unparseable pkScript", err)
}
if class != txscript.ScriptHashTy {
return newError(ErrTxSigning, fmt.Sprintf("pkScript is not P2SH: %s", class), nil)
}
redeemScript, err := getRedeemScript(mgr, addresses[0].(*btcutil.AddressScriptHash))
if err != nil {
return newError(ErrTxSigning, "unable to retrieve redeem script", err)
}
class, _, nRequired, err := txscript.ExtractPkScriptAddrs(redeemScript, mgr.ChainParams())
if err != nil {
return newError(ErrTxSigning, "unparseable redeem script", err)
}
if class != txscript.MultiSigTy {
return newError(ErrTxSigning, fmt.Sprintf("redeem script is not multi-sig: %v", class), nil)
}
if len(sigs) < nRequired {
errStr := fmt.Sprintf("not enough signatures; need %d but got only %d", nRequired,
len(sigs))
return newError(ErrTxSigning, errStr, nil)
}
// Construct the unlocking script.
// Start with an OP_0 because of the bug in bitcoind, then add nRequired signatures.
unlockingScript := txscript.NewScriptBuilder().AddOp(txscript.OP_FALSE)
for _, sig := range sigs[:nRequired] {
unlockingScript.AddData(sig)
}
// Combine the redeem script and the unlocking script to get the actual signature script.
sigScript := unlockingScript.AddData(redeemScript)
script, err := sigScript.Script()
if err != nil {
return newError(ErrTxSigning, "error building sigscript", err)
}
tx.TxIn[idx].SignatureScript = script
if err := validateSigScript(tx, idx, pkScript); err != nil {
return err
}
return nil
}
// validateSigScripts executes the signature script of the tx input with the
// given index, returning an error if it fails.
func validateSigScript(msgtx *wire.MsgTx, idx int, pkScript []byte) error {
txIn := msgtx.TxIn[idx]
engine, err := txscript.NewScript(
txIn.SignatureScript, pkScript, idx, msgtx, txscript.StandardVerifyFlags)
if err != nil {
return newError(ErrTxSigning, "cannot create script engine", err)
}
if err = engine.Execute(); err != nil {
return newError(ErrTxSigning, "cannot validate tx signature", err)
}
return nil
}
// calculateTxFee calculates the expected network fees for a given tx. We use
// a variable instead of a function so that it can be replaced in tests.
var calculateTxFee = func(tx *withdrawalTx) btcutil.Amount {
return btcutil.Amount(1+calculateTxSize(tx)/1000) * feeIncrement
}
// isTxTooBig returns true if the size (in bytes) of the given tx is greater
// than or equal to txMaxSize. It is defined as a variable so it can be
// replaced for testing purposes.
var isTxTooBig = func(tx *withdrawalTx) bool {
// In bitcoind a tx is considered standard only if smaller than
// MAX_STANDARD_TX_SIZE; that's why we consider anything >= txMaxSize to
// be too big.
return calculateTxSize(tx) >= txMaxSize
}
// calculateTxSize returns an estimate of the serialized size (in bytes) of the
// given transaction. It assumes all tx inputs are P2SH multi-sig. We use a
// variable instead of a function so that it can be replaced in tests.
var calculateTxSize = func(tx *withdrawalTx) int {
msgtx := tx.toMsgTx()
// Assume that there will always be a change output, for simplicity. We
// simulate that by simply copying the first output as all we care about is
// the size of its serialized form, which should be the same for all of them
// as they're either P2PKH or P2SH..
if !tx.hasChange() {
msgtx.AddTxOut(msgtx.TxOut[0])
}
// Craft a SignatureScript with dummy signatures for every input in this tx
// so that we can use msgtx.SerializeSize() to get its size and don't need
// to rely on estimations.
for i, txin := range msgtx.TxIn {
// 1 byte for the OP_FALSE opcode, then 73+1 bytes for each signature
// with their OP_DATA opcode and finally the redeem script + 1 byte
// for its OP_PUSHDATA opcode and N bytes for the redeem script's size.
// Notice that we use 73 as the signature length as that's the maximum
// length they may have:
// https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm
addr := tx.inputs[i].Address()
redeemScriptLen := len(addr.redeemScript())
n := wire.VarIntSerializeSize(uint64(redeemScriptLen))
sigScriptLen := 1 + (74 * int(addr.series().reqSigs)) + redeemScriptLen + 1 + n
txin.SignatureScript = bytes.Repeat([]byte{1}, sigScriptLen)
}
return msgtx.SerializeSize()
}
func nextChangeAddress(a ChangeAddress) (ChangeAddress, error) {
index := a.index
seriesID := a.seriesID
if index == math.MaxUint32 {
index = 0
seriesID++
} else {
index++
}
addr, err := a.pool.ChangeAddress(seriesID, index)
return *addr, err
}

View file

@ -0,0 +1,133 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool_test
import (
"bytes"
"testing"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
vp "github.com/btcsuite/btcwallet/votingpool"
)
func TestStartWithdrawal(t *testing.T) {
tearDown, pool, store := vp.TstCreatePoolAndTxStore(t)
defer tearDown()
mgr := pool.Manager()
masters := []*hdkeychain.ExtendedKey{
vp.TstCreateMasterKey(t, bytes.Repeat([]byte{0x00, 0x01}, 16)),
vp.TstCreateMasterKey(t, bytes.Repeat([]byte{0x02, 0x01}, 16)),
vp.TstCreateMasterKey(t, bytes.Repeat([]byte{0x03, 0x01}, 16))}
def := vp.TstCreateSeriesDef(t, pool, 2, masters)
vp.TstCreateSeries(t, pool, []vp.TstSeriesDef{def})
// Create eligible inputs and the list of outputs we need to fulfil.
vp.TstCreateCreditsOnSeries(t, pool, def.SeriesID, []int64{5e6, 4e6}, store)
address1 := "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6"
address2 := "3PbExiaztsSYgh6zeMswC49hLUwhTQ86XG"
requests := []vp.OutputRequest{
vp.TstNewOutputRequest(t, 1, address1, 4e6, mgr.ChainParams()),
vp.TstNewOutputRequest(t, 2, address2, 1e6, mgr.ChainParams()),
}
changeStart := vp.TstNewChangeAddress(t, pool, def.SeriesID, 0)
startAddr := vp.TstNewWithdrawalAddress(t, pool, def.SeriesID, 0, 0)
lastSeriesID := def.SeriesID
dustThreshold := btcutil.Amount(1e4)
currentBlock := int32(vp.TstInputsBlock + vp.TstEligibleInputMinConfirmations + 1)
var status *vp.WithdrawalStatus
var err error
vp.TstRunWithManagerUnlocked(t, mgr, func() {
status, err = pool.StartWithdrawal(0, requests, *startAddr, lastSeriesID, *changeStart,
store, currentBlock, dustThreshold)
})
if err != nil {
t.Fatal(err)
}
// Check that all outputs were successfully fulfilled.
checkWithdrawalOutputs(t, status, map[string]btcutil.Amount{address1: 4e6, address2: 1e6})
if status.Fees() != btcutil.Amount(1e3) {
t.Fatalf("Wrong amount for fees; got %v, want %v", status.Fees(), btcutil.Amount(1e3))
}
// This withdrawal generated a single transaction with just one change
// output, so the next change address will be on the same series with the
// index incremented by 1.
nextChangeAddr := status.NextChangeAddr()
if nextChangeAddr.SeriesID() != changeStart.SeriesID() {
t.Fatalf("Wrong nextChangeStart series; got %d, want %d", nextChangeAddr.SeriesID(),
changeStart.SeriesID())
}
if nextChangeAddr.Index() != changeStart.Index()+1 {
t.Fatalf("Wrong nextChangeStart index; got %d, want %d", nextChangeAddr.Index(),
changeStart.Index()+1)
}
// NOTE: The ntxid is deterministic so we hardcode it here, but if the test
// or the code is changed in a way that causes the generated transaction to
// change (e.g. different inputs/outputs), the ntxid will change too and
// this will have to be updated.
ntxid := vp.Ntxid("eb753083db55bd0ad2eb184bfd196a7ea8b90eaa000d9293e892999695af2519")
txSigs := status.Sigs()[ntxid]
// Finally we use SignTx() to construct the SignatureScripts (using the raw
// signatures). Must unlock the manager as signing involves looking up the
// redeem script, which is stored encrypted.
msgtx := status.TstGetMsgTx(ntxid)
vp.TstRunWithManagerUnlocked(t, mgr, func() {
if err = vp.SignTx(msgtx, txSigs, mgr, store); err != nil {
t.Fatal(err)
}
})
}
func checkWithdrawalOutputs(
t *testing.T, wStatus *vp.WithdrawalStatus, amounts map[string]btcutil.Amount) {
fulfilled := wStatus.Outputs()
if len(fulfilled) != 2 {
t.Fatalf("Unexpected number of outputs in WithdrawalStatus; got %d, want %d",
len(fulfilled), 2)
}
for _, output := range fulfilled {
addr := output.Address()
amount, ok := amounts[addr]
if !ok {
t.Fatalf("Unexpected output addr: %s", addr)
}
status := output.Status()
if status != "success" {
t.Fatalf(
"Unexpected status for output %v; got '%s', want 'success'", output, status)
}
outpoints := output.Outpoints()
if len(outpoints) != 1 {
t.Fatalf(
"Unexpected number of outpoints for output %v; got %d, want 1", output,
len(outpoints))
}
gotAmount := outpoints[0].Amount()
if gotAmount != amount {
t.Fatalf("Unexpected amount for output %v; got %v, want %v", output, gotAmount, amount)
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -154,26 +154,6 @@ var errorCodeStrings = map[ErrorCode]string{
ErrTooManyAddresses: "ErrTooManyAddresses",
ErrWrongPassphrase: "ErrWrongPassphrase",
ErrWrongNet: "ErrWrongNet",
// The following error codes are defined in pool_error.go.
ErrSeriesStorage: "ErrSeriesStorage",
ErrSeriesVersion: "ErrSeriesVersion",
ErrSeriesNotExists: "ErrSeriesNotExists",
ErrSeriesAlreadyExists: "ErrSeriesAlreadyExists",
ErrSeriesAlreadyEmpowered: "ErrSeriesAlreadyEmpowered",
ErrKeyIsPrivate: "ErrKeyIsPrivate",
ErrKeyIsPublic: "ErrKeyIsPublic",
ErrKeyNeuter: "ErrKeyNeuter",
ErrKeyMismatch: "ErrKeyMismatch",
ErrKeysPrivatePublicMismatch: "ErrKeysPrivatePublicMismatch",
ErrKeyDuplicate: "ErrKeyDuplicate",
ErrTooFewPublicKeys: "ErrTooFewPublicKeys",
ErrVotingPoolAlreadyExists: "ErrVotingPoolAlreadyExists",
ErrVotingPoolNotExists: "ErrVotingPoolNotExists",
ErrScriptCreation: "ErrScriptCreation",
ErrTooManyReqSignatures: "ErrTooManyReqSignatures",
ErrInvalidBranch: "ErrInvalidBranch",
ErrInvalidValue: "ErrInvalidValue",
}
// String returns the ErrorCode as a human-readable name.

View file

@ -48,22 +48,6 @@ func TestErrorCodeStringer(t *testing.T) {
{waddrmgr.ErrTooManyAddresses, "ErrTooManyAddresses"},
{waddrmgr.ErrWrongPassphrase, "ErrWrongPassphrase"},
{waddrmgr.ErrWrongNet, "ErrWrongNet"},
// The following error codes are defined in pool_error.go.
{waddrmgr.ErrSeriesStorage, "ErrSeriesStorage"},
{waddrmgr.ErrSeriesNotExists, "ErrSeriesNotExists"},
{waddrmgr.ErrSeriesAlreadyExists, "ErrSeriesAlreadyExists"},
{waddrmgr.ErrSeriesAlreadyEmpowered, "ErrSeriesAlreadyEmpowered"},
{waddrmgr.ErrKeyIsPrivate, "ErrKeyIsPrivate"},
{waddrmgr.ErrKeyNeuter, "ErrKeyNeuter"},
{waddrmgr.ErrKeyMismatch, "ErrKeyMismatch"},
{waddrmgr.ErrKeysPrivatePublicMismatch, "ErrKeysPrivatePublicMismatch"},
{waddrmgr.ErrKeyDuplicate, "ErrKeyDuplicate"},
{waddrmgr.ErrTooFewPublicKeys, "ErrTooFewPublicKeys"},
{waddrmgr.ErrVotingPoolNotExists, "ErrVotingPoolNotExists"},
{waddrmgr.ErrScriptCreation, "ErrScriptCreation"},
{waddrmgr.ErrTooManyReqSignatures, "ErrTooManyReqSignatures"},
{0xffff, "Unknown ErrorCode (65535)"},
}
t.Logf("Running %d tests", len(tests))

View file

@ -1,92 +0,0 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* 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
)