mirror of
https://github.com/LBRYFoundation/lbcwallet.git
synced 2025-09-10 12:39:47 +00:00
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:
parent
68a9168d9e
commit
2181f4859d
24 changed files with 5550 additions and 1179 deletions
|
@ -628,7 +628,7 @@ func (t *TxRecord) AddDebits() (Debits, error) {
|
||||||
defer t.s.mtx.Unlock()
|
defer t.s.mtx.Unlock()
|
||||||
|
|
||||||
if t.debits == nil {
|
if t.debits == nil {
|
||||||
spent, err := t.s.findPreviousCredits(t.Tx())
|
spent, err := t.s.FindPreviousCredits(t.Tx())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Debits{}, err
|
return Debits{}, err
|
||||||
}
|
}
|
||||||
|
@ -654,9 +654,9 @@ func (t *TxRecord) AddDebits() (Debits, error) {
|
||||||
return d, nil
|
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.
|
// for tx.
|
||||||
func (s *Store) findPreviousCredits(tx *btcutil.Tx) ([]Credit, error) {
|
func (s *Store) FindPreviousCredits(tx *btcutil.Tx) ([]Credit, error) {
|
||||||
type createdCredit struct {
|
type createdCredit struct {
|
||||||
credit Credit
|
credit Credit
|
||||||
err error
|
err error
|
||||||
|
|
88
votingpool/common_test.go
Normal file
88
votingpool/common_test.go
Normal 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 }
|
||||||
|
}
|
155
votingpool/db.go
155
votingpool/db.go
|
@ -22,7 +22,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/btcsuite/btcwallet/snacl"
|
"github.com/btcsuite/btcwallet/snacl"
|
||||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
|
||||||
"github.com/btcsuite/btcwallet/walletdb"
|
"github.com/btcsuite/btcwallet/walletdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,6 +43,8 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
usedAddrsBucketName = []byte("usedaddrs")
|
||||||
|
seriesBucketName = []byte("series")
|
||||||
// string representing a non-existent private key
|
// string representing a non-existent private key
|
||||||
seriesNullPrivKey = [seriesKeyLength]byte{}
|
seriesNullPrivKey = [seriesKeyLength]byte{}
|
||||||
)
|
)
|
||||||
|
@ -56,29 +57,103 @@ type dbSeriesRow struct {
|
||||||
privKeysEncrypted [][]byte
|
privKeysEncrypted [][]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// putPool stores a voting pool in the database, creating a bucket named
|
// getUsedAddrBucketID returns the used addresses bucket ID for the given series
|
||||||
// after the voting pool id.
|
// and branch. It has the form seriesID:branch.
|
||||||
func putPool(tx walletdb.Tx, votingPoolID []byte) error {
|
func getUsedAddrBucketID(seriesID uint32, branch Branch) []byte {
|
||||||
_, err := tx.RootBucket().CreateBucket(votingPoolID)
|
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 {
|
if err != nil {
|
||||||
str := fmt.Sprintf("cannot create voting pool %v", votingPoolID)
|
return newError(ErrDatabase, "failed to store used address hash", err)
|
||||||
return managerError(waddrmgr.ErrDatabase, str, 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadAllSeries returns a map of all the series stored inside a voting pool
|
// loadAllSeries returns a map of all the series stored inside a voting pool
|
||||||
// bucket, keyed by id.
|
// bucket, keyed by id.
|
||||||
func loadAllSeries(tx walletdb.Tx, votingPoolID []byte) (map[uint32]*dbSeriesRow, error) {
|
func loadAllSeries(tx walletdb.Tx, poolID []byte) (map[uint32]*dbSeriesRow, error) {
|
||||||
bucket := tx.RootBucket().Bucket(votingPoolID)
|
bucket := tx.RootBucket().Bucket(poolID).Bucket(seriesBucketName)
|
||||||
allSeries := make(map[uint32]*dbSeriesRow)
|
allSeries := make(map[uint32]*dbSeriesRow)
|
||||||
err := bucket.ForEach(
|
err := bucket.ForEach(
|
||||||
func(k, v []byte) error {
|
func(k, v []byte) error {
|
||||||
seriesID := bytesToUint32(k)
|
seriesID := bytesToUint32(k)
|
||||||
series, err := deserializeSeriesRow(v)
|
series, err := deserializeSeriesRow(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("cannot deserialize series %v", v)
|
return err
|
||||||
return managerError(waddrmgr.ErrSeriesStorage, str, err)
|
|
||||||
}
|
}
|
||||||
allSeries[seriesID] = series
|
allSeries[seriesID] = series
|
||||||
return nil
|
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
|
// existsPool checks the existence of a bucket named after the given
|
||||||
// voting pool id.
|
// voting pool id.
|
||||||
func existsPool(tx walletdb.Tx, votingPoolID []byte) bool {
|
func existsPool(tx walletdb.Tx, poolID []byte) bool {
|
||||||
bucket := tx.RootBucket().Bucket(votingPoolID)
|
bucket := tx.RootBucket().Bucket(poolID)
|
||||||
return bucket != nil
|
return bucket != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// putSeries stores the given series inside a voting pool bucket named after
|
// putSeries stores the given series inside a voting pool bucket named after
|
||||||
// votingPoolID. The voting pool bucket does not need to be created beforehand.
|
// poolID. 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 {
|
func putSeries(tx walletdb.Tx, poolID []byte, version, ID uint32, active bool, reqSigs uint32, pubKeysEncrypted, privKeysEncrypted [][]byte) error {
|
||||||
row := &dbSeriesRow{
|
row := &dbSeriesRow{
|
||||||
version: version,
|
version: version,
|
||||||
active: active,
|
active: active,
|
||||||
|
@ -106,27 +181,27 @@ func putSeries(tx walletdb.Tx, votingPoolID []byte, version, ID uint32, active b
|
||||||
pubKeysEncrypted: pubKeysEncrypted,
|
pubKeysEncrypted: pubKeysEncrypted,
|
||||||
privKeysEncrypted: privKeysEncrypted,
|
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
|
// 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.
|
// beforehand.
|
||||||
func putSeriesRow(tx walletdb.Tx, votingPoolID []byte, ID uint32, row *dbSeriesRow) error {
|
func putSeriesRow(tx walletdb.Tx, poolID []byte, ID uint32, row *dbSeriesRow) error {
|
||||||
bucket, err := tx.RootBucket().CreateBucketIfNotExists(votingPoolID)
|
bucket, err := tx.RootBucket().CreateBucketIfNotExists(poolID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("cannot create bucket %v", votingPoolID)
|
str := fmt.Sprintf("cannot create bucket %v", poolID)
|
||||||
return managerError(waddrmgr.ErrDatabase, str, err)
|
return newError(ErrDatabase, str, err)
|
||||||
}
|
}
|
||||||
|
bucket = bucket.Bucket(seriesBucketName)
|
||||||
serialized, err := serializeSeriesRow(row)
|
serialized, err := serializeSeriesRow(row)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("cannot serialize series %v", row)
|
return err
|
||||||
return managerError(waddrmgr.ErrSeriesStorage, str, err)
|
|
||||||
}
|
}
|
||||||
err = bucket.Put(uint32ToBytes(ID), serialized)
|
err = bucket.Put(uint32ToBytes(ID), serialized)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("cannot put series %v into bucket %v", serialized, votingPoolID)
|
str := fmt.Sprintf("cannot put series %v into bucket %v", serialized, poolID)
|
||||||
return managerError(waddrmgr.ErrSeriesStorage, str, err)
|
return newError(ErrDatabase, str, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -142,17 +217,15 @@ func deserializeSeriesRow(serializedSeries []byte) (*dbSeriesRow, error) {
|
||||||
// Given the above, the length of the serialized series should be
|
// Given the above, the length of the serialized series should be
|
||||||
// at minimum the length of the constants.
|
// at minimum the length of the constants.
|
||||||
if len(serializedSeries) < seriesMinSerial {
|
if len(serializedSeries) < seriesMinSerial {
|
||||||
str := fmt.Sprintf("serialized series is too short: %v",
|
str := fmt.Sprintf("serialized series is too short: %v", serializedSeries)
|
||||||
serializedSeries)
|
return nil, newError(ErrSeriesSerialization, str, nil)
|
||||||
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maximum number of public keys is 15 and the same for public keys
|
// Maximum number of public keys is 15 and the same for public keys
|
||||||
// this gives us an upper bound.
|
// this gives us an upper bound.
|
||||||
if len(serializedSeries) > seriesMaxSerial {
|
if len(serializedSeries) > seriesMaxSerial {
|
||||||
str := fmt.Sprintf("serialized series is too long: %v",
|
str := fmt.Sprintf("serialized series is too long: %v", serializedSeries)
|
||||||
serializedSeries)
|
return nil, newError(ErrSeriesSerialization, str, nil)
|
||||||
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keeps track of the position of the next set of bytes to deserialize.
|
// 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 {
|
if row.version > seriesMaxVersion {
|
||||||
str := fmt.Sprintf("deserialization supports up to version %v not %v",
|
str := fmt.Sprintf("deserialization supports up to version %v not %v",
|
||||||
seriesMaxVersion, row.version)
|
seriesMaxVersion, row.version)
|
||||||
return nil, managerError(waddrmgr.ErrSeriesVersion, str, nil)
|
return nil, newError(ErrSeriesVersion, str, nil)
|
||||||
}
|
}
|
||||||
current += 4
|
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.
|
// Check to see if we have the right number of bytes to consume.
|
||||||
if len(serializedSeries) < current+int(nKeys)*seriesKeyLength*2 {
|
if len(serializedSeries) < current+int(nKeys)*seriesKeyLength*2 {
|
||||||
str := fmt.Sprintf("serialized series has not enough data: %v",
|
str := fmt.Sprintf("serialized series has not enough data: %v", serializedSeries)
|
||||||
serializedSeries)
|
return nil, newError(ErrSeriesSerialization, str, nil)
|
||||||
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
|
|
||||||
} else if len(serializedSeries) > current+int(nKeys)*seriesKeyLength*2 {
|
} else if len(serializedSeries) > current+int(nKeys)*seriesKeyLength*2 {
|
||||||
str := fmt.Sprintf("serialized series has too much data: %v",
|
str := fmt.Sprintf("serialized series has too much data: %v", serializedSeries)
|
||||||
serializedSeries)
|
return nil, newError(ErrSeriesSerialization, str, nil)
|
||||||
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deserialize the pubkey/privkey pairs.
|
// Deserialize the pubkey/privkey pairs.
|
||||||
|
@ -219,13 +290,13 @@ func serializeSeriesRow(row *dbSeriesRow) ([]byte, error) {
|
||||||
len(row.pubKeysEncrypted) != len(row.privKeysEncrypted) {
|
len(row.pubKeysEncrypted) != len(row.privKeysEncrypted) {
|
||||||
str := fmt.Sprintf("different # of pub (%v) and priv (%v) keys",
|
str := fmt.Sprintf("different # of pub (%v) and priv (%v) keys",
|
||||||
len(row.pubKeysEncrypted), len(row.privKeysEncrypted))
|
len(row.pubKeysEncrypted), len(row.privKeysEncrypted))
|
||||||
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
|
return nil, newError(ErrSeriesSerialization, str, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if row.version > seriesMaxVersion {
|
if row.version > seriesMaxVersion {
|
||||||
str := fmt.Sprintf("serialization supports up to version %v, not %v",
|
str := fmt.Sprintf("serialization supports up to version %v, not %v",
|
||||||
seriesMaxVersion, row.version)
|
seriesMaxVersion, row.version)
|
||||||
return nil, managerError(waddrmgr.ErrSeriesVersion, str, nil)
|
return nil, newError(ErrSeriesVersion, str, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
serialized := make([]byte, 0, serializedLen)
|
serialized := make([]byte, 0, serializedLen)
|
||||||
|
@ -245,7 +316,7 @@ func serializeSeriesRow(row *dbSeriesRow) ([]byte, error) {
|
||||||
if len(pubKeyEncrypted) != seriesKeyLength {
|
if len(pubKeyEncrypted) != seriesKeyLength {
|
||||||
str := fmt.Sprintf("wrong length of Encrypted Public Key: %v",
|
str := fmt.Sprintf("wrong length of Encrypted Public Key: %v",
|
||||||
pubKeyEncrypted)
|
pubKeyEncrypted)
|
||||||
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
|
return nil, newError(ErrSeriesSerialization, str, nil)
|
||||||
}
|
}
|
||||||
serialized = append(serialized, pubKeyEncrypted...)
|
serialized = append(serialized, pubKeyEncrypted...)
|
||||||
|
|
||||||
|
@ -260,7 +331,7 @@ func serializeSeriesRow(row *dbSeriesRow) ([]byte, error) {
|
||||||
} else if len(privKeyEncrypted) != seriesKeyLength {
|
} else if len(privKeyEncrypted) != seriesKeyLength {
|
||||||
str := fmt.Sprintf("wrong length of Encrypted Private Key: %v",
|
str := fmt.Sprintf("wrong length of Encrypted Private Key: %v",
|
||||||
len(privKeyEncrypted))
|
len(privKeyEncrypted))
|
||||||
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
|
return nil, newError(ErrSeriesSerialization, str, nil)
|
||||||
} else {
|
} else {
|
||||||
serialized = append(serialized, privKeyEncrypted...)
|
serialized = append(serialized, privKeyEncrypted...)
|
||||||
}
|
}
|
||||||
|
|
82
votingpool/db_wb_test.go
Normal file
82
votingpool/db_wb_test.go
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,19 +20,24 @@ Package votingpool provides voting pool functionality for btcwallet.
|
||||||
Overview
|
Overview
|
||||||
|
|
||||||
The purpose of the voting pool package is to make it possible to store
|
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
|
bitcoins using m-of-n multisig transactions. A pool can have multiple
|
||||||
holds one of the n private keys needed to create a transaction and can
|
series, each of them with a set of pubkeys (one for each of the members
|
||||||
only create transactions that can spend the bitcoins if m - 1 other
|
in that pool's series) and the minimum number of required signatures (m)
|
||||||
members of the pool agree to it.
|
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
|
More details about voting pools as well as some of its use cases can
|
||||||
instances of the waddrgmgr.Manager structure.
|
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
|
Creating a voting pool
|
||||||
|
|
||||||
A voting pool is created via the Create function. This function
|
A voting pool is created via the Create function. This function
|
||||||
accepts a database namespace which will be used to store all
|
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
|
Loading an existing pool
|
||||||
|
|
||||||
|
@ -43,28 +48,52 @@ Creating a series
|
||||||
|
|
||||||
A series can be created via the CreateSeries method, which accepts a
|
A series can be created via the CreateSeries method, which accepts a
|
||||||
version number, a series identifier, a number of required signatures
|
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
|
Deposit Addresses
|
||||||
|
|
||||||
A deposit address can be created via the DepositScriptAddress
|
A deposit address can be created via the DepositScriptAddress
|
||||||
method, which based on a seriesID a branch number and an index
|
method, which returns a series-specific P2SH address from the multi-sig
|
||||||
creates a pay-to-script-hash address, where the script is a multisig
|
script constructed with the index-th child of the series' public keys and
|
||||||
script. The public keys used as inputs for generating the address are
|
sorted according to the given branch. The procedure to construct multi-sig
|
||||||
generated from the public keys passed to CreateSeries. In [1] the
|
deposit addresses is described in detail at
|
||||||
generated public keys correspend to the lowest level or the
|
http://opentransactions.org/wiki/index.php/Deposit_Address_(voting_pools)
|
||||||
'address_index' in the hierarchy.
|
|
||||||
|
|
||||||
Replacing a series
|
Replacing a series
|
||||||
|
|
||||||
A series can be replaced via the ReplaceSeries method. It accepts
|
A series can be replaced via the ReplaceSeries method. It accepts
|
||||||
the same parameters as the CreateSeries method.
|
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
|
package votingpool
|
||||||
|
|
219
votingpool/error.go
Normal file
219
votingpool/error.go
Normal 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
82
votingpool/error_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,69 +17,92 @@
|
||||||
package votingpool_test
|
package votingpool_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
"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/votingpool"
|
||||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||||
"github.com/btcsuite/btcwallet/walletdb"
|
"github.com/btcsuite/btcwallet/walletdb"
|
||||||
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
|
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Example_basic() {
|
var (
|
||||||
// This example demonstrates how to create a voting pool, create a
|
pubPassphrase = []byte("pubPassphrase")
|
||||||
// series, get a deposit address from a series and lastly how to
|
privPassphrase = []byte("privPassphrase")
|
||||||
// replace a series.
|
)
|
||||||
|
|
||||||
// Create a new wallet DB.
|
func ExampleCreate() {
|
||||||
dir, err := ioutil.TempDir("", "pool_test")
|
// Create a new walletdb.DB. See the walletdb docs for instructions on how
|
||||||
|
// to do that.
|
||||||
|
db, dbTearDown, err := createWalletDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to create db dir: %v\n", err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
db, err := walletdb.Create("bdb", filepath.Join(dir, "wallet.db"))
|
defer dbTearDown()
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Failed to create wallet DB: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(dir)
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Create a new walletdb namespace for the address manager.
|
// Create a new walletdb namespace for the address manager.
|
||||||
mgrNamespace, err := db.Namespace([]byte("waddrmgr"))
|
mgrNamespace, err := db.Namespace([]byte("waddrmgr"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to create addr manager DB namespace: %v\n", err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the address manager
|
// Create the address manager.
|
||||||
mgr, err := waddrmgr.Create(mgrNamespace, seed, pubPassphrase, privPassphrase,
|
seed := bytes.Repeat([]byte{0x2a, 0x64, 0xdf, 0x08}, 8)
|
||||||
&chaincfg.MainNetParams, nil)
|
var fastScrypt = &waddrmgr.Options{ScryptN: 16, ScryptR: 8, ScryptP: 1}
|
||||||
|
mgr, err := waddrmgr.Create(
|
||||||
|
mgrNamespace, seed, pubPassphrase, privPassphrase, &chaincfg.MainNetParams, fastScrypt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to create addr manager: %v\n", err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer mgr.Close()
|
|
||||||
|
|
||||||
// Create a walletdb for votingpools.
|
// Create a walletdb namespace for votingpools.
|
||||||
vpNamespace, err := db.Namespace([]byte("votingpool"))
|
vpNamespace, err := db.Namespace([]byte("votingpool"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to create VotingPool DB namespace: %v\n", err)
|
fmt.Println(err)
|
||||||
return
|
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.
|
// Create the voting pool.
|
||||||
pool, err := votingpool.Create(vpNamespace, mgr, []byte{0x00})
|
pool, err := votingpool.Create(vpNamespace, mgr, []byte{0x00})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Voting Pool creation failed: %v\n", err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a 2-of-3 series.
|
// Create a 2-of-3 series.
|
||||||
apiVersion := uint32(1)
|
|
||||||
seriesID := uint32(1)
|
seriesID := uint32(1)
|
||||||
requiredSignatures := uint32(2)
|
requiredSignatures := uint32(2)
|
||||||
pubKeys := []string{
|
pubKeys := []string{
|
||||||
|
@ -87,39 +110,220 @@ func Example_basic() {
|
||||||
"xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9",
|
"xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9",
|
||||||
"xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh",
|
"xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh",
|
||||||
}
|
}
|
||||||
err = pool.CreateSeries(apiVersion, seriesID, requiredSignatures, pubKeys)
|
err = pool.CreateSeries(votingpool.CurrentVersion, seriesID, requiredSignatures, pubKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Cannot create series: %v\n", err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a deposit address.
|
// Create a deposit address.
|
||||||
branch := uint32(0) // The change branch
|
addr, err := pool.DepositScriptAddress(seriesID, votingpool.Branch(0), votingpool.Index(1))
|
||||||
index := uint32(1)
|
|
||||||
addr, err := pool.DepositScriptAddress(seriesID, branch, index)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("DepositScriptAddress failed for series: %d, branch: %d, index: %d\n",
|
fmt.Println(err)
|
||||||
seriesID, branch, index)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Println("Generated deposit address:", addr.EncodeAddress())
|
fmt.Println("Generated deposit address:", addr.EncodeAddress())
|
||||||
|
|
||||||
// Replace the existing series with a 3-of-5 series.
|
// Output:
|
||||||
pubKeys = []string{
|
// Generated deposit address: 3QTzpc9d3tTbNLJLB7xwt87nWM38boAhAw
|
||||||
"xpub661MyMwAqRbcFQfXKHwz8ZbTtePwAKu8pmGYyVrWEM96DYUTWDYipMnHrFcemZHn13jcRMfsNU3UWQUudiaE7mhkWCHGFRMavF167DQM4Va",
|
}
|
||||||
"xpub661MyMwAqRbcGnTEXx3ehjx8EiqQGnL4uhwZw3ZxvZAa2E6E4YVAp63UoVtvm2vMDDF8BdPpcarcf7PWcEKvzHhxzAYw1zG23C2egeh82AR",
|
|
||||||
"xpub661MyMwAqRbcG83KwFyr1RVrNUmqVwYxV6nzxbqoRTNc8fRnWxq1yQiTBifTHhevcEM9ucZ1TqFS7Kv17Gd81cesv6RDrrvYS9SLPjPXhV5",
|
// This example demonstrates how to empower a series by loading the private
|
||||||
"xpub661MyMwAqRbcFGJbLPhMjtpC1XntFpg6jjQWjr6yXN8b9wfS1RiU5EhJt5L7qoFuidYawc3XJoLjT2PcjVpXryS3hn1WmSPCyvQDNuKsfgM",
|
// key for one of the series' public keys.
|
||||||
"xpub661MyMwAqRbcGJDX4GYocn7qCzvMJwNisxpzkYZAakcvXtWV6CanXuz9xdfe5kTptFMJ4hDt2iTiT11zyN14u8R5zLvoZ1gnEVqNLxp1r3v",
|
func Example_empowerSeries() {
|
||||||
"xpub661MyMwAqRbcG13FtwvZVaA15pTerP4JdAGvytPykqDr2fKXePqw3wLhCALPAixsE176jFkc2ac9K3tnF4KwaTRKUqFF5apWD6XL9LHCu7E",
|
// Create the address manager and votingpool DB namespace. See the example
|
||||||
}
|
// for the Create() function for more info on how this is done.
|
||||||
requiredSignatures = 3
|
mgr, vpNamespace, tearDownFunc, err := exampleCreateMgrAndDBNamespace()
|
||||||
err = pool.ReplaceSeries(apiVersion, seriesID, requiredSignatures, pubKeys)
|
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output:
|
// 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
388
votingpool/factory_test.go
Normal 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 }
|
||||||
|
}
|
287
votingpool/input_selection.go
Normal file
287
votingpool/input_selection.go
Normal 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
|
||||||
|
}
|
410
votingpool/input_selection_wb_test.go
Normal file
410
votingpool/input_selection_wb_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,11 +17,16 @@
|
||||||
package votingpool
|
package votingpool
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcutil/hdkeychain"
|
"github.com/btcsuite/btcutil/hdkeychain"
|
||||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||||
"github.com/btcsuite/btcwallet/walletdb"
|
"github.com/btcsuite/btcwallet/walletdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var TstLastErr = lastErr
|
||||||
|
|
||||||
|
const TstEligibleInputMinConfirmations = eligibleInputMinConfirmations
|
||||||
|
|
||||||
// TstPutSeries transparently wraps the voting pool putSeries method.
|
// TstPutSeries transparently wraps the voting pool putSeries method.
|
||||||
func (vp *Pool) TstPutSeries(version, seriesID, reqSigs uint32, inRawPubKeys []string) error {
|
func (vp *Pool) TstPutSeries(version, seriesID, reqSigs uint32, inRawPubKeys []string) error {
|
||||||
return vp.putSeries(version, seriesID, reqSigs, inRawPubKeys)
|
return vp.putSeries(version, seriesID, reqSigs, inRawPubKeys)
|
||||||
|
@ -31,7 +36,24 @@ var TstBranchOrder = branchOrder
|
||||||
|
|
||||||
// TstExistsSeries checks whether a series is stored in the database.
|
// TstExistsSeries checks whether a series is stored in the database.
|
||||||
func (vp *Pool) TstExistsSeries(seriesID uint32) (bool, error) {
|
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.
|
// 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)
|
return vp.decryptExtendedKey(keyType, encrypted)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SeriesRow mimics dbSeriesRow defined in db.go .
|
// TstGetMsgTx returns the withdrawal transaction with the given ntxid.
|
||||||
type SeriesRow struct {
|
func (s *WithdrawalStatus) TstGetMsgTx(ntxid Ntxid) *wire.MsgTx {
|
||||||
Version uint32
|
return s.transactions[ntxid].MsgTx
|
||||||
Active bool
|
|
||||||
ReqSigs uint32
|
|
||||||
PubKeysEncrypted [][]byte
|
|
||||||
PrivKeysEncrypted [][]byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SerializeSeries wraps serializeSeriesRow by passing it a freshly-built
|
|
||||||
// dbSeriesRow.
|
|
||||||
func SerializeSeries(version uint32, active bool, reqSigs uint32, pubKeys, privKeys [][]byte) ([]byte, error) {
|
|
||||||
row := &dbSeriesRow{
|
|
||||||
version: version,
|
|
||||||
active: active,
|
|
||||||
reqSigs: reqSigs,
|
|
||||||
pubKeysEncrypted: pubKeys,
|
|
||||||
privKeysEncrypted: privKeys,
|
|
||||||
}
|
|
||||||
return serializeSeriesRow(row)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeserializeSeries wraps deserializeSeriesRow and returns a freshly-built
|
|
||||||
// SeriesRow.
|
|
||||||
func DeserializeSeries(serializedSeries []byte) (*SeriesRow, error) {
|
|
||||||
row, err := deserializeSeriesRow(serializedSeries)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &SeriesRow{
|
|
||||||
Version: row.version,
|
|
||||||
Active: row.active,
|
|
||||||
ReqSigs: row.reqSigs,
|
|
||||||
PubKeysEncrypted: row.pubKeysEncrypted,
|
|
||||||
PrivKeysEncrypted: row.privKeysEncrypted,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var TstValidateAndDecryptKeys = validateAndDecryptKeys
|
|
||||||
|
|
42
votingpool/log.go
Normal file
42
votingpool/log.go
Normal 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
|
||||||
|
}
|
|
@ -30,8 +30,16 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
minSeriesPubKeys = 3
|
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.
|
// SeriesData represents a Series for a given Pool.
|
||||||
type SeriesData struct {
|
type SeriesData struct {
|
||||||
version uint32
|
version uint32
|
||||||
|
@ -55,6 +63,36 @@ type Pool struct {
|
||||||
namespace walletdb.Namespace
|
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
|
// Create creates a new entry in the database with the given ID
|
||||||
// and returns the Pool representing it.
|
// and returns the Pool representing it.
|
||||||
func Create(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID []byte) (*Pool, error) {
|
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 {
|
if err != nil {
|
||||||
str := fmt.Sprintf("unable to add voting pool %v to db", poolID)
|
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
|
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 {
|
func(tx walletdb.Tx) error {
|
||||||
if exists := existsPool(tx, poolID); !exists {
|
if exists := existsPool(tx, poolID); !exists {
|
||||||
str := fmt.Sprintf("unable to find voting pool %v in db", poolID)
|
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
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
vp := newPool(namespace, m, poolID)
|
p := newPool(namespace, m, poolID)
|
||||||
if err = vp.LoadAllSeries(); err != nil {
|
if err = p.LoadAllSeries(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return vp, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newPool creates a new Pool instance.
|
// 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,
|
// LoadAndGetDepositScript generates and returns a deposit script for the given seriesID,
|
||||||
// branch and index of the Pool identified by poolID.
|
// 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)
|
pid := []byte(poolID)
|
||||||
vp, err := Load(namespace, m, pid)
|
p, err := Load(namespace, m, pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
script, err := vp.DepositScript(seriesID, branch, index)
|
script, err := p.DepositScript(seriesID, branch, index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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,
|
func LoadAndCreateSeries(namespace walletdb.Namespace, m *waddrmgr.Manager, version uint32,
|
||||||
poolID string, seriesID, reqSigs uint32, rawPubKeys []string) error {
|
poolID string, seriesID, reqSigs uint32, rawPubKeys []string) error {
|
||||||
pid := []byte(poolID)
|
pid := []byte(poolID)
|
||||||
vp, err := Load(namespace, m, pid)
|
p, err := Load(namespace, m, pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
managerErr := err.(waddrmgr.ManagerError)
|
vpErr := err.(Error)
|
||||||
if managerErr.ErrorCode == waddrmgr.ErrVotingPoolNotExists {
|
if vpErr.ErrorCode == ErrPoolNotExists {
|
||||||
vp, err = Create(namespace, m, pid)
|
p, err = Create(namespace, m, pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -133,7 +171,7 @@ func LoadAndCreateSeries(namespace walletdb.Namespace, m *waddrmgr.Manager, vers
|
||||||
return err
|
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,
|
// 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,
|
func LoadAndReplaceSeries(namespace walletdb.Namespace, m *waddrmgr.Manager, version uint32,
|
||||||
poolID string, seriesID, reqSigs uint32, rawPubKeys []string) error {
|
poolID string, seriesID, reqSigs uint32, rawPubKeys []string) error {
|
||||||
pid := []byte(poolID)
|
pid := []byte(poolID)
|
||||||
vp, err := Load(namespace, m, pid)
|
p, err := Load(namespace, m, pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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,
|
// 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)
|
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.
|
// exist.
|
||||||
func (vp *Pool) GetSeries(seriesID uint32) *SeriesData {
|
func (p *Pool) Series(seriesID uint32) *SeriesData {
|
||||||
series, exists := vp.seriesLookup[seriesID]
|
series, exists := p.seriesLookup[seriesID]
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return series
|
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,
|
// saveSeriesToDisk stores the given series ID and data in the database,
|
||||||
// first encrypting the public/private extended keys.
|
// 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
|
var err error
|
||||||
encryptedPubKeys := make([][]byte, len(data.publicKeys))
|
encryptedPubKeys := make([][]byte, len(data.publicKeys))
|
||||||
for i, pubKey := range 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()))
|
waddrmgr.CKTPublic, []byte(pubKey.String()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("key %v failed encryption", pubKey)
|
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))
|
encryptedPrivKeys := make([][]byte, len(data.privateKeys))
|
||||||
|
@ -188,22 +233,22 @@ func (vp *Pool) saveSeriesToDisk(seriesID uint32, data *SeriesData) error {
|
||||||
if privKey == nil {
|
if privKey == nil {
|
||||||
encryptedPrivKeys[i] = nil
|
encryptedPrivKeys[i] = nil
|
||||||
} else {
|
} else {
|
||||||
encryptedPrivKeys[i], err = vp.manager.Encrypt(
|
encryptedPrivKeys[i], err = p.manager.Encrypt(
|
||||||
waddrmgr.CKTPrivate, []byte(privKey.String()))
|
waddrmgr.CKTPrivate, []byte(privKey.String()))
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("key %v failed encryption", privKey)
|
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 {
|
err = p.namespace.Update(func(tx walletdb.Tx) error {
|
||||||
return putSeries(tx, vp.ID, data.version, seriesID, data.active,
|
return putSeries(tx, p.ID, data.version, seriesID, data.active,
|
||||||
data.reqSigs, encryptedPubKeys, encryptedPrivKeys)
|
data.reqSigs, encryptedPubKeys, encryptedPrivKeys)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("cannot put series #%d into db", seriesID)
|
str := fmt.Sprintf("cannot put series #%d into db", seriesID)
|
||||||
return managerError(waddrmgr.ErrSeriesStorage, str, err)
|
return newError(ErrSeriesSerialization, str, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -226,19 +271,19 @@ func convertAndValidatePubKeys(rawPubKeys []string) ([]*hdkeychain.ExtendedKey,
|
||||||
for i, rawPubKey := range rawPubKeys {
|
for i, rawPubKey := range rawPubKeys {
|
||||||
if _, seen := seenKeys[rawPubKey]; seen {
|
if _, seen := seenKeys[rawPubKey]; seen {
|
||||||
str := fmt.Sprintf("duplicated public key: %v", rawPubKey)
|
str := fmt.Sprintf("duplicated public key: %v", rawPubKey)
|
||||||
return nil, managerError(waddrmgr.ErrKeyDuplicate, str, nil)
|
return nil, newError(ErrKeyDuplicate, str, nil)
|
||||||
}
|
}
|
||||||
seenKeys[rawPubKey] = true
|
seenKeys[rawPubKey] = true
|
||||||
|
|
||||||
key, err := hdkeychain.NewKeyFromString(rawPubKey)
|
key, err := hdkeychain.NewKeyFromString(rawPubKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("invalid extended public key %v", rawPubKey)
|
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() {
|
if key.IsPrivate() {
|
||||||
str := fmt.Sprintf("private keys not accepted: %v", rawPubKey)
|
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
|
keys[i] = key
|
||||||
}
|
}
|
||||||
|
@ -251,16 +296,18 @@ func convertAndValidatePubKeys(rawPubKeys []string) ([]*hdkeychain.ExtendedKey,
|
||||||
// pool's seriesLookup map. It also ensures inRawPubKeys has at least
|
// pool's seriesLookup map. It also ensures inRawPubKeys has at least
|
||||||
// minSeriesPubKeys items and reqSigs is not greater than the number of items in
|
// minSeriesPubKeys items and reqSigs is not greater than the number of items in
|
||||||
// inRawPubKeys.
|
// 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 {
|
if len(inRawPubKeys) < minSeriesPubKeys {
|
||||||
str := fmt.Sprintf("need at least %d public keys to create a series", 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)) {
|
if reqSigs > uint32(len(inRawPubKeys)) {
|
||||||
str := fmt.Sprintf(
|
str := fmt.Sprintf(
|
||||||
"the number of required signatures cannot be more than the number of keys")
|
"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)
|
rawPubKeys := CanonicalKeyOrder(inRawPubKeys)
|
||||||
|
@ -278,59 +325,90 @@ func (vp *Pool) putSeries(version, seriesID, reqSigs uint32, inRawPubKeys []stri
|
||||||
privateKeys: make([]*hdkeychain.ExtendedKey, len(keys)),
|
privateKeys: make([]*hdkeychain.ExtendedKey, len(keys)),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = vp.saveSeriesToDisk(seriesID, data)
|
err = p.saveSeriesToDisk(seriesID, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
vp.seriesLookup[seriesID] = data
|
p.seriesLookup[seriesID] = data
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateSeries will create and return a new non-existing series.
|
// 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;
|
// - rawPubKeys has to contain three or more public keys;
|
||||||
// - reqSigs has to be less or equal than the number of public keys in rawPubKeys.
|
// - 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 {
|
func (p *Pool) CreateSeries(version, seriesID, reqSigs uint32, rawPubKeys []string) error {
|
||||||
if series := vp.GetSeries(seriesID); series != nil {
|
if seriesID == 0 {
|
||||||
str := fmt.Sprintf("series #%d already exists", seriesID)
|
return newError(ErrSeriesIDInvalid, "series ID cannot be 0", nil)
|
||||||
return managerError(waddrmgr.ErrSeriesAlreadyExists, str, 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.
|
// ReplaceSeries will replace an already existing series.
|
||||||
//
|
//
|
||||||
// - rawPubKeys has to contain three or more public keys
|
// - rawPubKeys has to contain three or more public keys
|
||||||
// - reqSigs has to be less or equal than the number of public keys in rawPubKeys.
|
// - 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 {
|
func (p *Pool) ReplaceSeries(version, seriesID, reqSigs uint32, rawPubKeys []string) error {
|
||||||
series := vp.GetSeries(seriesID)
|
series := p.Series(seriesID)
|
||||||
if series == nil {
|
if series == nil {
|
||||||
str := fmt.Sprintf("series #%d does not exist, cannot replace it", seriesID)
|
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() {
|
if series.IsEmpowered() {
|
||||||
str := fmt.Sprintf("series #%d has private keys and cannot be replaced", seriesID)
|
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
|
// decryptExtendedKey uses Manager.Decrypt() to decrypt the encrypted byte slice and return
|
||||||
// an extended (public or private) key representing it.
|
// 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 {
|
if err != nil {
|
||||||
str := fmt.Sprintf("cannot decrypt key %v", encrypted)
|
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))
|
result, err := hdkeychain.NewKeyFromString(string(decrypted))
|
||||||
zero.Bytes(decrypted)
|
zero.Bytes(decrypted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("cannot get key from string %v", decrypted)
|
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
|
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
|
// 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
|
// slices is the same, decrypts them, ensures the non-nil private keys have a matching
|
||||||
// public key and returns them.
|
// 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))
|
pubKeys = make([]*hdkeychain.ExtendedKey, len(rawPubKeys))
|
||||||
privKeys = make([]*hdkeychain.ExtendedKey, len(rawPrivKeys))
|
privKeys = make([]*hdkeychain.ExtendedKey, len(rawPrivKeys))
|
||||||
if len(pubKeys) != len(privKeys) {
|
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",
|
"the pub key and priv key arrays should have the same number of elements",
|
||||||
nil)
|
nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, encryptedPub := range rawPubKeys {
|
for i, encryptedPub := range rawPubKeys {
|
||||||
pubKey, err := vp.decryptExtendedKey(waddrmgr.CKTPublic, encryptedPub)
|
pubKey, err := p.decryptExtendedKey(waddrmgr.CKTPublic, encryptedPub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -359,7 +439,7 @@ func validateAndDecryptKeys(rawPubKeys, rawPrivKeys [][]byte, vp *Pool) (pubKeys
|
||||||
if encryptedPriv == nil {
|
if encryptedPriv == nil {
|
||||||
privKey = nil
|
privKey = nil
|
||||||
} else {
|
} else {
|
||||||
privKey, err = vp.decryptExtendedKey(waddrmgr.CKTPrivate, encryptedPriv)
|
privKey, err = p.decryptExtendedKey(waddrmgr.CKTPrivate, encryptedPriv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -370,12 +450,12 @@ func validateAndDecryptKeys(rawPubKeys, rawPrivKeys [][]byte, vp *Pool) (pubKeys
|
||||||
checkPubKey, err := privKey.Neuter()
|
checkPubKey, err := privKey.Neuter()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("cannot neuter key %v", privKey)
|
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() {
|
if pubKey.String() != checkPubKey.String() {
|
||||||
str := fmt.Sprintf("public key %v different than expected %v",
|
str := fmt.Sprintf("public key %v different than expected %v",
|
||||||
pubKey, checkPubKey)
|
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
|
// 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
|
// a series, it will also ensure they have a matching extended public key
|
||||||
// in that series.
|
// 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
|
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
|
var err error
|
||||||
series, err = loadAllSeries(tx, vp.ID)
|
series, err = loadAllSeries(tx, p.ID)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -399,11 +483,11 @@ func (vp *Pool) LoadAllSeries() error {
|
||||||
}
|
}
|
||||||
for id, series := range series {
|
for id, series := range series {
|
||||||
pubKeys, privKeys, err := validateAndDecryptKeys(
|
pubKeys, privKeys, err := validateAndDecryptKeys(
|
||||||
series.pubKeysEncrypted, series.privKeysEncrypted, vp)
|
series.pubKeysEncrypted, series.privKeysEncrypted, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
vp.seriesLookup[id] = &SeriesData{
|
p.seriesLookup[id] = &SeriesData{
|
||||||
publicKeys: pubKeys,
|
publicKeys: pubKeys,
|
||||||
privateKeys: privKeys,
|
privateKeys: privKeys,
|
||||||
reqSigs: series.reqSigs,
|
reqSigs: series.reqSigs,
|
||||||
|
@ -412,41 +496,22 @@ func (vp *Pool) LoadAllSeries() error {
|
||||||
return nil
|
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.
|
// Change the order of the pubkeys based on branch number.
|
||||||
// Given the three pubkeys ABC, this would mean:
|
// Given the three pubkeys ABC, this would mean:
|
||||||
// - branch 0: CBA (reversed)
|
// - branch 0: CBA (reversed)
|
||||||
// - branch 1: ABC (first key priority)
|
// - branch 1: ABC (first key priority)
|
||||||
// - branch 2: BAC (second key priority)
|
// - branch 2: BAC (second key priority)
|
||||||
// - branch 3: CAB (third 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 {
|
if pks == nil {
|
||||||
// This really shouldn't happen, but we want to be good citizens, so we
|
// This really shouldn't happen, but we want to be good citizens, so we
|
||||||
// return an error instead of crashing.
|
// 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)) {
|
if branch > Branch(len(pks)) {
|
||||||
return nil, managerError(waddrmgr.ErrInvalidBranch, "branch number is bigger than number of public keys", nil)
|
return nil, newError(
|
||||||
|
ErrInvalidBranch, "branch number is bigger than number of public keys", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if branch == 0 {
|
if branch == 0 {
|
||||||
|
@ -472,27 +537,29 @@ func branchOrder(pks []*hdkeychain.ExtendedKey, branch uint32) ([]*hdkeychain.Ex
|
||||||
return tmp, nil
|
return tmp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DepositScriptAddress constructs a multi-signature redemption script using DepositScript
|
// DepositScriptAddress calls DepositScript to get a multi-signature
|
||||||
// and returns the pay-to-script-hash-address for that script.
|
// redemption script and returns the pay-to-script-hash-address for that script.
|
||||||
func (vp *Pool) DepositScriptAddress(seriesID, branch, index uint32) (btcutil.Address, error) {
|
func (p *Pool) DepositScriptAddress(seriesID uint32, branch Branch, index Index) (btcutil.Address, error) {
|
||||||
script, err := vp.DepositScript(seriesID, branch, index)
|
script, err := p.DepositScript(seriesID, branch, index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
scriptHash := btcutil.Hash160(script)
|
return p.addressFor(script)
|
||||||
|
}
|
||||||
|
|
||||||
return btcutil.NewAddressScriptHashFromHash(scriptHash,
|
func (p *Pool) addressFor(script []byte) (btcutil.Address, error) {
|
||||||
vp.manager.ChainParams())
|
scriptHash := btcutil.Hash160(script)
|
||||||
|
return btcutil.NewAddressScriptHashFromHash(scriptHash, p.manager.ChainParams())
|
||||||
}
|
}
|
||||||
|
|
||||||
// DepositScript constructs and returns a multi-signature redemption script where
|
// DepositScript constructs and returns a multi-signature redemption script where
|
||||||
// a certain number (Series.reqSigs) of the public keys belonging to the series
|
// 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.
|
// 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) {
|
func (p *Pool) DepositScript(seriesID uint32, branch Branch, index Index) ([]byte, error) {
|
||||||
series := vp.GetSeries(seriesID)
|
series := p.Series(seriesID)
|
||||||
if series == nil {
|
if series == nil {
|
||||||
str := fmt.Sprintf("series #%d does not exist", seriesID)
|
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)
|
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))
|
pks := make([]*btcutil.AddressPubKey, len(pubKeys))
|
||||||
for i, key := range 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,
|
// TODO: implement getting the next index until we find a valid one,
|
||||||
// in case there is a hdkeychain.ErrInvalidChild.
|
// in case there is a hdkeychain.ErrInvalidChild.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("child #%d for this pubkey %d does not exist", index, i)
|
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()
|
pubkey, err := child.ECPubKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("child #%d for this pubkey %d does not exist", index, i)
|
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(),
|
pks[i], err = btcutil.NewAddressPubKey(pubkey.SerializeCompressed(),
|
||||||
vp.manager.ChainParams())
|
p.manager.ChainParams())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf(
|
str := fmt.Sprintf(
|
||||||
"child #%d for this pubkey %d could not be converted to an address",
|
"child #%d for this pubkey %d could not be converted to an address",
|
||||||
index, i)
|
index, i)
|
||||||
return nil, managerError(waddrmgr.ErrKeyChain, str, err)
|
return nil, newError(ErrKeyChain, str, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
script, err := txscript.MultiSigScript(pks, int(series.reqSigs))
|
script, err := txscript.MultiSigScript(pks, int(series.reqSigs))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("error while making multisig script hash, %d", len(pks))
|
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
|
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
|
// EmpowerSeries adds the given extended private key (in raw format) to the
|
||||||
// series with the given ID, thus allowing it to sign deposit/withdrawal
|
// 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
|
// 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.
|
// 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
|
// make sure this series exists
|
||||||
series := vp.GetSeries(seriesID)
|
series := p.Series(seriesID)
|
||||||
if series == nil {
|
if series == nil {
|
||||||
str := fmt.Sprintf("series %d does not exist for this voting pool",
|
str := fmt.Sprintf("series %d does not exist for this voting pool",
|
||||||
seriesID)
|
seriesID)
|
||||||
return managerError(waddrmgr.ErrSeriesNotExists, str, nil)
|
return newError(ErrSeriesNotExists, str, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the private key is valid.
|
// Check that the private key is valid.
|
||||||
privKey, err := hdkeychain.NewKeyFromString(rawPrivKey)
|
privKey, err := hdkeychain.NewKeyFromString(rawPrivKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("invalid extended private key %v", rawPrivKey)
|
str := fmt.Sprintf("invalid extended private key %v", rawPrivKey)
|
||||||
return managerError(waddrmgr.ErrKeyChain, str, err)
|
return newError(ErrKeyChain, str, err)
|
||||||
}
|
}
|
||||||
if !privKey.IsPrivate() {
|
if !privKey.IsPrivate() {
|
||||||
str := fmt.Sprintf(
|
str := fmt.Sprintf(
|
||||||
"to empower a series you need the extended private key, not an extended public key %v",
|
"to empower a series you need the extended private key, not an extended public key %v",
|
||||||
privKey)
|
privKey)
|
||||||
return managerError(waddrmgr.ErrKeyIsPublic, str, err)
|
return newError(ErrKeyIsPublic, str, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pubKey, err := privKey.Neuter()
|
pubKey, err := privKey.Neuter()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := fmt.Sprintf("invalid extended private key %v, can't convert to public key",
|
str := fmt.Sprintf("invalid extended private key %v, can't convert to public key",
|
||||||
rawPrivKey)
|
rawPrivKey)
|
||||||
return managerError(waddrmgr.ErrKeyNeuter, str, err)
|
return newError(ErrKeyNeuter, str, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
lookingFor := pubKey.String()
|
lookingFor := pubKey.String()
|
||||||
|
@ -581,18 +717,153 @@ func (vp *Pool) EmpowerSeries(seriesID uint32, rawPrivKey string) error {
|
||||||
if !found {
|
if !found {
|
||||||
str := fmt.Sprintf(
|
str := fmt.Sprintf(
|
||||||
"private Key does not have a corresponding public key in this series")
|
"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 = p.saveSeriesToDisk(seriesID, series); err != nil {
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
// IsEmpowered returns true if this series is empowered (i.e. if it has
|
||||||
// at least one private key loaded).
|
// at least one private key loaded).
|
||||||
func (s *SeriesData) IsEmpowered() bool {
|
func (s *SeriesData) IsEmpowered() bool {
|
||||||
|
@ -604,8 +875,12 @@ func (s *SeriesData) IsEmpowered() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// managerError creates a waddrmgr.ManagerError given a set of arguments.
|
func (s *SeriesData) getPrivKeyFor(pubKey *hdkeychain.ExtendedKey) (*hdkeychain.ExtendedKey, error) {
|
||||||
// XXX(lars): We should probably make our own votingpoolError function.
|
for i, key := range s.publicKeys {
|
||||||
func managerError(c waddrmgr.ErrorCode, desc string, err error) waddrmgr.ManagerError {
|
if key.String() == pubKey.String() {
|
||||||
return waddrmgr.ManagerError{ErrorCode: c, Description: desc, Err: err}
|
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
463
votingpool/pool_wb_test.go
Normal 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
|
||||||
|
}
|
|
@ -1,92 +1,142 @@
|
||||||
|
|
||||||
github.com/conformal/btcwallet/votingpool/db.go serializeSeriesRow 100.00% (29/29)
|
github.com/btcsuite/btcwallet/votingpool/db.go serializeSeriesRow 100.00% (31/31)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go branchOrder 100.00% (19/19)
|
github.com/btcsuite/btcwallet/votingpool/pool.go branchOrder 100.00% (19/19)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go convertAndValidatePubKeys 100.00% (16/16)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.splitLastOutput 100.00% (16/16)
|
||||||
github.com/conformal/btcwallet/votingpool/input_selection.go Credits.Less 100.00% (12/12)
|
github.com/btcsuite/btcwallet/votingpool/pool.go convertAndValidatePubKeys 100.00% (16/16)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go Pool.decryptExtendedKey 100.00% (10/10)
|
github.com/btcsuite/btcwallet/votingpool/input_selection.go byAddress.Less 100.00% (12/12)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go Pool.ReplaceSeries 100.00% (8/8)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.maybeDropRequests 100.00% (12/12)
|
||||||
github.com/conformal/btcwallet/votingpool/input_selection.go AddressRange.NumAddresses 100.00% (7/7)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.rollBackLastOutput 100.00% (10/10)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go Create 100.00% (5/5)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go calculateSize 100.00% (10/10)
|
||||||
github.com/conformal/btcwallet/votingpool/db.go putPool 100.00% (5/5)
|
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.decryptExtendedKey 100.00% (10/10)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go Pool.DepositScriptAddress 100.00% (5/5)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go OutputRequest.outBailmentIDHash 100.00% (8/8)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.addTxIn 100.00% (4/4)
|
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.ReplaceSeries 100.00% (8/8)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.addTxOut 100.00% (4/4)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.toMsgTx 100.00% (8/8)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go CanonicalKeyOrder 100.00% (4/4)
|
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.CreateSeries 100.00% (8/8)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go @81:3 100.00% (4/4)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.addChange 100.00% (7/7)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go Pool.GetSeries 100.00% (4/4)
|
github.com/btcsuite/btcwallet/votingpool/db.go putPool 100.00% (5/5)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go seriesData.IsEmpowered 100.00% (4/4)
|
github.com/btcsuite/btcwallet/votingpool/pool.go Create 100.00% (5/5)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go Pool.CreateSeries 100.00% (4/4)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go newWithdrawalTx 100.00% (5/5)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go Pool.existsSeries 100.00% (3/3)
|
github.com/btcsuite/btcwallet/votingpool/db.go getUsedAddrHash 100.00% (4/4)
|
||||||
github.com/conformal/btcwallet/votingpool/db.go uint32ToBytes 100.00% (3/3)
|
github.com/btcsuite/btcwallet/votingpool/pool.go CanonicalKeyOrder 100.00% (4/4)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go @398:27 100.00% (3/3)
|
github.com/btcsuite/btcwallet/votingpool/pool.go SeriesData.IsEmpowered 100.00% (4/4)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go zero 100.00% (2/2)
|
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.DepositScriptAddress 100.00% (4/4)
|
||||||
github.com/conformal/btcwallet/votingpool/db.go putSeries 100.00% (2/2)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go getRedeemScript 100.00% (4/4)
|
||||||
github.com/conformal/btcwallet/votingpool/db.go existsPool 100.00% (2/2)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go newWithdrawal 100.00% (4/4)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go Ntxid 100.00% (2/2)
|
github.com/btcsuite/btcwallet/votingpool/db.go @102:3 100.00% (4/4)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go NewOutputRequest 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.removeInput 100.00% (4/4)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.Index 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.removeOutput 100.00% (4/4)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go init 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.GetSeries 100.00% (4/4)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go estimateSize 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/pool.go @119:3 100.00% (4/4)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go calculateFee 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.outputTotal 100.00% (3/3)
|
||||||
github.com/conformal/btcwallet/votingpool/db.go bytesToUint32 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.inputTotal 100.00% (3/3)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.isTooBig 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.highestUsedIndexFor 100.00% (3/3)
|
||||||
github.com/conformal/btcwallet/votingpool/error.go newError 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.popRequest 100.00% (3/3)
|
||||||
github.com/conformal/btcwallet/votingpool/input_selection.go Credit.TxSha 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/pool.go @458:26 100.00% (3/3)
|
||||||
github.com/conformal/btcwallet/votingpool/input_selection.go Credit.OutputIndex 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/db.go uint32ToBytes 100.00% (3/3)
|
||||||
github.com/conformal/btcwallet/votingpool/input_selection.go Credit.Address 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/pool.go @811:3 100.00% (3/3)
|
||||||
github.com/conformal/btcwallet/votingpool/input_selection.go newCredit 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.popInput 100.00% (3/3)
|
||||||
github.com/conformal/btcwallet/votingpool/input_selection.go Credits.Len 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.updateStatusFor 100.00% (3/3)
|
||||||
github.com/conformal/btcwallet/votingpool/input_selection.go Credits.Swap 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.addInput 100.00% (2/2)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Outpoints 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.addOutput 100.00% (2/2)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Address 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/db.go putSeries 100.00% (2/2)
|
||||||
github.com/conformal/btcwallet/votingpool/input_selection.go Pool.isCharterOutput 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go Ntxid 100.00% (2/2)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go @67:3 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.addressFor 100.00% (2/2)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go newPool 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/pool.go zero 100.00% (2/2)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Status 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/pool.go @780:3 100.00% (2/2)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.addOutpoint 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/db.go existsPool 100.00% (2/2)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalStatus.Outputs 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/db.go getUsedAddrBucketID 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Amount 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/db.go bytesToUint32 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go ChangeAddress.Next 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/error.go newError 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go @205:28 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/input_selection.go newCredit 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.Addr 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/input_selection.go credit.TxSha 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.SeriesID 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/input_selection.go credit.OutputIndex 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go managerError 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/input_selection.go credit.Address 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.Branch 100.00% (1/1)
|
github.com/btcsuite/btcwallet/votingpool/input_selection.go byAddress.Len 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go Pool.EmpowerSeries 96.43% (27/28)
|
github.com/btcsuite/btcwallet/votingpool/input_selection.go byAddress.Swap 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/db.go deserializeSeriesRow 94.87% (37/39)
|
github.com/btcsuite/btcwallet/votingpool/input_selection.go Pool.isCharterOutput 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go Pool.putSeries 93.75% (15/16)
|
github.com/btcsuite/btcwallet/votingpool/log.go init 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go validateAndDecryptKeys 92.31% (24/26)
|
github.com/btcsuite/btcwallet/votingpool/log.go DisableLog 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/input_selection.go Pool.getEligibleInputsFromSeries 86.36% (19/22)
|
github.com/btcsuite/btcwallet/votingpool/log.go UseLogger 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/input_selection.go Pool.getEligibleInputs 85.71% (6/7)
|
github.com/btcsuite/btcwallet/votingpool/pool.go @105:3 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go Load 85.71% (6/7)
|
github.com/btcsuite/btcwallet/votingpool/pool.go newPool 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/input_selection.go Pool.isCreditEligible 85.71% (6/7)
|
github.com/btcsuite/btcwallet/votingpool/pool.go Pool.Manager 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.finalizeCurrentTx 84.21% (16/19)
|
github.com/btcsuite/btcwallet/votingpool/pool.go @250:27 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/input_selection.go groupCreditsByAddr 83.33% (10/12)
|
github.com/btcsuite/btcwallet/votingpool/pool.go @761:3 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/db.go loadAllSeries 83.33% (5/6)
|
github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.Addr 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go Pool.ChangeAddress 83.33% (5/6)
|
github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.AddrIdentifier 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go LoadAndCreateSeries 80.00% (8/10)
|
github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.RedeemScript 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go Pool.LoadAllSeries 80.00% (8/10)
|
github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.Series 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go LoadAndEmpowerSeries 80.00% (4/5)
|
github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.SeriesID 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go LoadAndReplaceSeries 80.00% (4/5)
|
github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.Branch 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go Pool.WithdrawalAddress 80.00% (4/5)
|
github.com/btcsuite/btcwallet/votingpool/pool.go poolAddress.Index 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.sign 75.76% (25/33)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go byAmount.Len 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go LoadAndGetDepositScript 75.00% (6/8)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go byAmount.Less 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go OutputRequest.pkScript 75.00% (3/4)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go byAmount.Swap 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go Pool.DepositScript 73.08% (19/26)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go byOutBailmentID.Len 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go ValidateSigScripts 72.73% (8/11)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go byOutBailmentID.Swap 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.fulfilNextOutput 72.41% (21/29)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go byOutBailmentID.Less 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go SignMultiSigUTXO 71.43% (15/21)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalStatus.Outputs 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/db.go @77:3 71.43% (5/7)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go OutputRequest.String 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go getRedeemScript 71.43% (5/7)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go OutputRequest.outBailmentID 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go Pool.saveSeriesToDisk 70.00% (14/20)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalOutput.addOutpoint 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.fulfilOutputs 70.00% (7/10)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Status 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go getPrivKey 70.00% (7/10)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Address 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go Pool.Withdrawal 66.67% (12/18)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Outpoints 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/pool.go @426:3 66.67% (4/6)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go OutBailmentOutpoint.Amount 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/error.go ErrorCode.String 66.67% (2/3)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTxOut.pkScript 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/db.go putSeriesRow 53.85% (7/13)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go @237:20 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/error.go Error.Error 0.00% (0/3)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go @240:16 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.updateStatusFor 0.00% (0/0)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawalTx.hasChange 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.rollBackLastOutput 0.00% (0/0)
|
github.com/btcsuite/btcwallet/votingpool/withdrawal.go withdrawal.pushRequest 100.00% (1/1)
|
||||||
github.com/conformal/btcwallet/votingpool -------------------------------- 85.36% (554/649)
|
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)
|
||||||
|
|
||||||
|
|
43
votingpool/test_data_test.go
Normal file
43
votingpool/test_data_test.go
Normal 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
919
votingpool/withdrawal.go
Normal 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
|
||||||
|
}
|
133
votingpool/withdrawal_test.go
Normal file
133
votingpool/withdrawal_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1170
votingpool/withdrawal_wb_test.go
Normal file
1170
votingpool/withdrawal_wb_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -154,26 +154,6 @@ var errorCodeStrings = map[ErrorCode]string{
|
||||||
ErrTooManyAddresses: "ErrTooManyAddresses",
|
ErrTooManyAddresses: "ErrTooManyAddresses",
|
||||||
ErrWrongPassphrase: "ErrWrongPassphrase",
|
ErrWrongPassphrase: "ErrWrongPassphrase",
|
||||||
ErrWrongNet: "ErrWrongNet",
|
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.
|
// String returns the ErrorCode as a human-readable name.
|
||||||
|
|
|
@ -48,22 +48,6 @@ func TestErrorCodeStringer(t *testing.T) {
|
||||||
{waddrmgr.ErrTooManyAddresses, "ErrTooManyAddresses"},
|
{waddrmgr.ErrTooManyAddresses, "ErrTooManyAddresses"},
|
||||||
{waddrmgr.ErrWrongPassphrase, "ErrWrongPassphrase"},
|
{waddrmgr.ErrWrongPassphrase, "ErrWrongPassphrase"},
|
||||||
{waddrmgr.ErrWrongNet, "ErrWrongNet"},
|
{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)"},
|
{0xffff, "Unknown ErrorCode (65535)"},
|
||||||
}
|
}
|
||||||
t.Logf("Running %d tests", len(tests))
|
t.Logf("Running %d tests", len(tests))
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
Loading…
Add table
Reference in a new issue