Merge pull request #505 from cfromknecht/nuke-votingpool

votingpool: nuke package
This commit is contained in:
Olaoluwa Osuntokun 2019-04-18 21:36:24 -07:00 committed by GitHub
commit b386e6385e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 0 additions and 8272 deletions

View file

@ -1,40 +0,0 @@
votingpool
========
[![Build Status](https://travis-ci.org/btcsuite/btcwallet.png?branch=master)]
(https://travis-ci.org/btcsuite/btcwallet)
Package votingpool provides voting pool functionality for btcwallet as
described here:
[Voting Pools](http://opentransactions.org/wiki/index.php?title=Category:Voting_Pools).
A suite of tests is provided to ensure proper functionality. See
`test_coverage.txt` for the gocov coverage report. Alternatively, if you are
running a POSIX OS, you can run the `cov_report.sh` script for a real-time
report. Package votingpool is licensed under the liberal ISC license.
Note that this is still a work in progress.
## Feature Overview
- Create/Load pools
- Create series
- Replace series
- Create deposit addresses
- Comprehensive test coverage
## Documentation
[![GoDoc](https://godoc.org/github.com/btcsuite/btcwallet/votingpool?status.png)]
(http://godoc.org/github.com/btcsuite/btcwallet/votingpool)
Full `go doc` style documentation for the project can be viewed online without
installing this package by using the GoDoc site here:
http://godoc.org/github.com/btcsuite/btcwallet/votingpool
You can also view the documentation locally once the package is installed with
the `godoc` tool by running `godoc -http=":6060"` and pointing your browser to
http://localhost:6060/pkg/github.com/btcsuite/btcwallet/votingpool
Package votingpool is licensed under the [copyfree](http://copyfree.org) ISC
License.

View file

@ -1,85 +0,0 @@
// Copyright (c) 2014 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package votingpool
import (
"os"
"reflect"
"runtime"
"testing"
"github.com/btcsuite/btclog"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
)
func init() {
runtime.GOMAXPROCS(runtime.NumCPU())
// Enable logging (Debug level) to aid debugging failing tests.
logger := btclog.NewBackend(os.Stdout).Logger("TEST")
logger.SetLevel(btclog.LevelDebug)
UseLogger(logger)
}
// TstCheckError ensures the passed error is a votingpool.Error with an error
// code that matches the passed error code.
func TstCheckError(t *testing.T, testName string, gotErr error, wantErrCode ErrorCode) {
vpErr, ok := gotErr.(Error)
if !ok {
t.Errorf("%s: unexpected error type - got %T (%s), want %T",
testName, gotErr, gotErr, Error{})
}
if vpErr.ErrorCode != wantErrCode {
t.Errorf("%s: unexpected error code - got %s (%s), want %s",
testName, vpErr.ErrorCode, vpErr, wantErrCode)
}
}
// TstRunWithManagerUnlocked calls the given callback with the manager unlocked,
// and locks it again before returning.
func TstRunWithManagerUnlocked(t *testing.T, mgr *waddrmgr.Manager, addrmgrNs walletdb.ReadBucket, callback func()) {
if err := mgr.Unlock(addrmgrNs, privPassphrase); err != nil {
t.Fatal(err)
}
defer mgr.Lock()
callback()
}
// TstCheckWithdrawalStatusMatches compares s1 and s2 using reflect.DeepEqual
// and calls t.Fatal() if they're not identical.
func TstCheckWithdrawalStatusMatches(t *testing.T, s1, s2 WithdrawalStatus) {
if s1.Fees() != s2.Fees() {
t.Fatalf("Wrong amount of network fees; want %d, got %d", s1.Fees(), s2.Fees())
}
if !reflect.DeepEqual(s1.Sigs(), s2.Sigs()) {
t.Fatalf("Wrong tx signatures; got %x, want %x", s1.Sigs(), s2.Sigs())
}
if !reflect.DeepEqual(s1.NextInputAddr(), s2.NextInputAddr()) {
t.Fatalf("Wrong NextInputAddr; got %v, want %v", s1.NextInputAddr(), s2.NextInputAddr())
}
if !reflect.DeepEqual(s1.NextChangeAddr(), s2.NextChangeAddr()) {
t.Fatalf("Wrong NextChangeAddr; got %v, want %v", s1.NextChangeAddr(), s2.NextChangeAddr())
}
if !reflect.DeepEqual(s1.Outputs(), s2.Outputs()) {
t.Fatalf("Wrong WithdrawalOutputs; got %v, want %v", s1.Outputs(), s2.Outputs())
}
if !reflect.DeepEqual(s1.transactions, s2.transactions) {
t.Fatalf("Wrong transactions; got %v, want %v", s1.transactions, s2.transactions)
}
// The above checks could be replaced by this one, but when they fail the
// failure msg wouldn't give us much clue as to what is not equal, so we do
// the individual checks above and use this one as a catch-all check in case
// we forget to check any of the individual fields.
if !reflect.DeepEqual(s1, s2) {
t.Fatalf("Wrong WithdrawalStatus; got %v, want %v", s1, s2)
}
}

View file

@ -1,17 +0,0 @@
#!/bin/sh
# This script uses gocov to generate a test coverage report.
# The gocov tool my be obtained with the following command:
# go get github.com/axw/gocov/gocov
#
# It will be installed to $GOPATH/bin, so ensure that location is in your $PATH.
# Check for gocov.
type gocov >/dev/null 2>&1
if [ $? -ne 0 ]; then
echo >&2 "This script requires the gocov tool."
echo >&2 "You may obtain it with the following command:"
echo >&2 "go get github.com/axw/gocov/gocov"
exit 1
fi
gocov test | gocov report

View file

@ -1,592 +0,0 @@
// Copyright (c) 2014 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package votingpool
import (
"bytes"
"encoding/binary"
"encoding/gob"
"fmt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/snacl"
"github.com/btcsuite/btcwallet/walletdb"
)
// These constants define the serialized length for a given encrypted extended
// public or private key.
const (
// We can calculate the encrypted extended key length this way:
// snacl.Overhead == overhead for encrypting (16)
// actual base58 extended key length = (111)
// snacl.NonceSize == nonce size used for encryption (24)
seriesKeyLength = snacl.Overhead + 111 + snacl.NonceSize
// 4 bytes version + 1 byte active + 4 bytes nKeys + 4 bytes reqSigs
seriesMinSerial = 4 + 1 + 4 + 4
// 15 is the max number of keys in a voting pool, 1 each for
// pubkey and privkey
seriesMaxSerial = seriesMinSerial + 15*seriesKeyLength*2
// version of serialized Series that we support
seriesMaxVersion = 1
)
var (
usedAddrsBucketName = []byte("usedaddrs")
seriesBucketName = []byte("series")
withdrawalsBucketName = []byte("withdrawals")
// string representing a non-existent private key
seriesNullPrivKey = [seriesKeyLength]byte{}
)
type dbSeriesRow struct {
version uint32
active bool
reqSigs uint32
pubKeysEncrypted [][]byte
privKeysEncrypted [][]byte
}
type dbWithdrawalRow struct {
Requests []dbOutputRequest
StartAddress dbWithdrawalAddress
ChangeStart dbChangeAddress
LastSeriesID uint32
DustThreshold btcutil.Amount
Status dbWithdrawalStatus
}
type dbWithdrawalAddress struct {
SeriesID uint32
Branch Branch
Index Index
}
type dbChangeAddress struct {
SeriesID uint32
Index Index
}
type dbOutputRequest struct {
Addr string
Amount btcutil.Amount
Server string
Transaction uint32
}
type dbWithdrawalOutput struct {
// We store the OutBailmentID here as we need a way to look up the
// corresponding dbOutputRequest in dbWithdrawalRow when deserializing.
OutBailmentID OutBailmentID
Status outputStatus
Outpoints []dbOutBailmentOutpoint
}
type dbOutBailmentOutpoint struct {
Ntxid Ntxid
Index uint32
Amount btcutil.Amount
}
type dbChangeAwareTx struct {
SerializedMsgTx []byte
ChangeIdx int32
}
type dbWithdrawalStatus struct {
NextInputAddr dbWithdrawalAddress
NextChangeAddr dbChangeAddress
Fees btcutil.Amount
Outputs map[OutBailmentID]dbWithdrawalOutput
Sigs map[Ntxid]TxSigs
Transactions map[Ntxid]dbChangeAwareTx
}
// getUsedAddrBucketID returns the used addresses bucket ID for the given series
// and branch. It has the form seriesID:branch.
func getUsedAddrBucketID(seriesID uint32, branch Branch) []byte {
var bucketID [9]byte
binary.LittleEndian.PutUint32(bucketID[0:4], seriesID)
bucketID[4] = ':'
binary.LittleEndian.PutUint32(bucketID[5:9], uint32(branch))
return bucketID[:]
}
// putUsedAddrHash adds an entry (key==index, value==encryptedHash) to the used
// addresses bucket of the given pool, series and branch.
func putUsedAddrHash(ns walletdb.ReadWriteBucket, poolID []byte, seriesID uint32, branch Branch,
index Index, encryptedHash []byte) error {
usedAddrs := ns.NestedReadWriteBucket(poolID).NestedReadWriteBucket(usedAddrsBucketName)
bucket, err := usedAddrs.CreateBucketIfNotExists(getUsedAddrBucketID(seriesID, branch))
if err != nil {
return newError(ErrDatabase, "failed to store used address hash", err)
}
return bucket.Put(uint32ToBytes(uint32(index)), encryptedHash)
}
// getUsedAddrHash returns the addr hash with the given index from the used
// addresses bucket of the given pool, series and branch.
func getUsedAddrHash(ns walletdb.ReadBucket, poolID []byte, seriesID uint32, branch Branch,
index Index) []byte {
usedAddrs := ns.NestedReadBucket(poolID).NestedReadBucket(usedAddrsBucketName)
bucket := usedAddrs.NestedReadBucket(getUsedAddrBucketID(seriesID, branch))
if bucket == nil {
return nil
}
return bucket.Get(uint32ToBytes(uint32(index)))
}
// getMaxUsedIdx returns the highest used index from the used addresses bucket
// of the given pool, series and branch.
func getMaxUsedIdx(ns walletdb.ReadBucket, poolID []byte, seriesID uint32, branch Branch) (Index, error) {
maxIdx := Index(0)
usedAddrs := ns.NestedReadBucket(poolID).NestedReadBucket(usedAddrsBucketName)
bucket := usedAddrs.NestedReadBucket(getUsedAddrBucketID(seriesID, branch))
if bucket == nil {
return maxIdx, nil
}
// FIXME: This is far from optimal and should be optimized either by storing
// a separate key in the DB with the highest used idx for every
// series/branch or perhaps by doing a large gap linear forward search +
// binary backwards search (e.g. check for 1000000, 2000000, .... until it
// doesn't exist, and then use a binary search to find the max using the
// discovered bounds).
err := bucket.ForEach(
func(k, v []byte) error {
idx := Index(bytesToUint32(k))
if idx > maxIdx {
maxIdx = idx
}
return nil
})
if err != nil {
return Index(0), newError(ErrDatabase, "failed to get highest idx of used addresses", err)
}
return maxIdx, nil
}
// putPool stores a voting pool in the database, creating a bucket named
// after the voting pool id and two other buckets inside it to store series and
// used addresses for that pool.
func putPool(ns walletdb.ReadWriteBucket, poolID []byte) error {
poolBucket, err := ns.CreateBucket(poolID)
if err != nil {
return newError(ErrDatabase, fmt.Sprintf("cannot create pool %v", poolID), err)
}
_, err = poolBucket.CreateBucket(seriesBucketName)
if err != nil {
return newError(ErrDatabase, fmt.Sprintf("cannot create series bucket for pool %v",
poolID), err)
}
_, err = poolBucket.CreateBucket(usedAddrsBucketName)
if err != nil {
return newError(ErrDatabase, fmt.Sprintf("cannot create used addrs bucket for pool %v",
poolID), err)
}
_, err = poolBucket.CreateBucket(withdrawalsBucketName)
if err != nil {
return newError(
ErrDatabase, fmt.Sprintf("cannot create withdrawals bucket for pool %v", poolID), err)
}
return nil
}
// loadAllSeries returns a map of all the series stored inside a voting pool
// bucket, keyed by id.
func loadAllSeries(ns walletdb.ReadBucket, poolID []byte) (map[uint32]*dbSeriesRow, error) {
bucket := ns.NestedReadBucket(poolID).NestedReadBucket(seriesBucketName)
allSeries := make(map[uint32]*dbSeriesRow)
err := bucket.ForEach(
func(k, v []byte) error {
seriesID := bytesToUint32(k)
series, err := deserializeSeriesRow(v)
if err != nil {
return err
}
allSeries[seriesID] = series
return nil
})
if err != nil {
return nil, err
}
return allSeries, nil
}
// existsPool checks the existence of a bucket named after the given
// voting pool id.
func existsPool(ns walletdb.ReadBucket, poolID []byte) bool {
bucket := ns.NestedReadBucket(poolID)
return bucket != nil
}
// putSeries stores the given series inside a voting pool bucket named after
// poolID. The voting pool bucket does not need to be created beforehand.
func putSeries(ns walletdb.ReadWriteBucket, poolID []byte, version, ID uint32, active bool, reqSigs uint32, pubKeysEncrypted, privKeysEncrypted [][]byte) error {
row := &dbSeriesRow{
version: version,
active: active,
reqSigs: reqSigs,
pubKeysEncrypted: pubKeysEncrypted,
privKeysEncrypted: privKeysEncrypted,
}
return putSeriesRow(ns, poolID, ID, row)
}
// putSeriesRow stores the given series row inside a voting pool bucket named
// after poolID. The voting pool bucket does not need to be created
// beforehand.
func putSeriesRow(ns walletdb.ReadWriteBucket, poolID []byte, ID uint32, row *dbSeriesRow) error {
bucket, err := ns.CreateBucketIfNotExists(poolID)
if err != nil {
str := fmt.Sprintf("cannot create bucket %v", poolID)
return newError(ErrDatabase, str, err)
}
bucket, err = bucket.CreateBucketIfNotExists(seriesBucketName)
if err != nil {
return err
}
serialized, err := serializeSeriesRow(row)
if err != nil {
return err
}
err = bucket.Put(uint32ToBytes(ID), serialized)
if err != nil {
str := fmt.Sprintf("cannot put series %v into bucket %v", serialized, poolID)
return newError(ErrDatabase, str, err)
}
return nil
}
// deserializeSeriesRow deserializes a series storage into a dbSeriesRow struct.
func deserializeSeriesRow(serializedSeries []byte) (*dbSeriesRow, error) {
// The serialized series format is:
// <version><active><reqSigs><nKeys><pubKey1><privKey1>...<pubkeyN><privKeyN>
//
// 4 bytes version + 1 byte active + 4 bytes reqSigs + 4 bytes nKeys
// + seriesKeyLength * 2 * nKeys (1 for priv, 1 for pub)
// Given the above, the length of the serialized series should be
// at minimum the length of the constants.
if len(serializedSeries) < seriesMinSerial {
str := fmt.Sprintf("serialized series is too short: %v", serializedSeries)
return nil, newError(ErrSeriesSerialization, str, nil)
}
// Maximum number of public keys is 15 and the same for public keys
// this gives us an upper bound.
if len(serializedSeries) > seriesMaxSerial {
str := fmt.Sprintf("serialized series is too long: %v", serializedSeries)
return nil, newError(ErrSeriesSerialization, str, nil)
}
// Keeps track of the position of the next set of bytes to deserialize.
current := 0
row := dbSeriesRow{}
row.version = bytesToUint32(serializedSeries[current : current+4])
if row.version > seriesMaxVersion {
str := fmt.Sprintf("deserialization supports up to version %v not %v",
seriesMaxVersion, row.version)
return nil, newError(ErrSeriesVersion, str, nil)
}
current += 4
row.active = serializedSeries[current] == 0x01
current++
row.reqSigs = bytesToUint32(serializedSeries[current : current+4])
current += 4
nKeys := bytesToUint32(serializedSeries[current : current+4])
current += 4
// Check to see if we have the right number of bytes to consume.
if len(serializedSeries) < current+int(nKeys)*seriesKeyLength*2 {
str := fmt.Sprintf("serialized series has not enough data: %v", serializedSeries)
return nil, newError(ErrSeriesSerialization, str, nil)
} else if len(serializedSeries) > current+int(nKeys)*seriesKeyLength*2 {
str := fmt.Sprintf("serialized series has too much data: %v", serializedSeries)
return nil, newError(ErrSeriesSerialization, str, nil)
}
// Deserialize the pubkey/privkey pairs.
row.pubKeysEncrypted = make([][]byte, nKeys)
row.privKeysEncrypted = make([][]byte, nKeys)
for i := 0; i < int(nKeys); i++ {
pubKeyStart := current + seriesKeyLength*i*2
pubKeyEnd := current + seriesKeyLength*i*2 + seriesKeyLength
privKeyEnd := current + seriesKeyLength*(i+1)*2
row.pubKeysEncrypted[i] = serializedSeries[pubKeyStart:pubKeyEnd]
privKeyEncrypted := serializedSeries[pubKeyEnd:privKeyEnd]
if bytes.Equal(privKeyEncrypted, seriesNullPrivKey[:]) {
row.privKeysEncrypted[i] = nil
} else {
row.privKeysEncrypted[i] = privKeyEncrypted
}
}
return &row, nil
}
// serializeSeriesRow serializes a dbSeriesRow struct into storage format.
func serializeSeriesRow(row *dbSeriesRow) ([]byte, error) {
// The serialized series format is:
// <version><active><reqSigs><nKeys><pubKey1><privKey1>...<pubkeyN><privKeyN>
//
// 4 bytes version + 1 byte active + 4 bytes reqSigs + 4 bytes nKeys
// + seriesKeyLength * 2 * nKeys (1 for priv, 1 for pub)
serializedLen := 4 + 1 + 4 + 4 + (seriesKeyLength * 2 * len(row.pubKeysEncrypted))
if len(row.privKeysEncrypted) != 0 &&
len(row.pubKeysEncrypted) != len(row.privKeysEncrypted) {
str := fmt.Sprintf("different # of pub (%v) and priv (%v) keys",
len(row.pubKeysEncrypted), len(row.privKeysEncrypted))
return nil, newError(ErrSeriesSerialization, str, nil)
}
if row.version > seriesMaxVersion {
str := fmt.Sprintf("serialization supports up to version %v, not %v",
seriesMaxVersion, row.version)
return nil, newError(ErrSeriesVersion, str, nil)
}
serialized := make([]byte, 0, serializedLen)
serialized = append(serialized, uint32ToBytes(row.version)...)
if row.active {
serialized = append(serialized, 0x01)
} else {
serialized = append(serialized, 0x00)
}
serialized = append(serialized, uint32ToBytes(row.reqSigs)...)
nKeys := uint32(len(row.pubKeysEncrypted))
serialized = append(serialized, uint32ToBytes(nKeys)...)
var privKeyEncrypted []byte
for i, pubKeyEncrypted := range row.pubKeysEncrypted {
// check that the encrypted length is correct
if len(pubKeyEncrypted) != seriesKeyLength {
str := fmt.Sprintf("wrong length of Encrypted Public Key: %v",
pubKeyEncrypted)
return nil, newError(ErrSeriesSerialization, str, nil)
}
serialized = append(serialized, pubKeyEncrypted...)
if len(row.privKeysEncrypted) == 0 {
privKeyEncrypted = seriesNullPrivKey[:]
} else {
privKeyEncrypted = row.privKeysEncrypted[i]
}
if privKeyEncrypted == nil {
serialized = append(serialized, seriesNullPrivKey[:]...)
} else if len(privKeyEncrypted) != seriesKeyLength {
str := fmt.Sprintf("wrong length of Encrypted Private Key: %v",
len(privKeyEncrypted))
return nil, newError(ErrSeriesSerialization, str, nil)
} else {
serialized = append(serialized, privKeyEncrypted...)
}
}
return serialized, nil
}
// serializeWithdrawal constructs a dbWithdrawalRow and serializes it (using
// encoding/gob) so that it can be stored in the DB.
func serializeWithdrawal(requests []OutputRequest, startAddress WithdrawalAddress,
lastSeriesID uint32, changeStart ChangeAddress, dustThreshold btcutil.Amount,
status WithdrawalStatus) ([]byte, error) {
dbStartAddr := dbWithdrawalAddress{
SeriesID: startAddress.SeriesID(),
Branch: startAddress.Branch(),
Index: startAddress.Index(),
}
dbChangeStart := dbChangeAddress{
SeriesID: startAddress.SeriesID(),
Index: startAddress.Index(),
}
dbRequests := make([]dbOutputRequest, len(requests))
for i, request := range requests {
dbRequests[i] = dbOutputRequest{
Addr: request.Address.EncodeAddress(),
Amount: request.Amount,
Server: request.Server,
Transaction: request.Transaction,
}
}
dbOutputs := make(map[OutBailmentID]dbWithdrawalOutput, len(status.outputs))
for oid, output := range status.outputs {
dbOutpoints := make([]dbOutBailmentOutpoint, len(output.outpoints))
for i, outpoint := range output.outpoints {
dbOutpoints[i] = dbOutBailmentOutpoint{
Ntxid: outpoint.ntxid,
Index: outpoint.index,
Amount: outpoint.amount,
}
}
dbOutputs[oid] = dbWithdrawalOutput{
OutBailmentID: output.request.outBailmentID(),
Status: output.status,
Outpoints: dbOutpoints,
}
}
dbTransactions := make(map[Ntxid]dbChangeAwareTx, len(status.transactions))
for ntxid, tx := range status.transactions {
var buf bytes.Buffer
buf.Grow(tx.SerializeSize())
if err := tx.Serialize(&buf); err != nil {
return nil, err
}
dbTransactions[ntxid] = dbChangeAwareTx{
SerializedMsgTx: buf.Bytes(),
ChangeIdx: tx.changeIdx,
}
}
nextChange := status.nextChangeAddr
dbStatus := dbWithdrawalStatus{
NextChangeAddr: dbChangeAddress{
SeriesID: nextChange.seriesID,
Index: nextChange.index,
},
Fees: status.fees,
Outputs: dbOutputs,
Sigs: status.sigs,
Transactions: dbTransactions,
}
row := dbWithdrawalRow{
Requests: dbRequests,
StartAddress: dbStartAddr,
LastSeriesID: lastSeriesID,
ChangeStart: dbChangeStart,
DustThreshold: dustThreshold,
Status: dbStatus,
}
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(row); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// deserializeWithdrawal deserializes the given byte slice into a dbWithdrawalRow,
// converts it into an withdrawalInfo and returns it. This function must run
// with the address manager unlocked.
func deserializeWithdrawal(p *Pool, ns, addrmgrNs walletdb.ReadBucket, serialized []byte) (*withdrawalInfo, error) {
var row dbWithdrawalRow
if err := gob.NewDecoder(bytes.NewReader(serialized)).Decode(&row); err != nil {
return nil, newError(ErrWithdrawalStorage, "cannot deserialize withdrawal information",
err)
}
wInfo := &withdrawalInfo{
lastSeriesID: row.LastSeriesID,
dustThreshold: row.DustThreshold,
}
chainParams := p.Manager().ChainParams()
wInfo.requests = make([]OutputRequest, len(row.Requests))
// A map of requests indexed by OutBailmentID; needed to populate
// WithdrawalStatus.Outputs later on.
requestsByOID := make(map[OutBailmentID]OutputRequest)
for i, req := range row.Requests {
addr, err := btcutil.DecodeAddress(req.Addr, chainParams)
if err != nil {
return nil, newError(ErrWithdrawalStorage,
"cannot deserialize addr for requested output", err)
}
pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
return nil, newError(ErrWithdrawalStorage, "invalid addr for requested output", err)
}
request := OutputRequest{
Address: addr,
Amount: req.Amount,
PkScript: pkScript,
Server: req.Server,
Transaction: req.Transaction,
}
wInfo.requests[i] = request
requestsByOID[request.outBailmentID()] = request
}
startAddr := row.StartAddress
wAddr, err := p.WithdrawalAddress(ns, addrmgrNs, startAddr.SeriesID, startAddr.Branch, startAddr.Index)
if err != nil {
return nil, newError(ErrWithdrawalStorage, "cannot deserialize startAddress", err)
}
wInfo.startAddress = *wAddr
cAddr, err := p.ChangeAddress(row.ChangeStart.SeriesID, row.ChangeStart.Index)
if err != nil {
return nil, newError(ErrWithdrawalStorage, "cannot deserialize changeStart", err)
}
wInfo.changeStart = *cAddr
// TODO: Copy over row.Status.nextInputAddr. Not done because StartWithdrawal
// does not update that yet.
nextChangeAddr := row.Status.NextChangeAddr
cAddr, err = p.ChangeAddress(nextChangeAddr.SeriesID, nextChangeAddr.Index)
if err != nil {
return nil, newError(ErrWithdrawalStorage,
"cannot deserialize nextChangeAddress for withdrawal", err)
}
wInfo.status = WithdrawalStatus{
nextChangeAddr: *cAddr,
fees: row.Status.Fees,
outputs: make(map[OutBailmentID]*WithdrawalOutput, len(row.Status.Outputs)),
sigs: row.Status.Sigs,
transactions: make(map[Ntxid]changeAwareTx, len(row.Status.Transactions)),
}
for oid, output := range row.Status.Outputs {
outpoints := make([]OutBailmentOutpoint, len(output.Outpoints))
for i, outpoint := range output.Outpoints {
outpoints[i] = OutBailmentOutpoint{
ntxid: outpoint.Ntxid,
index: outpoint.Index,
amount: outpoint.Amount,
}
}
wInfo.status.outputs[oid] = &WithdrawalOutput{
request: requestsByOID[output.OutBailmentID],
status: output.Status,
outpoints: outpoints,
}
}
for ntxid, tx := range row.Status.Transactions {
var msgtx wire.MsgTx
if err := msgtx.Deserialize(bytes.NewBuffer(tx.SerializedMsgTx)); err != nil {
return nil, newError(ErrWithdrawalStorage, "cannot deserialize transaction", err)
}
wInfo.status.transactions[ntxid] = changeAwareTx{
MsgTx: &msgtx,
changeIdx: tx.ChangeIdx,
}
}
return wInfo, nil
}
func putWithdrawal(ns walletdb.ReadWriteBucket, poolID []byte, roundID uint32, serialized []byte) error {
bucket := ns.NestedReadWriteBucket(poolID)
return bucket.Put(uint32ToBytes(roundID), serialized)
}
func getWithdrawal(ns walletdb.ReadBucket, poolID []byte, roundID uint32) []byte {
bucket := ns.NestedReadBucket(poolID)
return bucket.Get(uint32ToBytes(roundID))
}
// uint32ToBytes converts a 32 bit unsigned integer into a 4-byte slice in
// little-endian order: 1 -> [1 0 0 0].
func uint32ToBytes(number uint32) []byte {
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, number)
return buf
}
// bytesToUint32 converts a 4-byte slice in little-endian order into a 32 bit
// unsigned integer: [1 0 0 0] -> 1.
func bytesToUint32(encoded []byte) uint32 {
return binary.LittleEndian.Uint32(encoded)
}

View file

@ -1,155 +0,0 @@
// Copyright (c) 2015 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package votingpool
import (
"bytes"
"reflect"
"testing"
"github.com/btcsuite/btcwallet/walletdb"
)
func TestPutUsedAddrHash(t *testing.T) {
tearDown, db, pool := TstCreatePool(t)
defer tearDown()
dummyHash := bytes.Repeat([]byte{0x09}, 10)
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
ns, _ := TstRWNamespaces(tx)
return putUsedAddrHash(ns, pool.ID, 0, 0, 0, dummyHash)
})
if err != nil {
t.Fatal(err)
}
var storedHash []byte
err = walletdb.View(db, func(tx walletdb.ReadTx) error {
ns, _ := TstRNamespaces(tx)
storedHash = getUsedAddrHash(ns, pool.ID, 0, 0, 0)
return nil
})
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(storedHash, dummyHash) {
t.Fatalf("Wrong stored hash; got %x, want %x", storedHash, dummyHash)
}
}
func TestGetMaxUsedIdx(t *testing.T) {
tearDown, db, pool := TstCreatePool(t)
defer tearDown()
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
ns, _ := TstRWNamespaces(tx)
for i, idx := range []int{0, 7, 9, 3001, 41, 500, 6} {
dummyHash := bytes.Repeat([]byte{byte(i)}, 10)
err := putUsedAddrHash(ns, pool.ID, 0, 0, Index(idx), dummyHash)
if err != nil {
return err
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
var maxIdx Index
err = walletdb.View(db, func(tx walletdb.ReadTx) error {
ns, _ := TstRNamespaces(tx)
var err error
maxIdx, err = getMaxUsedIdx(ns, pool.ID, 0, 0)
return err
})
if err != nil {
t.Fatal(err)
}
if maxIdx != Index(3001) {
t.Fatalf("Wrong max idx; got %d, want %d", maxIdx, Index(3001))
}
}
func TestWithdrawalSerialization(t *testing.T) {
tearDown, db, pool := TstCreatePool(t)
defer tearDown()
dbtx, err := db.BeginReadWriteTx()
if err != nil {
t.Fatal(err)
}
defer dbtx.Commit()
ns, addrmgrNs := TstRWNamespaces(dbtx)
roundID := uint32(0)
wi := createAndFulfillWithdrawalRequests(t, dbtx, pool, roundID)
serialized, err := serializeWithdrawal(wi.requests, wi.startAddress, wi.lastSeriesID,
wi.changeStart, wi.dustThreshold, wi.status)
if err != nil {
t.Fatal(err)
}
var wInfo *withdrawalInfo
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
wInfo, err = deserializeWithdrawal(pool, ns, addrmgrNs, serialized)
if err != nil {
t.Fatal(err)
}
})
if !reflect.DeepEqual(wInfo.startAddress, wi.startAddress) {
t.Fatalf("Wrong startAddr; got %v, want %v", wInfo.startAddress, wi.startAddress)
}
if !reflect.DeepEqual(wInfo.changeStart, wi.changeStart) {
t.Fatalf("Wrong changeStart; got %v, want %v", wInfo.changeStart, wi.changeStart)
}
if wInfo.lastSeriesID != wi.lastSeriesID {
t.Fatalf("Wrong LastSeriesID; got %d, want %d", wInfo.lastSeriesID, wi.lastSeriesID)
}
if wInfo.dustThreshold != wi.dustThreshold {
t.Fatalf("Wrong DustThreshold; got %d, want %d", wInfo.dustThreshold, wi.dustThreshold)
}
if !reflect.DeepEqual(wInfo.requests, wi.requests) {
t.Fatalf("Wrong output requests; got %v, want %v", wInfo.requests, wi.requests)
}
TstCheckWithdrawalStatusMatches(t, wInfo.status, wi.status)
}
func TestPutAndGetWithdrawal(t *testing.T) {
tearDown, db, _ := TstCreatePool(t)
defer tearDown()
serialized := bytes.Repeat([]byte{1}, 10)
poolID := []byte{0x00}
roundID := uint32(0)
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
ns, _ := TstRWNamespaces(tx)
return putWithdrawal(ns, poolID, roundID, serialized)
})
if err != nil {
t.Fatal(err)
}
var retrieved []byte
err = walletdb.View(db, func(tx walletdb.ReadTx) error {
ns, _ := TstRNamespaces(tx)
retrieved = getWithdrawal(ns, poolID, roundID)
return nil
})
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(retrieved, serialized) {
t.Fatalf("Wrong value retrieved from DB; got %x, want %x", retrieved, serialized)
}
}

View file

@ -1,87 +0,0 @@
// Copyright (c) 2014 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
/*
Package votingpool provides voting pool functionality for btcwallet.
Overview
The purpose of the voting pool package is to make it possible to store
bitcoins using m-of-n multisig transactions. A pool can have multiple
series, each of them with a set of pubkeys (one for each of the members
in that pool's series) and the minimum number of required signatures (m)
needed to spend the pool's coins. Each member will hold a private key
matching one of the series' public keys, and at least m members will
need to be in agreement when spending the pool's coins.
More details about voting pools as well as some of its use cases can
be found at http://opentransactions.org/wiki/index.php?title=Category:Voting_Pools
This package depends on the waddrmgr and walletdb packages.
Creating a voting pool
A voting pool is created via the Create function. This function
accepts a database namespace which will be used to store all
information related to that pool under a bucket whose key is the
pool's ID.
Loading an existing pool
An existing voting pool is loaded via the Load function, which accepts
the database name used when creating the pool as well as the poolID.
Creating a series
A series can be created via the CreateSeries method, which accepts a
version number, a series identifier, a number of required signatures
(m in m-of-n multisig), and a set of public keys.
Deposit Addresses
A deposit address can be created via the DepositScriptAddress
method, which returns a series-specific P2SH address from the multi-sig
script constructed with the index-th child of the series' public keys and
sorted according to the given branch. The procedure to construct multi-sig
deposit addresses is described in detail at
http://opentransactions.org/wiki/index.php/Deposit_Address_(voting_pools)
Replacing a series
A series can be replaced via the ReplaceSeries method. It accepts
the same parameters as the CreateSeries method.
Empowering a series
For security reasons, most private keys will be maintained offline and
only brought online when they're needed. In order to bring a key online,
one must use the EmpowerSeries method, which takes just the series ID
and a raw private key matching one of the series' public keys.
Starting withdrawals
When withdrawing coins from the pool, we employ a deterministic process
in order to minimise the cost of coordinating transaction signing. For
this to work, members of the pool have to perform an out-of-band consensus
process (<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

View file

@ -1,217 +0,0 @@
// Copyright (c) 2014 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package votingpool
import "fmt"
// ErrorCode identifies a kind of error
type ErrorCode int
const (
// ErrInputSelection indicates an error in the input selection
// algorithm.
ErrInputSelection ErrorCode = iota
// ErrWithdrawalProcessing indicates an internal error when processing a
// withdrawal request.
ErrWithdrawalProcessing
// ErrUnknownPubKey indicates a pubkey that does not belong to a given
// series.
ErrUnknownPubKey
// ErrSeriesSerialization indicates that an error occurred while
// serializing or deserializing one or more series for storing into
// the database.
ErrSeriesSerialization
// ErrSeriesVersion indicates that we've been asked to deal with a series
// whose version is unsupported
ErrSeriesVersion
// ErrSeriesNotExists indicates that an attempt has been made to access
// a series that does not exist.
ErrSeriesNotExists
// ErrSeriesAlreadyExists indicates that an attempt has been made to
// create a series that already exists.
ErrSeriesAlreadyExists
// ErrSeriesAlreadyEmpowered indicates that an already empowered series
// was used where a not empowered one was expected.
ErrSeriesAlreadyEmpowered
// ErrSeriesNotActive indicates that an active series was needed but the
// selected one is not.
ErrSeriesNotActive
// ErrKeyIsPrivate indicates that a private key was used where a public
// one was expected.
ErrKeyIsPrivate
// ErrKeyIsPublic indicates that a public key was used where a private
// one was expected.
ErrKeyIsPublic
// ErrKeyNeuter indicates a problem when trying to neuter a private key.
ErrKeyNeuter
// ErrKeyMismatch indicates that the key is not the expected one.
ErrKeyMismatch
// ErrKeysPrivatePublicMismatch indicates that the number of private and
// public keys is not the same.
ErrKeysPrivatePublicMismatch
// ErrKeyDuplicate indicates that a key is duplicated.
ErrKeyDuplicate
// ErrTooFewPublicKeys indicates that a required minimum of public
// keys was not met.
ErrTooFewPublicKeys
// ErrPoolAlreadyExists indicates that an attempt has been made to
// create a voting pool that already exists.
ErrPoolAlreadyExists
// ErrPoolNotExists indicates that an attempt has been made to access
// a voting pool that does not exist.
ErrPoolNotExists
// ErrScriptCreation indicates that the creation of a deposit script
// failed.
ErrScriptCreation
// ErrTooManyReqSignatures indicates that too many required
// signatures are requested.
ErrTooManyReqSignatures
// ErrInvalidBranch indicates that the given branch number is not valid
// for a given set of public keys.
ErrInvalidBranch
// ErrInvalidValue indicates that the value of a given function argument
// is invalid.
ErrInvalidValue
// ErrDatabase indicates an error with the underlying database.
ErrDatabase
// ErrKeyChain indicates an error with the key chain typically either
// due to the inability to create an extended key or deriving a child
// extended key.
ErrKeyChain
// ErrCrypto indicates an error with the cryptography related operations
// such as decrypting or encrypting data, parsing an EC public key,
// or deriving a secret key from a password.
ErrCrypto
// ErrRawSigning indicates an error in the process of generating raw
// signatures for a transaction input.
ErrRawSigning
// ErrPreconditionNotMet indicates a programming error since a
// preconditon has not been met.
ErrPreconditionNotMet
// ErrTxSigning indicates an error when signing a transaction.
ErrTxSigning
// ErrSeriesIDNotSequential indicates an attempt to create a series with
// an ID that is not sequantial.
ErrSeriesIDNotSequential
// ErrInvalidScriptHash indicates an invalid P2SH.
ErrInvalidScriptHash
// ErrWithdrawFromUnusedAddr indicates an attempt to withdraw funds from
// an address which has not been used before.
ErrWithdrawFromUnusedAddr
// ErrSeriesIDInvalid indicates an attempt to create a series with an
// invalid ID.
ErrSeriesIDInvalid
// ErrWithdrawalTxStorage indicates an error when storing withdrawal
// transactions.
ErrWithdrawalTxStorage
// ErrWithdrawalStorage indicates an error occurred when serializing or
// deserializing withdrawal information.
ErrWithdrawalStorage
// lastErr is used for testing, making it possible to iterate over
// the error codes in order to check that they all have proper
// translations in errorCodeStrings.
lastErr
)
// Map of ErrorCode values back to their constant names for pretty printing.
var errorCodeStrings = map[ErrorCode]string{
ErrInputSelection: "ErrInputSelection",
ErrWithdrawalProcessing: "ErrWithdrawalProcessing",
ErrUnknownPubKey: "ErrUnknownPubKey",
ErrSeriesSerialization: "ErrSeriesSerialization",
ErrSeriesVersion: "ErrSeriesVersion",
ErrSeriesNotExists: "ErrSeriesNotExists",
ErrSeriesAlreadyExists: "ErrSeriesAlreadyExists",
ErrSeriesAlreadyEmpowered: "ErrSeriesAlreadyEmpowered",
ErrSeriesIDNotSequential: "ErrSeriesIDNotSequential",
ErrSeriesIDInvalid: "ErrSeriesIDInvalid",
ErrSeriesNotActive: "ErrSeriesNotActive",
ErrKeyIsPrivate: "ErrKeyIsPrivate",
ErrKeyIsPublic: "ErrKeyIsPublic",
ErrKeyNeuter: "ErrKeyNeuter",
ErrKeyMismatch: "ErrKeyMismatch",
ErrKeysPrivatePublicMismatch: "ErrKeysPrivatePublicMismatch",
ErrKeyDuplicate: "ErrKeyDuplicate",
ErrTooFewPublicKeys: "ErrTooFewPublicKeys",
ErrPoolAlreadyExists: "ErrPoolAlreadyExists",
ErrPoolNotExists: "ErrPoolNotExists",
ErrScriptCreation: "ErrScriptCreation",
ErrTooManyReqSignatures: "ErrTooManyReqSignatures",
ErrInvalidBranch: "ErrInvalidBranch",
ErrInvalidValue: "ErrInvalidValue",
ErrDatabase: "ErrDatabase",
ErrKeyChain: "ErrKeyChain",
ErrCrypto: "ErrCrypto",
ErrRawSigning: "ErrRawSigning",
ErrPreconditionNotMet: "ErrPreconditionNotMet",
ErrTxSigning: "ErrTxSigning",
ErrInvalidScriptHash: "ErrInvalidScriptHash",
ErrWithdrawFromUnusedAddr: "ErrWithdrawFromUnusedAddr",
ErrWithdrawalTxStorage: "ErrWithdrawalTxStorage",
ErrWithdrawalStorage: "ErrWithdrawalStorage",
}
// String returns the ErrorCode as a human-readable name.
func (e ErrorCode) String() string {
if s := errorCodeStrings[e]; s != "" {
return s
}
return fmt.Sprintf("Unknown ErrorCode (%d)", int(e))
}
// Error is a typed error for all errors arising during the
// operation of the voting pool.
type Error struct {
ErrorCode ErrorCode // Describes the kind of error
Description string // Human readable description of the issue
Err error // Underlying error
}
// Error satisfies the error interface and prints human-readable errors.
func (e Error) Error() string {
if e.Err != nil {
return e.Description + ": " + e.Err.Error()
}
return e.Description
}
// newError creates a new Error.
func newError(c ErrorCode, desc string, err error) Error {
return Error{ErrorCode: c, Description: desc, Err: err}
}

View file

@ -1,72 +0,0 @@
// Copyright (c) 2014 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package votingpool_test
import (
"testing"
vp "github.com/btcsuite/btcwallet/votingpool"
)
// TestErrorCodeStringer tests that all error codes has a text
// representation and that text representation is still correct,
// ie. that a refactoring and renaming of the error code has not
// drifted from the textual representation.
func TestErrorCodeStringer(t *testing.T) {
// All the errors in ths
tests := []struct {
in vp.ErrorCode
want string
}{
{vp.ErrInputSelection, "ErrInputSelection"},
{vp.ErrWithdrawalProcessing, "ErrWithdrawalProcessing"},
{vp.ErrUnknownPubKey, "ErrUnknownPubKey"},
{vp.ErrSeriesSerialization, "ErrSeriesSerialization"},
{vp.ErrSeriesVersion, "ErrSeriesVersion"},
{vp.ErrSeriesNotExists, "ErrSeriesNotExists"},
{vp.ErrSeriesAlreadyExists, "ErrSeriesAlreadyExists"},
{vp.ErrSeriesAlreadyEmpowered, "ErrSeriesAlreadyEmpowered"},
{vp.ErrSeriesIDNotSequential, "ErrSeriesIDNotSequential"},
{vp.ErrSeriesIDInvalid, "ErrSeriesIDInvalid"},
{vp.ErrSeriesNotActive, "ErrSeriesNotActive"},
{vp.ErrKeyIsPrivate, "ErrKeyIsPrivate"},
{vp.ErrKeyIsPublic, "ErrKeyIsPublic"},
{vp.ErrKeyNeuter, "ErrKeyNeuter"},
{vp.ErrKeyMismatch, "ErrKeyMismatch"},
{vp.ErrKeysPrivatePublicMismatch, "ErrKeysPrivatePublicMismatch"},
{vp.ErrKeyDuplicate, "ErrKeyDuplicate"},
{vp.ErrTooFewPublicKeys, "ErrTooFewPublicKeys"},
{vp.ErrPoolAlreadyExists, "ErrPoolAlreadyExists"},
{vp.ErrPoolNotExists, "ErrPoolNotExists"},
{vp.ErrScriptCreation, "ErrScriptCreation"},
{vp.ErrTooManyReqSignatures, "ErrTooManyReqSignatures"},
{vp.ErrInvalidBranch, "ErrInvalidBranch"},
{vp.ErrInvalidValue, "ErrInvalidValue"},
{vp.ErrDatabase, "ErrDatabase"},
{vp.ErrKeyChain, "ErrKeyChain"},
{vp.ErrCrypto, "ErrCrypto"},
{vp.ErrRawSigning, "ErrRawSigning"},
{vp.ErrPreconditionNotMet, "ErrPreconditionNotMet"},
{vp.ErrTxSigning, "ErrTxSigning"},
{vp.ErrInvalidScriptHash, "ErrInvalidScriptHash"},
{vp.ErrWithdrawFromUnusedAddr, "ErrWithdrawFromUnusedAddr"},
{vp.ErrWithdrawalTxStorage, "ErrWithdrawalTxStorage"},
{vp.ErrWithdrawalStorage, "ErrWithdrawalStorage"},
{0xffff, "Unknown ErrorCode (65535)"},
}
if int(vp.TstLastErr) != len(tests)-1 {
t.Errorf("Wrong number of errorCodeStrings. Got: %d, want: %d",
int(vp.TstLastErr), len(tests))
}
for i, test := range tests {
result := test.in.String()
if result != test.want {
t.Errorf("String #%d\ngot: %s\nwant: %s", i, result,
test.want)
}
}
}

View file

@ -1,366 +0,0 @@
/*
* Copyright (c) 2014-2017 The btcsuite developers
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool_test
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/votingpool"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
"github.com/btcsuite/btcwallet/wtxmgr"
)
var (
pubPassphrase = []byte("pubPassphrase")
privPassphrase = []byte("privPassphrase")
seed = bytes.Repeat([]byte{0x2a, 0x64, 0xdf, 0x08}, 8)
fastScrypt = &waddrmgr.ScryptOptions{N: 16, R: 8, P: 1}
)
func createWaddrmgr(ns walletdb.ReadWriteBucket, params *chaincfg.Params) (*waddrmgr.Manager, error) {
err := waddrmgr.Create(ns, seed, pubPassphrase, privPassphrase, params,
fastScrypt, time.Now())
if err != nil {
return nil, err
}
return waddrmgr.Open(ns, pubPassphrase, params)
}
func ExampleCreate() {
// Create a new walletdb.DB. See the walletdb docs for instructions on how
// to do that.
db, dbTearDown, err := createWalletDB()
if err != nil {
fmt.Println(err)
return
}
defer dbTearDown()
dbtx, err := db.BeginReadWriteTx()
if err != nil {
fmt.Println(err)
return
}
defer dbtx.Commit()
// Create a new walletdb namespace for the address manager.
mgrNamespace, err := dbtx.CreateTopLevelBucket([]byte("waddrmgr"))
if err != nil {
fmt.Println(err)
return
}
// Create the address manager.
mgr, err := createWaddrmgr(mgrNamespace, &chaincfg.MainNetParams)
if err != nil {
fmt.Println(err)
return
}
// Create a walletdb namespace for votingpools.
vpNamespace, err := dbtx.CreateTopLevelBucket([]byte("votingpool"))
if err != nil {
fmt.Println(err)
return
}
// Create a voting pool.
_, err = votingpool.Create(vpNamespace, mgr, []byte{0x00})
if err != nil {
fmt.Println(err)
return
}
// Output:
//
}
// This example demonstrates how to create a voting pool with one
// series and get a deposit address for that series.
func Example_depositAddress() {
// Create the address manager and votingpool DB namespace. See the example
// for the Create() function for more info on how this is done.
teardown, db, mgr := exampleCreateDBAndMgr()
defer teardown()
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
ns := votingpoolNamespace(tx)
// Create the voting pool.
pool, err := votingpool.Create(ns, mgr, []byte{0x00})
if err != nil {
return err
}
// Create a 2-of-3 series.
seriesID := uint32(1)
requiredSignatures := uint32(2)
pubKeys := []string{
"xpub661MyMwAqRbcFDDrR5jY7LqsRioFDwg3cLjc7tML3RRcfYyhXqqgCH5SqMSQdpQ1Xh8EtVwcfm8psD8zXKPcRaCVSY4GCqbb3aMEs27GitE",
"xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9",
"xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh",
}
err = pool.CreateSeries(ns, votingpool.CurrentVersion, seriesID, requiredSignatures, pubKeys)
if err != nil {
return err
}
// Create a deposit address.
addr, err := pool.DepositScriptAddress(seriesID, votingpool.Branch(0), votingpool.Index(1))
if err != nil {
return err
}
fmt.Println("Generated deposit address:", addr.EncodeAddress())
return nil
})
if err != nil {
fmt.Println(err)
return
}
// Output:
// Generated deposit address: 3QTzpc9d3tTbNLJLB7xwt87nWM38boAhAw
}
// This example demonstrates how to empower a series by loading the private
// key for one of the series' public keys.
func Example_empowerSeries() {
// Create the address manager and votingpool DB namespace. See the example
// for the Create() function for more info on how this is done.
teardown, db, mgr := exampleCreateDBAndMgr()
defer teardown()
// Create a pool and a series. See the DepositAddress example for more info
// on how this is done.
pool, seriesID := exampleCreatePoolAndSeries(db, mgr)
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
ns := votingpoolNamespace(tx)
addrmgrNs := addrmgrNamespace(tx)
// Now empower the series with one of its private keys. Notice that in order
// to do that we need to unlock the address manager.
err := mgr.Unlock(addrmgrNs, privPassphrase)
if err != nil {
return err
}
defer mgr.Lock()
privKey := "xprv9s21ZrQH143K2j9PK4CXkCu8sgxkpUxCF7p1KVwiV5tdnkeYzJXReUkxz5iB2FUzTXC1L15abCDG4RMxSYT5zhm67uvsnLYxuDhZfoFcB6a"
return pool.EmpowerSeries(ns, seriesID, privKey)
})
if err != nil {
fmt.Println(err)
return
}
// Output:
//
}
// This example demonstrates how to use the Pool.StartWithdrawal method.
func Example_startWithdrawal() {
// Create the address manager and votingpool DB namespace. See the example
// for the Create() function for more info on how this is done.
teardown, db, mgr := exampleCreateDBAndMgr()
defer teardown()
// Create a pool and a series. See the DepositAddress example for more info
// on how this is done.
pool, seriesID := exampleCreatePoolAndSeries(db, mgr)
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
ns := votingpoolNamespace(tx)
addrmgrNs := addrmgrNamespace(tx)
txmgrNs := txmgrNamespace(tx)
// Create the transaction store for later use.
txstore := exampleCreateTxStore(txmgrNs)
// Unlock the manager
err := mgr.Unlock(addrmgrNs, privPassphrase)
if err != nil {
return err
}
defer mgr.Lock()
addr, _ := btcutil.DecodeAddress("1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX", mgr.ChainParams())
pkScript, _ := txscript.PayToAddrScript(addr)
requests := []votingpool.OutputRequest{
{
PkScript: pkScript,
Address: addr,
Amount: 1e6,
Server: "server-id",
Transaction: 123,
},
}
changeStart, err := pool.ChangeAddress(seriesID, votingpool.Index(0))
if err != nil {
return err
}
// This is only needed because we have not used any deposit addresses from
// the series, and we cannot create a WithdrawalAddress for an unused
// branch/idx pair.
err = pool.EnsureUsedAddr(ns, addrmgrNs, seriesID, votingpool.Branch(1), votingpool.Index(0))
if err != nil {
return err
}
startAddr, err := pool.WithdrawalAddress(ns, addrmgrNs, seriesID, votingpool.Branch(1), votingpool.Index(0))
if err != nil {
return err
}
lastSeriesID := seriesID
dustThreshold := btcutil.Amount(1e4)
currentBlock := int32(19432)
roundID := uint32(0)
_, err = pool.StartWithdrawal(ns, addrmgrNs,
roundID, requests, *startAddr, lastSeriesID, *changeStart, txstore, txmgrNs, currentBlock,
dustThreshold)
return err
})
if err != nil {
fmt.Println(err)
return
}
// Output:
//
}
func createWalletDB() (walletdb.DB, func(), error) {
dir, err := ioutil.TempDir("", "votingpool_example")
if err != nil {
return nil, nil, err
}
db, err := walletdb.Create("bdb", filepath.Join(dir, "wallet.db"))
if err != nil {
return nil, nil, err
}
dbTearDown := func() {
db.Close()
os.RemoveAll(dir)
}
return db, dbTearDown, nil
}
var (
addrmgrNamespaceKey = []byte("addrmgr")
txmgrNamespaceKey = []byte("txmgr")
votingpoolNamespaceKey = []byte("votingpool")
)
func addrmgrNamespace(dbtx walletdb.ReadWriteTx) walletdb.ReadWriteBucket {
return dbtx.ReadWriteBucket(addrmgrNamespaceKey)
}
func txmgrNamespace(dbtx walletdb.ReadWriteTx) walletdb.ReadWriteBucket {
return dbtx.ReadWriteBucket(txmgrNamespaceKey)
}
func votingpoolNamespace(dbtx walletdb.ReadWriteTx) walletdb.ReadWriteBucket {
return dbtx.ReadWriteBucket(votingpoolNamespaceKey)
}
func exampleCreateDBAndMgr() (teardown func(), db walletdb.DB, mgr *waddrmgr.Manager) {
db, dbTearDown, err := createWalletDB()
if err != nil {
dbTearDown()
panic(err)
}
// Create a new walletdb namespace for the address manager.
err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
addrmgrNs, err := tx.CreateTopLevelBucket(addrmgrNamespaceKey)
if err != nil {
return err
}
_, err = tx.CreateTopLevelBucket(votingpoolNamespaceKey)
if err != nil {
return err
}
_, err = tx.CreateTopLevelBucket(txmgrNamespaceKey)
if err != nil {
return err
}
// Create the address manager
mgr, err = createWaddrmgr(addrmgrNs, &chaincfg.MainNetParams)
return err
})
if err != nil {
dbTearDown()
panic(err)
}
teardown = func() {
mgr.Close()
dbTearDown()
}
return teardown, db, mgr
}
func exampleCreatePoolAndSeries(db walletdb.DB, mgr *waddrmgr.Manager) (pool *votingpool.Pool, seriesID uint32) {
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
ns := votingpoolNamespace(tx)
var err error
pool, err = votingpool.Create(ns, mgr, []byte{0x00})
if err != nil {
return err
}
// Create a 2-of-3 series.
seriesID = uint32(1)
requiredSignatures := uint32(2)
pubKeys := []string{
"xpub661MyMwAqRbcFDDrR5jY7LqsRioFDwg3cLjc7tML3RRcfYyhXqqgCH5SqMSQdpQ1Xh8EtVwcfm8psD8zXKPcRaCVSY4GCqbb3aMEs27GitE",
"xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9",
"xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh",
}
err = pool.CreateSeries(ns, votingpool.CurrentVersion, seriesID, requiredSignatures, pubKeys)
if err != nil {
return err
}
return pool.ActivateSeries(ns, seriesID)
})
if err != nil {
panic(err)
}
return pool, seriesID
}
func exampleCreateTxStore(ns walletdb.ReadWriteBucket) *wtxmgr.Store {
err := wtxmgr.Create(ns)
if err != nil {
panic(err)
}
s, err := wtxmgr.Open(ns, &chaincfg.MainNetParams)
if err != nil {
panic(err)
}
return s
}

View file

@ -1,472 +0,0 @@
/*
* Copyright (c) 2014-2016 The btcsuite developers
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
// Helpers to create parameterized objects to use in tests.
package votingpool
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/btcwallet/wtxmgr"
)
var (
// seed is the master seed used to create extended keys.
seed = bytes.Repeat([]byte{0x2a, 0x64, 0xdf, 0x08}, 8)
pubPassphrase = []byte("_DJr{fL4H0O}*-0\n:V1izc)(6BomK")
privPassphrase = []byte("81lUHXnOMZ@?XXd7O9xyDIWIbXX-lj")
uniqueCounter = uint32(0)
// The block height where all our test inputs are created.
TstInputsBlock = int32(10)
)
func getUniqueID() uint32 {
return atomic.AddUint32(&uniqueCounter, 1)
}
// createWithdrawalTx creates a withdrawalTx with the given input and output amounts.
func createWithdrawalTx(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, inputAmounts []int64, outputAmounts []int64) *withdrawalTx {
net := pool.Manager().ChainParams()
tx := newWithdrawalTx(defaultTxOptions)
_, credits := TstCreateCreditsOnNewSeries(t, dbtx, pool, inputAmounts)
for _, c := range credits {
tx.addInput(c)
}
for i, amount := range outputAmounts {
request := TstNewOutputRequest(
t, uint32(i), "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", btcutil.Amount(amount), net)
tx.addOutput(request)
}
return tx
}
func createMsgTx(pkScript []byte, amts []int64) *wire.MsgTx {
msgtx := &wire.MsgTx{
Version: 1,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: wire.OutPoint{
Hash: chainhash.Hash{},
Index: 0xffffffff,
},
SignatureScript: []byte{txscript.OP_NOP},
Sequence: 0xffffffff,
},
},
LockTime: 0,
}
for _, amt := range amts {
msgtx.AddTxOut(wire.NewTxOut(amt, pkScript))
}
return msgtx
}
func TstNewDepositScript(t *testing.T, p *Pool, seriesID uint32, branch Branch, idx Index) []byte {
script, err := p.DepositScript(seriesID, branch, idx)
if err != nil {
t.Fatalf("Failed to create deposit script for series %d, branch %d, index %d: %v",
seriesID, branch, idx, err)
}
return script
}
func TstRNamespaces(tx walletdb.ReadTx) (votingpoolNs, addrmgrNs walletdb.ReadBucket) {
return tx.ReadBucket(votingpoolNamespaceKey), tx.ReadBucket(addrmgrNamespaceKey)
}
func TstRWNamespaces(tx walletdb.ReadWriteTx) (votingpoolNs, addrmgrNs walletdb.ReadWriteBucket) {
return tx.ReadWriteBucket(votingpoolNamespaceKey), tx.ReadWriteBucket(addrmgrNamespaceKey)
}
func TstTxStoreRWNamespace(tx walletdb.ReadWriteTx) walletdb.ReadWriteBucket {
return tx.ReadWriteBucket(txmgrNamespaceKey)
}
// TstEnsureUsedAddr ensures the addresses defined by the given series/branch and
// index==0..idx are present in the set of used addresses for the given Pool.
func TstEnsureUsedAddr(t *testing.T, dbtx walletdb.ReadWriteTx, p *Pool, seriesID uint32, branch Branch, idx Index) []byte {
ns, addrmgrNs := TstRWNamespaces(dbtx)
addr, err := p.getUsedAddr(ns, addrmgrNs, seriesID, branch, idx)
if err != nil {
t.Fatal(err)
} else if addr != nil {
var script []byte
TstRunWithManagerUnlocked(t, p.Manager(), addrmgrNs, func() {
script, err = addr.Script()
})
if err != nil {
t.Fatal(err)
}
return script
}
TstRunWithManagerUnlocked(t, p.Manager(), addrmgrNs, func() {
err = p.EnsureUsedAddr(ns, addrmgrNs, seriesID, branch, idx)
})
if err != nil {
t.Fatal(err)
}
return TstNewDepositScript(t, p, seriesID, branch, idx)
}
func TstCreatePkScript(t *testing.T, dbtx walletdb.ReadWriteTx, p *Pool, seriesID uint32, branch Branch, idx Index) []byte {
script := TstEnsureUsedAddr(t, dbtx, p, seriesID, branch, idx)
addr, err := p.addressFor(script)
if err != nil {
t.Fatal(err)
}
pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
t.Fatal(err)
}
return pkScript
}
type TstSeriesDef struct {
ReqSigs uint32
PubKeys []string
PrivKeys []string
SeriesID uint32
Inactive bool
}
// TstCreateSeries creates a new Series for every definition in the given slice
// of TstSeriesDef. If the definition includes any private keys, the Series is
// empowered with them.
func TstCreateSeries(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, definitions []TstSeriesDef) {
ns, addrmgrNs := TstRWNamespaces(dbtx)
for _, def := range definitions {
err := pool.CreateSeries(ns, CurrentVersion, def.SeriesID, def.ReqSigs, def.PubKeys)
if err != nil {
t.Fatalf("Cannot creates series %d: %v", def.SeriesID, err)
}
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
for _, key := range def.PrivKeys {
if err := pool.EmpowerSeries(ns, def.SeriesID, key); err != nil {
t.Fatal(err)
}
}
})
pool.Series(def.SeriesID).active = !def.Inactive
}
}
func TstCreateMasterKey(t *testing.T, seed []byte) *hdkeychain.ExtendedKey {
key, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
if err != nil {
t.Fatal(err)
}
return key
}
// createMasterKeys creates count master ExtendedKeys with unique seeds.
func createMasterKeys(t *testing.T, count int) []*hdkeychain.ExtendedKey {
keys := make([]*hdkeychain.ExtendedKey, count)
for i := range keys {
keys[i] = TstCreateMasterKey(t, bytes.Repeat(uint32ToBytes(getUniqueID()), 4))
}
return keys
}
// TstCreateSeriesDef creates a TstSeriesDef with a unique SeriesID, the given
// reqSigs and the raw public/private keys extracted from the list of private
// keys. The new series will be empowered with all private keys.
func TstCreateSeriesDef(t *testing.T, pool *Pool, reqSigs uint32, keys []*hdkeychain.ExtendedKey) TstSeriesDef {
pubKeys := make([]string, len(keys))
privKeys := make([]string, len(keys))
for i, key := range keys {
privKeys[i] = key.String()
pubkey, _ := key.Neuter()
pubKeys[i] = pubkey.String()
}
seriesID := uint32(len(pool.seriesLookup)) + 1
return TstSeriesDef{
ReqSigs: reqSigs, SeriesID: seriesID, PubKeys: pubKeys, PrivKeys: privKeys}
}
func TstCreatePoolAndTxStore(t *testing.T) (tearDown func(), db walletdb.DB, pool *Pool, store *wtxmgr.Store) {
teardown, db, pool := TstCreatePool(t)
store = TstCreateTxStore(t, db)
return teardown, db, pool, store
}
// TstCreateCreditsOnNewSeries creates a new Series (with a unique ID) and a
// slice of credits locked to the series' address with branch==1 and index==0.
// The new Series will use a 2-of-3 configuration and will be empowered with
// all of its private keys.
func TstCreateCreditsOnNewSeries(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, amounts []int64) (uint32, []credit) {
masters := []*hdkeychain.ExtendedKey{
TstCreateMasterKey(t, bytes.Repeat(uint32ToBytes(getUniqueID()), 4)),
TstCreateMasterKey(t, bytes.Repeat(uint32ToBytes(getUniqueID()), 4)),
TstCreateMasterKey(t, bytes.Repeat(uint32ToBytes(getUniqueID()), 4)),
}
def := TstCreateSeriesDef(t, pool, 2, masters)
TstCreateSeries(t, dbtx, pool, []TstSeriesDef{def})
return def.SeriesID, TstCreateSeriesCredits(t, dbtx, pool, def.SeriesID, amounts)
}
// TstCreateSeriesCredits creates a new credit for every item in the amounts
// slice, locked to the given series' address with branch==1 and index==0.
func TstCreateSeriesCredits(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, seriesID uint32, amounts []int64) []credit {
addr := TstNewWithdrawalAddress(t, dbtx, pool, seriesID, Branch(1), Index(0))
pkScript, err := txscript.PayToAddrScript(addr.addr)
if err != nil {
t.Fatal(err)
}
msgTx := createMsgTx(pkScript, amounts)
txHash := msgTx.TxHash()
credits := make([]credit, len(amounts))
for i := range msgTx.TxOut {
c := wtxmgr.Credit{
OutPoint: wire.OutPoint{
Hash: txHash,
Index: uint32(i),
},
BlockMeta: wtxmgr.BlockMeta{
Block: wtxmgr.Block{Height: TstInputsBlock},
},
Amount: btcutil.Amount(msgTx.TxOut[i].Value),
PkScript: msgTx.TxOut[i].PkScript,
}
credits[i] = newCredit(c, *addr)
}
return credits
}
// TstCreateSeriesCreditsOnStore inserts a new credit in the given store for
// every item in the amounts slice. These credits are locked to the votingpool
// address composed of the given seriesID, branch==1 and index==0.
func TstCreateSeriesCreditsOnStore(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, seriesID uint32, amounts []int64,
store *wtxmgr.Store) []credit {
branch := Branch(1)
idx := Index(0)
pkScript := TstCreatePkScript(t, dbtx, pool, seriesID, branch, idx)
eligible := make([]credit, len(amounts))
for i, credit := range TstCreateCreditsOnStore(t, dbtx, store, pkScript, amounts) {
eligible[i] = newCredit(credit, *TstNewWithdrawalAddress(t, dbtx, pool, seriesID, branch, idx))
}
return eligible
}
// TstCreateCreditsOnStore inserts a new credit in the given store for
// every item in the amounts slice.
func TstCreateCreditsOnStore(t *testing.T, dbtx walletdb.ReadWriteTx, s *wtxmgr.Store, pkScript []byte, amounts []int64) []wtxmgr.Credit {
msgTx := createMsgTx(pkScript, amounts)
meta := &wtxmgr.BlockMeta{
Block: wtxmgr.Block{Height: TstInputsBlock},
}
rec, err := wtxmgr.NewTxRecordFromMsgTx(msgTx, time.Now())
if err != nil {
t.Fatal(err)
}
txmgrNs := dbtx.ReadWriteBucket(txmgrNamespaceKey)
if err := s.InsertTx(txmgrNs, rec, meta); err != nil {
t.Fatal("Failed to create inputs: ", err)
}
credits := make([]wtxmgr.Credit, len(msgTx.TxOut))
for i := range msgTx.TxOut {
if err := s.AddCredit(txmgrNs, rec, meta, uint32(i), false); err != nil {
t.Fatal("Failed to create inputs: ", err)
}
credits[i] = wtxmgr.Credit{
OutPoint: wire.OutPoint{
Hash: rec.Hash,
Index: uint32(i),
},
BlockMeta: *meta,
Amount: btcutil.Amount(msgTx.TxOut[i].Value),
PkScript: msgTx.TxOut[i].PkScript,
}
}
return credits
}
var (
addrmgrNamespaceKey = []byte("waddrmgr")
votingpoolNamespaceKey = []byte("votingpool")
txmgrNamespaceKey = []byte("testtxstore")
)
// TstCreatePool creates a Pool on a fresh walletdb and returns it. It also
// returns a teardown function that closes the Manager and removes the directory
// used to store the database.
func TstCreatePool(t *testing.T) (tearDownFunc func(), db walletdb.DB, pool *Pool) {
// This should be moved somewhere else eventually as not all of our tests
// call this function, but right now the only option would be to have the
// t.Parallel() call in each of our tests.
t.Parallel()
// Create a new wallet DB and addr manager.
dir, err := ioutil.TempDir("", "pool_test")
if err != nil {
t.Fatalf("Failed to create db dir: %v", err)
}
db, err = walletdb.Create("bdb", filepath.Join(dir, "wallet.db"))
if err != nil {
t.Fatalf("Failed to create wallet DB: %v", err)
}
var addrMgr *waddrmgr.Manager
err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
addrmgrNs, err := tx.CreateTopLevelBucket(addrmgrNamespaceKey)
if err != nil {
return err
}
votingpoolNs, err := tx.CreateTopLevelBucket(votingpoolNamespaceKey)
if err != nil {
return err
}
fastScrypt := &waddrmgr.ScryptOptions{N: 16, R: 8, P: 1}
err = waddrmgr.Create(addrmgrNs, seed, pubPassphrase, privPassphrase,
&chaincfg.MainNetParams, fastScrypt, time.Now())
if err != nil {
return err
}
addrMgr, err = waddrmgr.Open(addrmgrNs, pubPassphrase, &chaincfg.MainNetParams)
if err != nil {
return err
}
pool, err = Create(votingpoolNs, addrMgr, []byte{0x00})
return err
})
if err != nil {
t.Fatalf("Could not set up DB: %v", err)
}
tearDownFunc = func() {
addrMgr.Close()
db.Close()
os.RemoveAll(dir)
}
return tearDownFunc, db, pool
}
func TstCreateTxStore(t *testing.T, db walletdb.DB) *wtxmgr.Store {
var store *wtxmgr.Store
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
txmgrNs, err := tx.CreateTopLevelBucket(txmgrNamespaceKey)
if err != nil {
return err
}
err = wtxmgr.Create(txmgrNs)
if err != nil {
return err
}
store, err = wtxmgr.Open(txmgrNs, &chaincfg.MainNetParams)
return err
})
if err != nil {
t.Fatalf("Failed to create txmgr: %v", err)
}
return store
}
func TstNewOutputRequest(t *testing.T, transaction uint32, address string, amount btcutil.Amount,
net *chaincfg.Params) OutputRequest {
addr, err := btcutil.DecodeAddress(address, net)
if err != nil {
t.Fatalf("Unable to decode address %s", address)
}
pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
t.Fatalf("Unable to generate pkScript for %v", addr)
}
return OutputRequest{
PkScript: pkScript,
Address: addr,
Amount: amount,
Server: "server",
Transaction: transaction,
}
}
func TstNewWithdrawalOutput(r OutputRequest, status outputStatus,
outpoints []OutBailmentOutpoint) *WithdrawalOutput {
output := &WithdrawalOutput{
request: r,
status: status,
outpoints: outpoints,
}
return output
}
func TstNewWithdrawalAddress(t *testing.T, dbtx walletdb.ReadWriteTx, p *Pool, seriesID uint32, branch Branch,
index Index) (addr *WithdrawalAddress) {
TstEnsureUsedAddr(t, dbtx, p, seriesID, branch, index)
ns, addrmgrNs := TstRNamespaces(dbtx)
var err error
TstRunWithManagerUnlocked(t, p.Manager(), addrmgrNs, func() {
addr, err = p.WithdrawalAddress(ns, addrmgrNs, seriesID, branch, index)
})
if err != nil {
t.Fatalf("Failed to get WithdrawalAddress: %v", err)
}
return addr
}
func TstNewChangeAddress(t *testing.T, p *Pool, seriesID uint32, idx Index) (addr *ChangeAddress) {
addr, err := p.ChangeAddress(seriesID, idx)
if err != nil {
t.Fatalf("Failed to get ChangeAddress: %v", err)
}
return addr
}
func TstConstantFee(fee btcutil.Amount) func() btcutil.Amount {
return func() btcutil.Amount { return fee }
}
func createAndFulfillWithdrawalRequests(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, roundID uint32) withdrawalInfo {
params := pool.Manager().ChainParams()
seriesID, eligible := TstCreateCreditsOnNewSeries(t, dbtx, pool, []int64{2e6, 4e6})
requests := []OutputRequest{
TstNewOutputRequest(t, 1, "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6", 3e6, params),
TstNewOutputRequest(t, 2, "3PbExiaztsSYgh6zeMswC49hLUwhTQ86XG", 2e6, params),
}
changeStart := TstNewChangeAddress(t, pool, seriesID, 0)
dustThreshold := btcutil.Amount(1e4)
startAddr := TstNewWithdrawalAddress(t, dbtx, pool, seriesID, 1, 0)
lastSeriesID := seriesID
w := newWithdrawal(roundID, requests, eligible, *changeStart)
if err := w.fulfillRequests(); err != nil {
t.Fatal(err)
}
return withdrawalInfo{
requests: requests,
startAddress: *startAddr,
changeStart: *changeStart,
lastSeriesID: lastSeriesID,
dustThreshold: dustThreshold,
status: *w.status,
}
}

View file

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

View file

@ -1,400 +0,0 @@
// Copyright (c) 2015-2017 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package votingpool
import (
"bytes"
"reflect"
"sort"
"testing"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/btcwallet/wtxmgr"
)
var (
// random small number of satoshis used as dustThreshold
dustThreshold btcutil.Amount = 1e4
)
func TestGetEligibleInputs(t *testing.T) {
tearDown, db, pool, store := TstCreatePoolAndTxStore(t)
defer tearDown()
dbtx, err := db.BeginReadWriteTx()
if err != nil {
t.Fatal(err)
}
defer dbtx.Commit()
ns, addrmgrNs := TstRWNamespaces(dbtx)
series := []TstSeriesDef{
{ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1},
{ReqSigs: 2, PubKeys: TstPubKeys[3:6], SeriesID: 2},
}
TstCreateSeries(t, dbtx, pool, series)
scripts := append(
getPKScriptsForAddressRange(t, dbtx, pool, 1, 0, 2, 0, 4),
getPKScriptsForAddressRange(t, dbtx, pool, 2, 0, 2, 0, 6)...)
// Create two eligible inputs locked to each of the PKScripts above.
expNoEligibleInputs := 2 * len(scripts)
eligibleAmounts := []int64{int64(dustThreshold + 1), int64(dustThreshold + 1)}
var inputs []wtxmgr.Credit
for i := 0; i < len(scripts); i++ {
created := TstCreateCreditsOnStore(t, dbtx, store, scripts[i], eligibleAmounts)
inputs = append(inputs, created...)
}
startAddr := TstNewWithdrawalAddress(t, dbtx, pool, 1, 0, 0)
lastSeriesID := uint32(2)
currentBlock := int32(TstInputsBlock + eligibleInputMinConfirmations + 1)
var eligibles []credit
txmgrNs := dbtx.ReadBucket(txmgrNamespaceKey)
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
eligibles, err = pool.getEligibleInputs(ns, addrmgrNs,
store, txmgrNs, *startAddr, lastSeriesID, dustThreshold, int32(currentBlock),
eligibleInputMinConfirmations)
})
if err != nil {
t.Fatal("InputSelection failed:", err)
}
// Check we got the expected number of eligible inputs.
if len(eligibles) != expNoEligibleInputs {
t.Fatalf("Wrong number of eligible inputs returned. Got: %d, want: %d.",
len(eligibles), expNoEligibleInputs)
}
// Check that the returned eligibles are reverse sorted by address.
if !sort.IsSorted(sort.Reverse(byAddress(eligibles))) {
t.Fatal("Eligible inputs are not sorted.")
}
// Check that all credits are unique
checkUniqueness(t, eligibles)
}
func TestNextAddrWithVaryingHighestIndices(t *testing.T) {
tearDown, db, pool := TstCreatePool(t)
defer tearDown()
dbtx, err := db.BeginReadWriteTx()
if err != nil {
t.Fatal(err)
}
defer dbtx.Commit()
ns, addrmgrNs := TstRWNamespaces(dbtx)
series := []TstSeriesDef{
{ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1},
}
TstCreateSeries(t, dbtx, pool, series)
stopSeriesID := uint32(2)
// Populate the used addr DB for branch 0 and indices ranging from 0 to 2.
TstEnsureUsedAddr(t, dbtx, pool, 1, Branch(0), 2)
// Populate the used addr DB for branch 1 and indices ranging from 0 to 1.
TstEnsureUsedAddr(t, dbtx, pool, 1, Branch(1), 1)
// Start with the address for branch==0, index==1.
addr := TstNewWithdrawalAddress(t, dbtx, pool, 1, 0, 1)
// The first call to nextAddr() should give us the address for branch==1
// and index==1.
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
addr, err = nextAddr(pool, ns, addrmgrNs, addr.seriesID, addr.branch, addr.index, stopSeriesID)
})
if err != nil {
t.Fatalf("Failed to get next address: %v", err)
}
checkWithdrawalAddressMatches(t, addr, 1, Branch(1), 1)
// The next call should give us the address for branch==0, index==2 since
// there are no used addresses for branch==2.
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
addr, err = nextAddr(pool, ns, addrmgrNs, addr.seriesID, addr.branch, addr.index, stopSeriesID)
})
if err != nil {
t.Fatalf("Failed to get next address: %v", err)
}
checkWithdrawalAddressMatches(t, addr, 1, Branch(0), 2)
// Since the last addr for branch==1 was the one with index==1, a subsequent
// call will return nil.
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
addr, err = nextAddr(pool, ns, addrmgrNs, addr.seriesID, addr.branch, addr.index, stopSeriesID)
})
if err != nil {
t.Fatalf("Failed to get next address: %v", err)
}
if addr != nil {
t.Fatalf("Wrong next addr; got '%s', want 'nil'", addr.addrIdentifier())
}
}
func TestNextAddr(t *testing.T) {
tearDown, db, pool := TstCreatePool(t)
defer tearDown()
dbtx, err := db.BeginReadWriteTx()
if err != nil {
t.Fatal(err)
}
defer dbtx.Commit()
ns, addrmgrNs := TstRWNamespaces(dbtx)
series := []TstSeriesDef{
{ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1},
{ReqSigs: 2, PubKeys: TstPubKeys[3:6], SeriesID: 2},
}
TstCreateSeries(t, dbtx, pool, series)
stopSeriesID := uint32(3)
lastIdx := Index(10)
// Populate used addresses DB with entries for seriesID==1, branch==0..3,
// idx==0..10.
for _, i := range []int{0, 1, 2, 3} {
TstEnsureUsedAddr(t, dbtx, pool, 1, Branch(i), lastIdx)
}
addr := TstNewWithdrawalAddress(t, dbtx, pool, 1, 0, lastIdx-1)
// nextAddr() first increments just the branch, which ranges from 0 to 3
// here (because our series has 3 public keys).
for _, i := range []int{1, 2, 3} {
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
addr, err = nextAddr(pool, ns, addrmgrNs, addr.seriesID, addr.branch, addr.index, stopSeriesID)
})
if err != nil {
t.Fatalf("Failed to get next address: %v", err)
}
checkWithdrawalAddressMatches(t, addr, 1, Branch(i), lastIdx-1)
}
// The last nextAddr() above gave us the addr with branch=3,
// idx=lastIdx-1, so the next 4 calls should give us the addresses with
// branch=[0-3] and idx=lastIdx.
for _, i := range []int{0, 1, 2, 3} {
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
addr, err = nextAddr(pool, ns, addrmgrNs, addr.seriesID, addr.branch, addr.index, stopSeriesID)
})
if err != nil {
t.Fatalf("Failed to get next address: %v", err)
}
checkWithdrawalAddressMatches(t, addr, 1, Branch(i), lastIdx)
}
// Populate used addresses DB with entries for seriesID==2, branch==0..3,
// idx==0..10.
for _, i := range []int{0, 1, 2, 3} {
TstEnsureUsedAddr(t, dbtx, pool, 2, Branch(i), lastIdx)
}
// Now we've gone through all the available branch/idx combinations, so
// we should move to the next series and start again with branch=0, idx=0.
for _, i := range []int{0, 1, 2, 3} {
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
addr, err = nextAddr(pool, ns, addrmgrNs, addr.seriesID, addr.branch, addr.index, stopSeriesID)
})
if err != nil {
t.Fatalf("Failed to get next address: %v", err)
}
checkWithdrawalAddressMatches(t, addr, 2, Branch(i), 0)
}
// Finally check that nextAddr() returns nil when we've reached the last
// available address before stopSeriesID.
addr = TstNewWithdrawalAddress(t, dbtx, pool, 2, 3, lastIdx)
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
addr, err = nextAddr(pool, ns, addrmgrNs, addr.seriesID, addr.branch, addr.index, stopSeriesID)
})
if err != nil {
t.Fatalf("Failed to get next address: %v", err)
}
if addr != nil {
t.Fatalf("Wrong WithdrawalAddress; got %s, want nil", addr.addrIdentifier())
}
}
func TestEligibleInputsAreEligible(t *testing.T) {
tearDown, db, pool := TstCreatePool(t)
defer tearDown()
dbtx, err := db.BeginReadWriteTx()
if err != nil {
t.Fatal(err)
}
defer dbtx.Commit()
var chainHeight int32 = 1000
_, credits := TstCreateCreditsOnNewSeries(t, dbtx, pool, []int64{int64(dustThreshold)})
c := credits[0]
// Make sure credit is old enough to pass the minConf check.
c.BlockMeta.Height = int32(eligibleInputMinConfirmations)
if !pool.isCreditEligible(c, eligibleInputMinConfirmations, chainHeight, dustThreshold) {
t.Errorf("Input is not eligible and it should be.")
}
}
func TestNonEligibleInputsAreNotEligible(t *testing.T) {
tearDown, db, pool := TstCreatePool(t)
defer tearDown()
dbtx, err := db.BeginReadWriteTx()
if err != nil {
t.Fatal(err)
}
defer dbtx.Commit()
var chainHeight int32 = 1000
_, credits := TstCreateCreditsOnNewSeries(t, dbtx, pool, []int64{int64(dustThreshold - 1)})
c := credits[0]
// Make sure credit is old enough to pass the minConf check.
c.BlockMeta.Height = int32(eligibleInputMinConfirmations)
// Check that credit below dustThreshold is rejected.
if pool.isCreditEligible(c, eligibleInputMinConfirmations, chainHeight, dustThreshold) {
t.Errorf("Input is eligible and it should not be.")
}
// Check that a credit with not enough confirmations is rejected.
_, credits = TstCreateCreditsOnNewSeries(t, dbtx, pool, []int64{int64(dustThreshold)})
c = credits[0]
// The calculation of if it has been confirmed does this: chainheigt - bh +
// 1 >= target, which is quite weird, but the reason why I need to put 902
// is *that* makes 1000 - 902 +1 = 99 >= 100 false
c.BlockMeta.Height = int32(902)
if pool.isCreditEligible(c, eligibleInputMinConfirmations, chainHeight, dustThreshold) {
t.Errorf("Input is eligible and it should not be.")
}
}
func TestCreditSortingByAddress(t *testing.T) {
teardown, db, pool := TstCreatePool(t)
defer teardown()
dbtx, err := db.BeginReadWriteTx()
if err != nil {
t.Fatal(err)
}
defer dbtx.Commit()
series := []TstSeriesDef{
{ReqSigs: 2, PubKeys: TstPubKeys[1:4], SeriesID: 1},
{ReqSigs: 2, PubKeys: TstPubKeys[3:6], SeriesID: 2},
}
TstCreateSeries(t, dbtx, pool, series)
shaHash0 := bytes.Repeat([]byte{0}, 32)
shaHash1 := bytes.Repeat([]byte{1}, 32)
shaHash2 := bytes.Repeat([]byte{2}, 32)
c0 := newDummyCredit(t, dbtx, pool, 1, 0, 0, shaHash0, 0)
c1 := newDummyCredit(t, dbtx, pool, 1, 0, 0, shaHash0, 1)
c2 := newDummyCredit(t, dbtx, pool, 1, 0, 0, shaHash1, 0)
c3 := newDummyCredit(t, dbtx, pool, 1, 0, 0, shaHash2, 0)
c4 := newDummyCredit(t, dbtx, pool, 1, 0, 1, shaHash0, 0)
c5 := newDummyCredit(t, dbtx, pool, 1, 1, 0, shaHash0, 0)
c6 := newDummyCredit(t, dbtx, pool, 2, 0, 0, shaHash0, 0)
randomCredits := [][]credit{
{c6, c5, c4, c3, c2, c1, c0},
{c2, c1, c0, c6, c5, c4, c3},
{c6, c4, c5, c2, c3, c0, c1},
}
want := []credit{c0, c1, c2, c3, c4, c5, c6}
for _, random := range randomCredits {
sort.Sort(byAddress(random))
got := random
if len(got) != len(want) {
t.Fatalf("Sorted credit slice size wrong: Got: %d, want: %d",
len(got), len(want))
}
for idx := 0; idx < len(want); idx++ {
if !reflect.DeepEqual(got[idx], want[idx]) {
t.Errorf("Wrong output index. Got: %v, want: %v",
got[idx], want[idx])
}
}
}
}
// newDummyCredit creates a new credit with the given hash and outpointIdx,
// locked to the votingpool address identified by the given
// series/index/branch.
func newDummyCredit(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, series uint32, index Index, branch Branch,
txHash []byte, outpointIdx uint32) credit {
var hash chainhash.Hash
if err := hash.SetBytes(txHash); err != nil {
t.Fatal(err)
}
// Ensure the address defined by the given series/branch/index is present on
// the set of used addresses as that's a requirement of WithdrawalAddress.
TstEnsureUsedAddr(t, dbtx, pool, series, branch, index)
addr := TstNewWithdrawalAddress(t, dbtx, pool, series, branch, index)
c := wtxmgr.Credit{
OutPoint: wire.OutPoint{
Hash: hash,
Index: outpointIdx,
},
}
return newCredit(c, *addr)
}
func checkUniqueness(t *testing.T, credits byAddress) {
type uniq struct {
series uint32
branch Branch
index Index
hash chainhash.Hash
outputIndex uint32
}
uniqMap := make(map[uniq]bool)
for _, c := range credits {
u := uniq{
series: c.addr.SeriesID(),
branch: c.addr.Branch(),
index: c.addr.Index(),
hash: c.OutPoint.Hash,
outputIndex: c.OutPoint.Index,
}
if _, exists := uniqMap[u]; exists {
t.Fatalf("Duplicate found: %v", u)
} else {
uniqMap[u] = true
}
}
}
func getPKScriptsForAddressRange(t *testing.T, dbtx walletdb.ReadWriteTx, pool *Pool, seriesID uint32,
startBranch, stopBranch Branch, startIdx, stopIdx Index) [][]byte {
var pkScripts [][]byte
for idx := startIdx; idx <= stopIdx; idx++ {
for branch := startBranch; branch <= stopBranch; branch++ {
pkScripts = append(pkScripts, TstCreatePkScript(t, dbtx, pool, seriesID, branch, idx))
}
}
return pkScripts
}
func checkWithdrawalAddressMatches(t *testing.T, addr *WithdrawalAddress, seriesID uint32,
branch Branch, index Index) {
if addr.SeriesID() != seriesID {
t.Fatalf("Wrong seriesID; got %d, want %d", addr.SeriesID(), seriesID)
}
if addr.Branch() != branch {
t.Fatalf("Wrong branch; got %d, want %d", addr.Branch(), branch)
}
if addr.Index() != index {
t.Fatalf("Wrong index; got %d, want %d", addr.Index(), index)
}
}

View file

@ -1,78 +0,0 @@
// Copyright (c) 2014 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package votingpool
import (
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
)
var TstLastErr = lastErr
const TstEligibleInputMinConfirmations = eligibleInputMinConfirmations
// TstPutSeries transparently wraps the voting pool putSeries method.
func (vp *Pool) TstPutSeries(ns walletdb.ReadWriteBucket, version, seriesID, reqSigs uint32, inRawPubKeys []string) error {
return vp.putSeries(ns, version, seriesID, reqSigs, inRawPubKeys)
}
var TstBranchOrder = branchOrder
// TstExistsSeries checks whether a series is stored in the database.
func (vp *Pool) TstExistsSeries(dbtx walletdb.ReadTx, seriesID uint32) (bool, error) {
ns, _ := TstRNamespaces(dbtx)
poolBucket := ns.NestedReadBucket(vp.ID)
if poolBucket == nil {
return false, nil
}
bucket := poolBucket.NestedReadBucket(seriesBucketName)
if bucket == nil {
return false, nil
}
return bucket.Get(uint32ToBytes(seriesID)) != nil, nil
}
// TstGetRawPublicKeys gets a series public keys in string format.
func (s *SeriesData) TstGetRawPublicKeys() []string {
rawKeys := make([]string, len(s.publicKeys))
for i, key := range s.publicKeys {
rawKeys[i] = key.String()
}
return rawKeys
}
// TstGetRawPrivateKeys gets a series private keys in string format.
func (s *SeriesData) TstGetRawPrivateKeys() []string {
rawKeys := make([]string, len(s.privateKeys))
for i, key := range s.privateKeys {
if key != nil {
rawKeys[i] = key.String()
}
}
return rawKeys
}
// TstGetReqSigs expose the series reqSigs attribute.
func (s *SeriesData) TstGetReqSigs() uint32 {
return s.reqSigs
}
// TstEmptySeriesLookup empties the voting pool seriesLookup attribute.
func (vp *Pool) TstEmptySeriesLookup() {
vp.seriesLookup = make(map[uint32]*SeriesData)
}
// TstDecryptExtendedKey expose the decryptExtendedKey method.
func (vp *Pool) TstDecryptExtendedKey(keyType waddrmgr.CryptoKeyType, encrypted []byte) (*hdkeychain.ExtendedKey, error) {
return vp.decryptExtendedKey(keyType, encrypted)
}
// TstGetMsgTx returns a copy of the withdrawal transaction with the given
// ntxid.
func (s *WithdrawalStatus) TstGetMsgTx(ntxid Ntxid) *wire.MsgTx {
return s.transactions[ntxid].MsgTx.Copy()
}

View file

@ -1,30 +0,0 @@
// Copyright (c) 2015 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package votingpool
import "github.com/btcsuite/btclog"
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var log btclog.Logger
// The default amount of logging is none.
func init() {
DisableLog()
}
// DisableLog disables all library log output. Logging output is disabled
// by default until either UseLogger or SetLogWriter are called.
func DisableLog() {
log = btclog.Disabled
}
// UseLogger uses a specified Logger to output package logging info.
// This should be used in preference to SetLogWriter if the caller is also
// using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}

View file

@ -1,841 +0,0 @@
// Copyright (c) 2014 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package votingpool
import (
"fmt"
"sort"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/internal/zero"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
)
const (
minSeriesPubKeys = 3
// CurrentVersion is the version used for newly created Series.
CurrentVersion = 1
)
// Branch is the type used to represent a branch number in a series.
type Branch uint32
// Index is the type used to represent an index number in a series.
type Index uint32
// SeriesData represents a Series for a given Pool.
type SeriesData struct {
version uint32
// Whether or not a series is active. This is serialized/deserialized but
// for now there's no way to deactivate a series.
active bool
// A.k.a. "m" in "m of n signatures needed".
reqSigs uint32
publicKeys []*hdkeychain.ExtendedKey
privateKeys []*hdkeychain.ExtendedKey
}
// Pool represents an arrangement of notary servers to securely
// store and account for customer cryptocurrency deposits and to redeem
// valid withdrawals. For details about how the arrangement works, see
// http://opentransactions.org/wiki/index.php?title=Category:Voting_Pools
type Pool struct {
ID []byte
seriesLookup map[uint32]*SeriesData
manager *waddrmgr.Manager
}
// PoolAddress represents a voting pool P2SH address, generated by
// deriving public HD keys from the series' master keys using the given
// branch/index and constructing a M-of-N multi-sig script.
type PoolAddress interface {
SeriesID() uint32
Branch() Branch
Index() Index
}
type poolAddress struct {
pool *Pool
addr btcutil.Address
script []byte
seriesID uint32
branch Branch
index Index
}
// ChangeAddress is a votingpool address meant to be used on transaction change
// outputs. All change addresses have branch==0.
type ChangeAddress struct {
*poolAddress
}
// WithdrawalAddress is a votingpool address that may contain unspent outputs
// available for use in a withdrawal.
type WithdrawalAddress struct {
*poolAddress
}
// Create creates a new entry in the database with the given ID
// and returns the Pool representing it.
func Create(ns walletdb.ReadWriteBucket, m *waddrmgr.Manager, poolID []byte) (*Pool, error) {
err := putPool(ns, poolID)
if err != nil {
str := fmt.Sprintf("unable to add voting pool %v to db", poolID)
return nil, newError(ErrPoolAlreadyExists, str, err)
}
return newPool(m, poolID), nil
}
// Load fetches the entry in the database with the given ID and returns the Pool
// representing it.
func Load(ns walletdb.ReadBucket, m *waddrmgr.Manager, poolID []byte) (*Pool, error) {
if !existsPool(ns, poolID) {
str := fmt.Sprintf("unable to find voting pool %v in db", poolID)
return nil, newError(ErrPoolNotExists, str, nil)
}
p := newPool(m, poolID)
if err := p.LoadAllSeries(ns); err != nil {
return nil, err
}
return p, nil
}
// newPool creates a new Pool instance.
func newPool(m *waddrmgr.Manager, poolID []byte) *Pool {
return &Pool{
ID: poolID,
seriesLookup: make(map[uint32]*SeriesData),
manager: m,
}
}
// LoadAndGetDepositScript generates and returns a deposit script for the given seriesID,
// branch and index of the Pool identified by poolID.
func LoadAndGetDepositScript(ns walletdb.ReadBucket, m *waddrmgr.Manager, poolID string, seriesID uint32, branch Branch, index Index) ([]byte, error) {
pid := []byte(poolID)
p, err := Load(ns, m, pid)
if err != nil {
return nil, err
}
script, err := p.DepositScript(seriesID, branch, index)
if err != nil {
return nil, err
}
return script, nil
}
// LoadAndCreateSeries loads the Pool with the given ID, creating a new one if it doesn't
// yet exist, and then creates and returns a Series with the given seriesID, rawPubKeys
// and reqSigs. See CreateSeries for the constraints enforced on rawPubKeys and reqSigs.
func LoadAndCreateSeries(ns walletdb.ReadWriteBucket, m *waddrmgr.Manager, version uint32,
poolID string, seriesID, reqSigs uint32, rawPubKeys []string) error {
pid := []byte(poolID)
p, err := Load(ns, m, pid)
if err != nil {
vpErr := err.(Error)
if vpErr.ErrorCode == ErrPoolNotExists {
p, err = Create(ns, m, pid)
if err != nil {
return err
}
} else {
return err
}
}
return p.CreateSeries(ns, version, seriesID, reqSigs, rawPubKeys)
}
// LoadAndReplaceSeries loads the voting pool with the given ID and calls ReplaceSeries,
// passing the given series ID, public keys and reqSigs to it.
func LoadAndReplaceSeries(ns walletdb.ReadWriteBucket, m *waddrmgr.Manager, version uint32,
poolID string, seriesID, reqSigs uint32, rawPubKeys []string) error {
pid := []byte(poolID)
p, err := Load(ns, m, pid)
if err != nil {
return err
}
return p.ReplaceSeries(ns, version, seriesID, reqSigs, rawPubKeys)
}
// LoadAndEmpowerSeries loads the voting pool with the given ID and calls EmpowerSeries,
// passing the given series ID and private key to it.
func LoadAndEmpowerSeries(ns walletdb.ReadWriteBucket, m *waddrmgr.Manager,
poolID string, seriesID uint32, rawPrivKey string) error {
pid := []byte(poolID)
pool, err := Load(ns, m, pid)
if err != nil {
return err
}
return pool.EmpowerSeries(ns, seriesID, rawPrivKey)
}
// Series returns the series with the given ID, or nil if it doesn't
// exist.
func (p *Pool) Series(seriesID uint32) *SeriesData {
series, exists := p.seriesLookup[seriesID]
if !exists {
return nil
}
return series
}
// Manager returns the waddrmgr.Manager used by this Pool.
func (p *Pool) Manager() *waddrmgr.Manager {
return p.manager
}
// saveSeriesToDisk stores the given series ID and data in the database,
// first encrypting the public/private extended keys.
//
// This method must be called with the Pool's manager unlocked.
func (p *Pool) saveSeriesToDisk(ns walletdb.ReadWriteBucket, seriesID uint32, data *SeriesData) error {
var err error
encryptedPubKeys := make([][]byte, len(data.publicKeys))
for i, pubKey := range data.publicKeys {
encryptedPubKeys[i], err = p.manager.Encrypt(
waddrmgr.CKTPublic, []byte(pubKey.String()))
if err != nil {
str := fmt.Sprintf("key %v failed encryption", pubKey)
return newError(ErrCrypto, str, err)
}
}
encryptedPrivKeys := make([][]byte, len(data.privateKeys))
for i, privKey := range data.privateKeys {
if privKey == nil {
encryptedPrivKeys[i] = nil
} else {
encryptedPrivKeys[i], err = p.manager.Encrypt(
waddrmgr.CKTPrivate, []byte(privKey.String()))
}
if err != nil {
str := fmt.Sprintf("key %v failed encryption", privKey)
return newError(ErrCrypto, str, err)
}
}
err = putSeries(ns, p.ID, data.version, seriesID, data.active,
data.reqSigs, encryptedPubKeys, encryptedPrivKeys)
if err != nil {
str := fmt.Sprintf("cannot put series #%d into db", seriesID)
return newError(ErrSeriesSerialization, str, err)
}
return nil
}
// CanonicalKeyOrder will return a copy of the input canonically
// ordered which is defined to be lexicographical.
func CanonicalKeyOrder(keys []string) []string {
orderedKeys := make([]string, len(keys))
copy(orderedKeys, keys)
sort.Sort(sort.StringSlice(orderedKeys))
return orderedKeys
}
// Convert the given slice of strings into a slice of ExtendedKeys,
// checking that all of them are valid public (and not private) keys,
// and that there are no duplicates.
func convertAndValidatePubKeys(rawPubKeys []string) ([]*hdkeychain.ExtendedKey, error) {
seenKeys := make(map[string]bool)
keys := make([]*hdkeychain.ExtendedKey, len(rawPubKeys))
for i, rawPubKey := range rawPubKeys {
if _, seen := seenKeys[rawPubKey]; seen {
str := fmt.Sprintf("duplicated public key: %v", rawPubKey)
return nil, newError(ErrKeyDuplicate, str, nil)
}
seenKeys[rawPubKey] = true
key, err := hdkeychain.NewKeyFromString(rawPubKey)
if err != nil {
str := fmt.Sprintf("invalid extended public key %v", rawPubKey)
return nil, newError(ErrKeyChain, str, err)
}
if key.IsPrivate() {
str := fmt.Sprintf("private keys not accepted: %v", rawPubKey)
return nil, newError(ErrKeyIsPrivate, str, nil)
}
keys[i] = key
}
return keys, nil
}
// putSeries creates a new seriesData with the given arguments, ordering the
// given public keys (using CanonicalKeyOrder), validating and converting them
// to hdkeychain.ExtendedKeys, saves that to disk and adds it to this voting
// pool's seriesLookup map. It also ensures inRawPubKeys has at least
// minSeriesPubKeys items and reqSigs is not greater than the number of items in
// inRawPubKeys.
//
// This method must be called with the Pool's manager unlocked.
func (p *Pool) putSeries(ns walletdb.ReadWriteBucket, version, seriesID, reqSigs uint32, inRawPubKeys []string) error {
if len(inRawPubKeys) < minSeriesPubKeys {
str := fmt.Sprintf("need at least %d public keys to create a series", minSeriesPubKeys)
return newError(ErrTooFewPublicKeys, str, nil)
}
if reqSigs > uint32(len(inRawPubKeys)) {
str := fmt.Sprintf(
"the number of required signatures cannot be more than the number of keys")
return newError(ErrTooManyReqSignatures, str, nil)
}
rawPubKeys := CanonicalKeyOrder(inRawPubKeys)
keys, err := convertAndValidatePubKeys(rawPubKeys)
if err != nil {
return err
}
data := &SeriesData{
version: version,
active: false,
reqSigs: reqSigs,
publicKeys: keys,
privateKeys: make([]*hdkeychain.ExtendedKey, len(keys)),
}
err = p.saveSeriesToDisk(ns, seriesID, data)
if err != nil {
return err
}
p.seriesLookup[seriesID] = data
return nil
}
// CreateSeries will create and return a new non-existing series.
//
// - seriesID must be greater than or equal 1;
// - rawPubKeys has to contain three or more public keys;
// - reqSigs has to be less or equal than the number of public keys in rawPubKeys.
func (p *Pool) CreateSeries(ns walletdb.ReadWriteBucket, version, seriesID, reqSigs uint32, rawPubKeys []string) error {
if seriesID == 0 {
return newError(ErrSeriesIDInvalid, "series ID cannot be 0", nil)
}
if series := p.Series(seriesID); series != nil {
str := fmt.Sprintf("series #%d already exists", seriesID)
return newError(ErrSeriesAlreadyExists, str, nil)
}
if seriesID != 1 {
if _, ok := p.seriesLookup[seriesID-1]; !ok {
str := fmt.Sprintf("series #%d cannot be created because series #%d does not exist",
seriesID, seriesID-1)
return newError(ErrSeriesIDNotSequential, str, nil)
}
}
return p.putSeries(ns, version, seriesID, reqSigs, rawPubKeys)
}
// ActivateSeries marks the series with the given ID as active.
func (p *Pool) ActivateSeries(ns walletdb.ReadWriteBucket, seriesID uint32) error {
series := p.Series(seriesID)
if series == nil {
str := fmt.Sprintf("series #%d does not exist, cannot activate it", seriesID)
return newError(ErrSeriesNotExists, str, nil)
}
series.active = true
err := p.saveSeriesToDisk(ns, seriesID, series)
if err != nil {
return err
}
p.seriesLookup[seriesID] = series
return nil
}
// ReplaceSeries will replace an already existing series.
//
// - rawPubKeys has to contain three or more public keys
// - reqSigs has to be less or equal than the number of public keys in rawPubKeys.
func (p *Pool) ReplaceSeries(ns walletdb.ReadWriteBucket, version, seriesID, reqSigs uint32, rawPubKeys []string) error {
series := p.Series(seriesID)
if series == nil {
str := fmt.Sprintf("series #%d does not exist, cannot replace it", seriesID)
return newError(ErrSeriesNotExists, str, nil)
}
if series.IsEmpowered() {
str := fmt.Sprintf("series #%d has private keys and cannot be replaced", seriesID)
return newError(ErrSeriesAlreadyEmpowered, str, nil)
}
return p.putSeries(ns, version, seriesID, reqSigs, rawPubKeys)
}
// decryptExtendedKey uses Manager.Decrypt() to decrypt the encrypted byte slice and return
// an extended (public or private) key representing it.
//
// This method must be called with the Pool's manager unlocked.
func (p *Pool) decryptExtendedKey(keyType waddrmgr.CryptoKeyType, encrypted []byte) (*hdkeychain.ExtendedKey, error) {
decrypted, err := p.manager.Decrypt(keyType, encrypted)
if err != nil {
str := fmt.Sprintf("cannot decrypt key %v", encrypted)
return nil, newError(ErrCrypto, str, err)
}
result, err := hdkeychain.NewKeyFromString(string(decrypted))
zero.Bytes(decrypted)
if err != nil {
str := fmt.Sprintf("cannot get key from string %v", decrypted)
return nil, newError(ErrKeyChain, str, err)
}
return result, nil
}
// validateAndDecryptSeriesKeys checks that the length of the public and private key
// slices is the same, decrypts them, ensures the non-nil private keys have a matching
// public key and returns them.
//
// This function must be called with the Pool's manager unlocked.
func validateAndDecryptKeys(rawPubKeys, rawPrivKeys [][]byte, p *Pool) (pubKeys, privKeys []*hdkeychain.ExtendedKey, err error) {
pubKeys = make([]*hdkeychain.ExtendedKey, len(rawPubKeys))
privKeys = make([]*hdkeychain.ExtendedKey, len(rawPrivKeys))
if len(pubKeys) != len(privKeys) {
return nil, nil, newError(ErrKeysPrivatePublicMismatch,
"the pub key and priv key arrays should have the same number of elements",
nil)
}
for i, encryptedPub := range rawPubKeys {
pubKey, err := p.decryptExtendedKey(waddrmgr.CKTPublic, encryptedPub)
if err != nil {
return nil, nil, err
}
pubKeys[i] = pubKey
encryptedPriv := rawPrivKeys[i]
var privKey *hdkeychain.ExtendedKey
if encryptedPriv == nil {
privKey = nil
} else {
privKey, err = p.decryptExtendedKey(waddrmgr.CKTPrivate, encryptedPriv)
if err != nil {
return nil, nil, err
}
}
privKeys[i] = privKey
if privKey != nil {
checkPubKey, err := privKey.Neuter()
if err != nil {
str := fmt.Sprintf("cannot neuter key %v", privKey)
return nil, nil, newError(ErrKeyNeuter, str, err)
}
if pubKey.String() != checkPubKey.String() {
str := fmt.Sprintf("public key %v different than expected %v",
pubKey, checkPubKey)
return nil, nil, newError(ErrKeyMismatch, str, nil)
}
}
}
return pubKeys, privKeys, nil
}
// LoadAllSeries fetches all series (decrypting their public and private
// extended keys) for this Pool from the database and populates the
// seriesLookup map with them. If there are any private extended keys for
// a series, it will also ensure they have a matching extended public key
// in that series.
//
// This method must be called with the Pool's manager unlocked.
// FIXME: We should be able to get rid of this (and loadAllSeries/seriesLookup)
// by making Series() load the series data directly from the DB.
func (p *Pool) LoadAllSeries(ns walletdb.ReadBucket) error {
series, err := loadAllSeries(ns, p.ID)
if err != nil {
return err
}
for id, series := range series {
pubKeys, privKeys, err := validateAndDecryptKeys(
series.pubKeysEncrypted, series.privKeysEncrypted, p)
if err != nil {
return err
}
p.seriesLookup[id] = &SeriesData{
publicKeys: pubKeys,
privateKeys: privKeys,
reqSigs: series.reqSigs,
}
}
return nil
}
// Change the order of the pubkeys based on branch number.
// Given the three pubkeys ABC, this would mean:
// - branch 0: CBA (reversed)
// - branch 1: ABC (first key priority)
// - branch 2: BAC (second key priority)
// - branch 3: CAB (third key priority)
func branchOrder(pks []*hdkeychain.ExtendedKey, branch Branch) ([]*hdkeychain.ExtendedKey, error) {
if pks == nil {
// This really shouldn't happen, but we want to be good citizens, so we
// return an error instead of crashing.
return nil, newError(ErrInvalidValue, "pks cannot be nil", nil)
}
if branch > Branch(len(pks)) {
return nil, newError(
ErrInvalidBranch, "branch number is bigger than number of public keys", nil)
}
if branch == 0 {
numKeys := len(pks)
res := make([]*hdkeychain.ExtendedKey, numKeys)
copy(res, pks)
// reverse pk
for i, j := 0, numKeys-1; i < j; i, j = i+1, j-1 {
res[i], res[j] = res[j], res[i]
}
return res, nil
}
tmp := make([]*hdkeychain.ExtendedKey, len(pks))
tmp[0] = pks[branch-1]
j := 1
for i := 0; i < len(pks); i++ {
if i != int(branch-1) {
tmp[j] = pks[i]
j++
}
}
return tmp, nil
}
// DepositScriptAddress calls DepositScript to get a multi-signature
// redemption script and returns the pay-to-script-hash-address for that script.
func (p *Pool) DepositScriptAddress(seriesID uint32, branch Branch, index Index) (btcutil.Address, error) {
script, err := p.DepositScript(seriesID, branch, index)
if err != nil {
return nil, err
}
return p.addressFor(script)
}
func (p *Pool) addressFor(script []byte) (btcutil.Address, error) {
scriptHash := btcutil.Hash160(script)
return btcutil.NewAddressScriptHashFromHash(scriptHash, p.manager.ChainParams())
}
// DepositScript constructs and returns a multi-signature redemption script where
// a certain number (Series.reqSigs) of the public keys belonging to the series
// with the given ID are required to sign the transaction for it to be successful.
func (p *Pool) DepositScript(seriesID uint32, branch Branch, index Index) ([]byte, error) {
series := p.Series(seriesID)
if series == nil {
str := fmt.Sprintf("series #%d does not exist", seriesID)
return nil, newError(ErrSeriesNotExists, str, nil)
}
pubKeys, err := branchOrder(series.publicKeys, branch)
if err != nil {
return nil, err
}
pks := make([]*btcutil.AddressPubKey, len(pubKeys))
for i, key := range pubKeys {
child, err := key.Child(uint32(index))
// TODO: implement getting the next index until we find a valid one,
// in case there is a hdkeychain.ErrInvalidChild.
if err != nil {
str := fmt.Sprintf("child #%d for this pubkey %d does not exist", index, i)
return nil, newError(ErrKeyChain, str, err)
}
pubkey, err := child.ECPubKey()
if err != nil {
str := fmt.Sprintf("child #%d for this pubkey %d does not exist", index, i)
return nil, newError(ErrKeyChain, str, err)
}
pks[i], err = btcutil.NewAddressPubKey(pubkey.SerializeCompressed(),
p.manager.ChainParams())
if err != nil {
str := fmt.Sprintf(
"child #%d for this pubkey %d could not be converted to an address",
index, i)
return nil, newError(ErrKeyChain, str, err)
}
}
script, err := txscript.MultiSigScript(pks, int(series.reqSigs))
if err != nil {
str := fmt.Sprintf("error while making multisig script hash, %d", len(pks))
return nil, newError(ErrScriptCreation, str, err)
}
return script, nil
}
// ChangeAddress returns a new votingpool address for the given seriesID and
// index, on the 0th branch (which is reserved for change addresses). The series
// with the given ID must be active.
func (p *Pool) ChangeAddress(seriesID uint32, index Index) (*ChangeAddress, error) {
series := p.Series(seriesID)
if series == nil {
return nil, newError(ErrSeriesNotExists,
fmt.Sprintf("series %d does not exist", seriesID), nil)
}
if !series.active {
str := fmt.Sprintf("ChangeAddress must be on active series; series #%d is not", seriesID)
return nil, newError(ErrSeriesNotActive, str, nil)
}
script, err := p.DepositScript(seriesID, Branch(0), index)
if err != nil {
return nil, err
}
pAddr, err := p.poolAddress(seriesID, Branch(0), index, script)
if err != nil {
return nil, err
}
return &ChangeAddress{poolAddress: pAddr}, nil
}
// WithdrawalAddress queries the address manager for the P2SH address
// of the redeem script generated with the given series/branch/index and uses
// that to populate the returned WithdrawalAddress. This is done because we
// should only withdraw from previously used addresses but also because when
// processing withdrawals we may iterate over a huge number of addresses and
// it'd be too expensive to re-generate the redeem script for all of them.
// This method must be called with the manager unlocked.
func (p *Pool) WithdrawalAddress(ns, addrmgrNs walletdb.ReadBucket, seriesID uint32, branch Branch, index Index) (
*WithdrawalAddress, error) {
// TODO: Ensure the given series is hot.
addr, err := p.getUsedAddr(ns, addrmgrNs, seriesID, branch, index)
if err != nil {
return nil, err
}
if addr == nil {
str := fmt.Sprintf("cannot withdraw from unused addr (series: %d, branch: %d, index: %d)",
seriesID, branch, index)
return nil, newError(ErrWithdrawFromUnusedAddr, str, nil)
}
script, err := addr.Script()
if err != nil {
return nil, err
}
pAddr, err := p.poolAddress(seriesID, branch, index, script)
if err != nil {
return nil, err
}
return &WithdrawalAddress{poolAddress: pAddr}, nil
}
func (p *Pool) poolAddress(seriesID uint32, branch Branch, index Index, script []byte) (
*poolAddress, error) {
addr, err := p.addressFor(script)
if err != nil {
return nil, err
}
return &poolAddress{
pool: p, seriesID: seriesID, branch: branch, index: index, addr: addr,
script: script},
nil
}
// EmpowerSeries adds the given extended private key (in raw format) to the
// series with the given ID, thus allowing it to sign deposit/withdrawal
// scripts. The series with the given ID must exist, the key must be a valid
// private extended key and must match one of the series' extended public keys.
//
// This method must be called with the Pool's manager unlocked.
func (p *Pool) EmpowerSeries(ns walletdb.ReadWriteBucket, seriesID uint32, rawPrivKey string) error {
// make sure this series exists
series := p.Series(seriesID)
if series == nil {
str := fmt.Sprintf("series %d does not exist for this voting pool",
seriesID)
return newError(ErrSeriesNotExists, str, nil)
}
// Check that the private key is valid.
privKey, err := hdkeychain.NewKeyFromString(rawPrivKey)
if err != nil {
str := fmt.Sprintf("invalid extended private key %v", rawPrivKey)
return newError(ErrKeyChain, str, err)
}
if !privKey.IsPrivate() {
str := fmt.Sprintf(
"to empower a series you need the extended private key, not an extended public key %v",
privKey)
return newError(ErrKeyIsPublic, str, err)
}
pubKey, err := privKey.Neuter()
if err != nil {
str := fmt.Sprintf("invalid extended private key %v, can't convert to public key",
rawPrivKey)
return newError(ErrKeyNeuter, str, err)
}
lookingFor := pubKey.String()
found := false
// Make sure the private key has the corresponding public key in the series,
// to be able to empower it.
for i, publicKey := range series.publicKeys {
if publicKey.String() == lookingFor {
found = true
series.privateKeys[i] = privKey
}
}
if !found {
str := fmt.Sprintf(
"private Key does not have a corresponding public key in this series")
return newError(ErrKeysPrivatePublicMismatch, str, nil)
}
if err = p.saveSeriesToDisk(ns, seriesID, series); err != nil {
return err
}
return nil
}
// EnsureUsedAddr ensures we have entries in our used addresses DB for the given
// seriesID, branch and all indices up to the given one. It must be called with
// the manager unlocked.
func (p *Pool) EnsureUsedAddr(ns, addrmgrNs walletdb.ReadWriteBucket, seriesID uint32, branch Branch, index Index) error {
lastIdx, err := p.highestUsedIndexFor(ns, seriesID, branch)
if err != nil {
return err
}
if lastIdx == 0 {
// highestUsedIndexFor() returns 0 when there are no used addresses for a
// given seriesID/branch, so we do this to ensure there is an entry with
// index==0.
if err := p.addUsedAddr(ns, addrmgrNs, seriesID, branch, lastIdx); err != nil {
return err
}
}
lastIdx++
for lastIdx <= index {
if err := p.addUsedAddr(ns, addrmgrNs, seriesID, branch, lastIdx); err != nil {
return err
}
lastIdx++
}
return nil
}
// addUsedAddr creates a deposit script for the given seriesID/branch/index,
// ensures it is imported into the address manager and finaly adds the script
// hash to our used addresses DB. It must be called with the manager unlocked.
func (p *Pool) addUsedAddr(ns, addrmgrNs walletdb.ReadWriteBucket, seriesID uint32, branch Branch, index Index) error {
script, err := p.DepositScript(seriesID, branch, index)
if err != nil {
return err
}
// First ensure the address manager has our script. That way there's no way
// to have it in the used addresses DB but not in the address manager.
// TODO: Decide how far back we want the addr manager to rescan and set the
// BlockStamp height according to that.
manager, err := p.manager.FetchScopedKeyManager(waddrmgr.KeyScopeBIP0044)
if err != nil {
return err
}
_, err = manager.ImportScript(addrmgrNs, script, &waddrmgr.BlockStamp{})
if err != nil && err.(waddrmgr.ManagerError).ErrorCode != waddrmgr.ErrDuplicateAddress {
return err
}
encryptedHash, err := p.manager.Encrypt(waddrmgr.CKTPublic, btcutil.Hash160(script))
if err != nil {
return newError(ErrCrypto, "failed to encrypt script hash", err)
}
err = putUsedAddrHash(ns, p.ID, seriesID, branch, index, encryptedHash)
if err != nil {
return newError(ErrDatabase, "failed to store used addr script hash", err)
}
return nil
}
// getUsedAddr gets the script hash for the given series, branch and index from
// the used addresses DB and uses that to look up the ManagedScriptAddress
// from the address manager. It must be called with the manager unlocked.
func (p *Pool) getUsedAddr(ns, addrmgrNs walletdb.ReadBucket, seriesID uint32, branch Branch, index Index) (
waddrmgr.ManagedScriptAddress, error) {
mgr := p.manager
encryptedHash := getUsedAddrHash(ns, p.ID, seriesID, branch, index)
if encryptedHash == nil {
return nil, nil
}
hash, err := p.manager.Decrypt(waddrmgr.CKTPublic, encryptedHash)
if err != nil {
return nil, newError(ErrCrypto, "failed to decrypt stored script hash", err)
}
addr, err := btcutil.NewAddressScriptHashFromHash(hash, mgr.ChainParams())
if err != nil {
return nil, newError(ErrInvalidScriptHash, "failed to parse script hash", err)
}
mAddr, err := mgr.Address(addrmgrNs, addr)
if err != nil {
return nil, err
}
return mAddr.(waddrmgr.ManagedScriptAddress), nil
}
// highestUsedIndexFor returns the highest index from this Pool's used addresses
// with the given seriesID and branch. It returns 0 if there are no used
// addresses with the given seriesID and branch.
func (p *Pool) highestUsedIndexFor(ns walletdb.ReadBucket, seriesID uint32, branch Branch) (Index, error) {
return getMaxUsedIdx(ns, p.ID, seriesID, branch)
}
// String returns a string encoding of the underlying bitcoin payment address.
func (a *poolAddress) String() string {
return a.addr.EncodeAddress()
}
func (a *poolAddress) addrIdentifier() string {
return fmt.Sprintf("PoolAddress seriesID:%d, branch:%d, index:%d", a.seriesID, a.branch,
a.index)
}
func (a *poolAddress) redeemScript() []byte {
return a.script
}
func (a *poolAddress) series() *SeriesData {
return a.pool.Series(a.seriesID)
}
func (a *poolAddress) SeriesID() uint32 {
return a.seriesID
}
func (a *poolAddress) Branch() Branch {
return a.branch
}
func (a *poolAddress) Index() Index {
return a.index
}
// IsEmpowered returns true if this series is empowered (i.e. if it has
// at least one private key loaded).
func (s *SeriesData) IsEmpowered() bool {
for _, key := range s.privateKeys {
if key != nil {
return true
}
}
return false
}
func (s *SeriesData) getPrivKeyFor(pubKey *hdkeychain.ExtendedKey) (*hdkeychain.ExtendedKey, error) {
for i, key := range s.publicKeys {
if key.String() == pubKey.String() {
return s.privateKeys[i], nil
}
}
return nil, newError(ErrUnknownPubKey, fmt.Sprintf("unknown public key '%s'",
pubKey.String()), nil)
}

File diff suppressed because it is too large Load diff

View file

@ -1,491 +0,0 @@
// Copyright (c) 2015 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package votingpool
import (
"bytes"
"fmt"
"testing"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/waddrmgr"
)
func TestPoolEnsureUsedAddr(t *testing.T) {
tearDown, db, pool := TstCreatePool(t)
defer tearDown()
dbtx, err := db.BeginReadWriteTx()
if err != nil {
t.Fatal(err)
}
defer dbtx.Commit()
ns, addrmgrNs := TstRWNamespaces(dbtx)
var script []byte
var addr waddrmgr.ManagedScriptAddress
TstCreateSeries(t, dbtx, pool, []TstSeriesDef{{ReqSigs: 2, PubKeys: TstPubKeys[0:3], SeriesID: 1}})
idx := Index(0)
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
err = pool.EnsureUsedAddr(ns, addrmgrNs, 1, 0, idx)
})
if err != nil {
t.Fatalf("Failed to ensure used addresses: %v", err)
}
addr, err = pool.getUsedAddr(ns, addrmgrNs, 1, 0, 0)
if err != nil {
t.Fatalf("Failed to get addr from used addresses set: %v", err)
}
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
script, err = addr.Script()
})
if err != nil {
t.Fatalf("Failed to get script: %v", err)
}
wantScript, _ := pool.DepositScript(1, 0, 0)
if !bytes.Equal(script, wantScript) {
t.Fatalf("Script from looked up addr is not what we expect")
}
idx = Index(3)
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
err = pool.EnsureUsedAddr(ns, addrmgrNs, 1, 0, idx)
})
if err != nil {
t.Fatalf("Failed to ensure used addresses: %v", err)
}
for _, i := range []int{0, 1, 2, 3} {
addr, err = pool.getUsedAddr(ns, addrmgrNs, 1, 0, Index(i))
if err != nil {
t.Fatalf("Failed to get addr from used addresses set: %v", err)
}
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
script, err = addr.Script()
})
if err != nil {
t.Fatalf("Failed to get script: %v", err)
}
wantScript, _ := pool.DepositScript(1, 0, Index(i))
if !bytes.Equal(script, wantScript) {
t.Fatalf("Script from looked up addr is not what we expect")
}
}
}
func TestPoolGetUsedAddr(t *testing.T) {
tearDown, db, pool := TstCreatePool(t)
defer tearDown()
dbtx, err := db.BeginReadWriteTx()
if err != nil {
t.Fatal(err)
}
defer dbtx.Commit()
ns, addrmgrNs := TstRWNamespaces(dbtx)
TstCreateSeries(t, dbtx, pool, []TstSeriesDef{{ReqSigs: 2, PubKeys: TstPubKeys[0:3], SeriesID: 1}})
// Addr with series=1, branch=0, index=10 has never been used, so it should
// return nil.
addr, err := pool.getUsedAddr(ns, addrmgrNs, 1, 0, 10)
if err != nil {
t.Fatalf("Error when looking up used addr: %v", err)
}
if addr != nil {
t.Fatalf("Unused address found in used addresses DB: %v", addr)
}
// Now we add that addr to the used addresses DB and check that the value
// returned by getUsedAddr() is what we expect.
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
err = pool.addUsedAddr(ns, addrmgrNs, 1, 0, 10)
})
if err != nil {
t.Fatalf("Error when storing addr in used addresses DB: %v", err)
}
var script []byte
addr, err = pool.getUsedAddr(ns, addrmgrNs, 1, 0, 10)
if err != nil {
t.Fatalf("Error when looking up used addr: %v", err)
}
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
script, err = addr.Script()
})
if err != nil {
t.Fatalf("Failed to get script: %v", err)
}
wantScript, _ := pool.DepositScript(1, 0, 10)
if !bytes.Equal(script, wantScript) {
t.Fatalf("Script from looked up addr is not what we expect")
}
}
func TestSerializationErrors(t *testing.T) {
tearDown, db, pool := TstCreatePool(t)
defer tearDown()
dbtx, err := db.BeginReadWriteTx()
if err != nil {
t.Fatal(err)
}
defer dbtx.Commit()
_, addrmgrNs := TstRWNamespaces(dbtx)
tests := []struct {
version uint32
pubKeys []string
privKeys []string
reqSigs uint32
err ErrorCode
}{
{
version: 2,
pubKeys: TstPubKeys[0:3],
err: ErrSeriesVersion,
},
{
pubKeys: []string{"NONSENSE"},
// Not a valid length public key.
err: ErrSeriesSerialization,
},
{
pubKeys: TstPubKeys[0:3],
privKeys: TstPrivKeys[0:1],
// The number of public and private keys should be the same.
err: ErrSeriesSerialization,
},
{
pubKeys: TstPubKeys[0:1],
privKeys: []string{"NONSENSE"},
// Not a valid length private key.
err: ErrSeriesSerialization,
},
}
active := true
for testNum, test := range tests {
encryptedPubs, err := encryptKeys(test.pubKeys, pool.Manager(), waddrmgr.CKTPublic)
if err != nil {
t.Fatalf("Test #%d - Error encrypting pubkeys: %v", testNum, err)
}
var encryptedPrivs [][]byte
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
encryptedPrivs, err = encryptKeys(test.privKeys, pool.Manager(), waddrmgr.CKTPrivate)
})
if err != nil {
t.Fatalf("Test #%d - Error encrypting privkeys: %v", testNum, err)
}
row := &dbSeriesRow{
version: test.version,
active: active,
reqSigs: test.reqSigs,
pubKeysEncrypted: encryptedPubs,
privKeysEncrypted: encryptedPrivs}
_, err = serializeSeriesRow(row)
TstCheckError(t, fmt.Sprintf("Test #%d", testNum), err, test.err)
}
}
func TestSerialization(t *testing.T) {
tearDown, db, pool := TstCreatePool(t)
defer tearDown()
dbtx, err := db.BeginReadWriteTx()
if err != nil {
t.Fatal(err)
}
defer dbtx.Commit()
_, addrmgrNs := TstRWNamespaces(dbtx)
tests := []struct {
version uint32
active bool
pubKeys []string
privKeys []string
reqSigs uint32
}{
{
version: 1,
active: true,
pubKeys: TstPubKeys[0:1],
reqSigs: 1,
},
{
version: 0,
active: false,
pubKeys: TstPubKeys[0:1],
privKeys: TstPrivKeys[0:1],
reqSigs: 1,
},
{
pubKeys: TstPubKeys[0:3],
privKeys: []string{TstPrivKeys[0], "", ""},
reqSigs: 2,
},
{
pubKeys: TstPubKeys[0:5],
reqSigs: 3,
},
{
pubKeys: TstPubKeys[0:7],
privKeys: []string{"", TstPrivKeys[1], "", TstPrivKeys[3], "", "", ""},
reqSigs: 4,
},
}
var encryptedPrivs [][]byte
for testNum, test := range tests {
encryptedPubs, err := encryptKeys(test.pubKeys, pool.Manager(), waddrmgr.CKTPublic)
if err != nil {
t.Fatalf("Test #%d - Error encrypting pubkeys: %v", testNum, err)
}
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
encryptedPrivs, err = encryptKeys(test.privKeys, pool.Manager(), waddrmgr.CKTPrivate)
})
if err != nil {
t.Fatalf("Test #%d - Error encrypting privkeys: %v", testNum, err)
}
row := &dbSeriesRow{
version: test.version,
active: test.active,
reqSigs: test.reqSigs,
pubKeysEncrypted: encryptedPubs,
privKeysEncrypted: encryptedPrivs,
}
serialized, err := serializeSeriesRow(row)
if err != nil {
t.Fatalf("Test #%d - Error in serialization %v", testNum, err)
}
row, err = deserializeSeriesRow(serialized)
if err != nil {
t.Fatalf("Test #%d - Failed to deserialize %v %v", testNum, serialized, err)
}
if row.version != test.version {
t.Errorf("Serialization #%d - version mismatch: got %d want %d",
testNum, row.version, test.version)
}
if row.active != test.active {
t.Errorf("Serialization #%d - active mismatch: got %v want %v",
testNum, row.active, test.active)
}
if row.reqSigs != test.reqSigs {
t.Errorf("Serialization #%d - row reqSigs off. Got %d, want %d",
testNum, row.reqSigs, test.reqSigs)
}
if len(row.pubKeysEncrypted) != len(test.pubKeys) {
t.Errorf("Serialization #%d - Wrong no. of pubkeys. Got %d, want %d",
testNum, len(row.pubKeysEncrypted), len(test.pubKeys))
}
for i, encryptedPub := range encryptedPubs {
got := string(row.pubKeysEncrypted[i])
if got != string(encryptedPub) {
t.Errorf("Serialization #%d - Pubkey deserialization. Got %v, want %v",
testNum, got, string(encryptedPub))
}
}
if len(row.privKeysEncrypted) != len(row.pubKeysEncrypted) {
t.Errorf("Serialization #%d - no. privkeys (%d) != no. pubkeys (%d)",
testNum, len(row.privKeysEncrypted), len(row.pubKeysEncrypted))
}
for i, encryptedPriv := range encryptedPrivs {
got := string(row.privKeysEncrypted[i])
if got != string(encryptedPriv) {
t.Errorf("Serialization #%d - Privkey deserialization. Got %v, want %v",
testNum, got, string(encryptedPriv))
}
}
}
}
func TestDeserializationErrors(t *testing.T) {
t.Parallel()
tests := []struct {
serialized []byte
err ErrorCode
}{
{
serialized: make([]byte, seriesMaxSerial+1),
// Too many bytes (over seriesMaxSerial).
err: ErrSeriesSerialization,
},
{
serialized: make([]byte, seriesMinSerial-1),
// Not enough bytes (under seriesMinSerial).
err: ErrSeriesSerialization,
},
{
serialized: []byte{
1, 0, 0, 0, // 4 bytes (version)
0, // 1 byte (active)
2, 0, 0, 0, // 4 bytes (reqSigs)
3, 0, 0, 0, // 4 bytes (nKeys)
},
// Here we have the constant data but are missing any public/private keys.
err: ErrSeriesSerialization,
},
{
serialized: []byte{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
// Unsupported version.
err: ErrSeriesVersion,
},
}
for testNum, test := range tests {
_, err := deserializeSeriesRow(test.serialized)
TstCheckError(t, fmt.Sprintf("Test #%d", testNum), err, test.err)
}
}
func TestValidateAndDecryptKeys(t *testing.T) {
tearDown, db, pool := TstCreatePool(t)
defer tearDown()
dbtx, err := db.BeginReadWriteTx()
if err != nil {
t.Fatal(err)
}
defer dbtx.Commit()
_, addrmgrNs := TstRWNamespaces(dbtx)
rawPubKeys, err := encryptKeys(TstPubKeys[0:2], pool.Manager(), waddrmgr.CKTPublic)
if err != nil {
t.Fatalf("Failed to encrypt public keys: %v", err)
}
var rawPrivKeys [][]byte
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
rawPrivKeys, err = encryptKeys([]string{TstPrivKeys[0], ""}, pool.Manager(), waddrmgr.CKTPrivate)
})
if err != nil {
t.Fatalf("Failed to encrypt private keys: %v", err)
}
var pubKeys, privKeys []*hdkeychain.ExtendedKey
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
pubKeys, privKeys, err = validateAndDecryptKeys(rawPubKeys, rawPrivKeys, pool)
})
if err != nil {
t.Fatalf("Error when validating/decrypting keys: %v", err)
}
if len(pubKeys) != 2 {
t.Fatalf("Unexpected number of decrypted public keys: got %d, want 2", len(pubKeys))
}
if len(privKeys) != 2 {
t.Fatalf("Unexpected number of decrypted private keys: got %d, want 2", len(privKeys))
}
if pubKeys[0].String() != TstPubKeys[0] || pubKeys[1].String() != TstPubKeys[1] {
t.Fatalf("Public keys don't match: %v!=%v ", TstPubKeys[0:2], pubKeys)
}
if privKeys[0].String() != TstPrivKeys[0] || privKeys[1] != nil {
t.Fatalf("Private keys don't match: %v, %v", []string{TstPrivKeys[0], ""}, privKeys)
}
neuteredKey, err := privKeys[0].Neuter()
if err != nil {
t.Fatalf("Unable to neuter private key: %v", err)
}
if pubKeys[0].String() != neuteredKey.String() {
t.Errorf("Public key (%v) does not match neutered private key (%v)",
pubKeys[0].String(), neuteredKey.String())
}
}
func TestValidateAndDecryptKeysErrors(t *testing.T) {
tearDown, db, pool := TstCreatePool(t)
defer tearDown()
dbtx, err := db.BeginReadWriteTx()
if err != nil {
t.Fatal(err)
}
defer dbtx.Commit()
_, addrmgrNs := TstRWNamespaces(dbtx)
encryptedPubKeys, err := encryptKeys(TstPubKeys[0:1], pool.Manager(), waddrmgr.CKTPublic)
if err != nil {
t.Fatalf("Failed to encrypt public key: %v", err)
}
var encryptedPrivKeys [][]byte
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
encryptedPrivKeys, err = encryptKeys(TstPrivKeys[1:2], pool.Manager(), waddrmgr.CKTPrivate)
})
if err != nil {
t.Fatalf("Failed to encrypt private key: %v", err)
}
tests := []struct {
rawPubKeys [][]byte
rawPrivKeys [][]byte
err ErrorCode
}{
{
// Number of public keys does not match number of private keys.
rawPubKeys: [][]byte{[]byte(TstPubKeys[0])},
rawPrivKeys: [][]byte{},
err: ErrKeysPrivatePublicMismatch,
},
{
// Failure to decrypt public key.
rawPubKeys: [][]byte{[]byte(TstPubKeys[0])},
rawPrivKeys: [][]byte{[]byte(TstPrivKeys[0])},
err: ErrCrypto,
},
{
// Failure to decrypt private key.
rawPubKeys: encryptedPubKeys,
rawPrivKeys: [][]byte{[]byte(TstPrivKeys[0])},
err: ErrCrypto,
},
{
// One public and one private key, but they don't match.
rawPubKeys: encryptedPubKeys,
rawPrivKeys: encryptedPrivKeys,
err: ErrKeyMismatch,
},
}
for i, test := range tests {
TstRunWithManagerUnlocked(t, pool.Manager(), addrmgrNs, func() {
_, _, err = validateAndDecryptKeys(test.rawPubKeys, test.rawPrivKeys, pool)
})
TstCheckError(t, fmt.Sprintf("Test #%d", i), err, test.err)
}
}
func encryptKeys(keys []string, mgr *waddrmgr.Manager, keyType waddrmgr.CryptoKeyType) ([][]byte, error) {
encryptedKeys := make([][]byte, len(keys))
var err error
for i, key := range keys {
if key == "" {
encryptedKeys[i] = nil
} else {
encryptedKeys[i], err = mgr.Encrypt(keyType, []byte(key))
}
if err != nil {
return nil, err
}
}
return encryptedKeys, nil
}

View file

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

View file

@ -1,31 +0,0 @@
// Copyright (c) 2015 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package votingpool
// Sample data used across our tests.
var TstPrivKeys = []string{
"xprv9s21ZrQH143K2j9PK4CXkCu8sgxkpUxCF7p1KVwiV5tdnkeYzJXReUkxz5iB2FUzTXC1L15abCDG4RMxSYT5zhm67uvsnLYxuDhZfoFcB6a",
"xprv9s21ZrQH143K4PtW77ATQAKAGk7KAFFCzxFuAcWduoMEeQhCgWpuYWQvMGZknqdispUbgLZV1YPqFCbpzMJij8tSZ5xPSaZqPbchojeNuq7",
"xprv9s21ZrQH143K27XboWxXZGU5j7VZ9SqVBnmMQPKTbddiWAhuNzeLynKHaZTAti6N454tVUUcvy6u15DfuW68NCBUxry6ZsHHzqoA8UtzdMn",
"xprv9s21ZrQH143K2vb4DGQymRejLcZSksBHTYLxB7Stg1c7Lk9JxgEUGZTozwUKxoEWJPoGSdGnJY1TW7LNFQCWrpZjDdEXJeqJuDde6BmdD4P",
"xprv9s21ZrQH143K4JNmRvWeLc1PggzusKcDYV1y8fAMNDdb9Rm5X1AvGHizxEdhTVR3sc62XvifC6dLAXMuQesX1y6999xnDwQ3aVno8KviU9d",
"xprv9s21ZrQH143K3dxrqESqeHZ7pSwM6Uq77ssQADSBs7qdFs6dyRWmRcPyLUTQRpgB3EduNhJuWkCGG2LHjuUisw8KKfXJpPqYJ1MSPrZpe1z",
"xprv9s21ZrQH143K2nE8ENAMNksTTVxPrMxFNWUuwThMy2bcH9LHTtQDXSNq2pTNcbuq36n5A3J9pbXVqnq5LDXvqniFRLN299kW7Svnxsx9tQv",
"xprv9s21ZrQH143K3p93xF1oFeB6ey5ruUesWjuPxA9Z2R5wf6BLYfGXz7fg7NavWkQ2cx3Vm8w2HV9uKpSprNNHnenGeW9XhYDPSjwS9hyCs33",
"xprv9s21ZrQH143K3WxnnvPZ8SDGXndASvLTFwMLBVzNCVgs9rzP6rXgW92DLvozdyBm8T9bSQvrFm1jMpTJrRE6w1KY5tshFeDk9Nn3K6V5FYX",
}
var TstPubKeys = []string{
"xpub661MyMwAqRbcFDDrR5jY7LqsRioFDwg3cLjc7tML3RRcfYyhXqqgCH5SqMSQdpQ1Xh8EtVwcfm8psD8zXKPcRaCVSY4GCqbb3aMEs27GitE",
"xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9",
"xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh",
"xpub661MyMwAqRbcFQfXKHwz8ZbTtePwAKu8pmGYyVrWEM96DYUTWDYipMnHrFcemZHn13jcRMfsNU3UWQUudiaE7mhkWCHGFRMavF167DQM4Va",
"xpub661MyMwAqRbcGnTEXx3ehjx8EiqQGnL4uhwZw3ZxvZAa2E6E4YVAp63UoVtvm2vMDDF8BdPpcarcf7PWcEKvzHhxzAYw1zG23C2egeh82AR",
"xpub661MyMwAqRbcG83KwFyr1RVrNUmqVwYxV6nzxbqoRTNc8fRnWxq1yQiTBifTHhevcEM9ucZ1TqFS7Kv17Gd81cesv6RDrrvYS9SLPjPXhV5",
"xpub661MyMwAqRbcFGJbLPhMjtpC1XntFpg6jjQWjr6yXN8b9wfS1RiU5EhJt5L7qoFuidYawc3XJoLjT2PcjVpXryS3hn1WmSPCyvQDNuKsfgM",
"xpub661MyMwAqRbcGJDX4GYocn7qCzvMJwNisxpzkYZAakcvXtWV6CanXuz9xdfe5kTptFMJ4hDt2iTiT11zyN14u8R5zLvoZ1gnEVqNLxp1r3v",
"xpub661MyMwAqRbcG13FtwvZVaA15pTerP4JdAGvytPykqDr2fKXePqw3wLhCALPAixsE176jFkc2ac9K3tnF4KwaTRKUqFF5apWD6XL9LHCu7E",
}

File diff suppressed because it is too large Load diff

View file

@ -1,141 +0,0 @@
// Copyright (c) 2014 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package votingpool_test
import (
"bytes"
"testing"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
vp "github.com/btcsuite/btcwallet/votingpool"
)
func TestStartWithdrawal(t *testing.T) {
tearDown, db, pool, store := vp.TstCreatePoolAndTxStore(t)
defer tearDown()
dbtx, err := db.BeginReadWriteTx()
if err != nil {
t.Fatal(err)
}
defer dbtx.Commit()
ns, addrmgrNs := vp.TstRWNamespaces(dbtx)
txmgrNs := vp.TstTxStoreRWNamespace(dbtx)
mgr := pool.Manager()
masters := []*hdkeychain.ExtendedKey{
vp.TstCreateMasterKey(t, bytes.Repeat([]byte{0x00, 0x01}, 16)),
vp.TstCreateMasterKey(t, bytes.Repeat([]byte{0x02, 0x01}, 16)),
vp.TstCreateMasterKey(t, bytes.Repeat([]byte{0x03, 0x01}, 16))}
def := vp.TstCreateSeriesDef(t, pool, 2, masters)
vp.TstCreateSeries(t, dbtx, pool, []vp.TstSeriesDef{def})
// Create eligible inputs and the list of outputs we need to fulfil.
vp.TstCreateSeriesCreditsOnStore(t, dbtx, pool, def.SeriesID, []int64{5e6, 4e6}, store)
address1 := "34eVkREKgvvGASZW7hkgE2uNc1yycntMK6"
address2 := "3PbExiaztsSYgh6zeMswC49hLUwhTQ86XG"
requests := []vp.OutputRequest{
vp.TstNewOutputRequest(t, 1, address1, 4e6, mgr.ChainParams()),
vp.TstNewOutputRequest(t, 2, address2, 1e6, mgr.ChainParams()),
}
changeStart := vp.TstNewChangeAddress(t, pool, def.SeriesID, 0)
startAddr := vp.TstNewWithdrawalAddress(t, dbtx, pool, def.SeriesID, 0, 0)
lastSeriesID := def.SeriesID
dustThreshold := btcutil.Amount(1e4)
currentBlock := int32(vp.TstInputsBlock + vp.TstEligibleInputMinConfirmations + 1)
var status *vp.WithdrawalStatus
vp.TstRunWithManagerUnlocked(t, mgr, addrmgrNs, func() {
status, err = pool.StartWithdrawal(ns, addrmgrNs, 0, requests, *startAddr, lastSeriesID, *changeStart,
store, txmgrNs, currentBlock, dustThreshold)
})
if err != nil {
t.Fatal(err)
}
// Check that all outputs were successfully fulfilled.
checkWithdrawalOutputs(t, status, map[string]btcutil.Amount{address1: 4e6, address2: 1e6})
if status.Fees() != btcutil.Amount(1e3) {
t.Fatalf("Wrong amount for fees; got %v, want %v", status.Fees(), btcutil.Amount(1e3))
}
// This withdrawal generated a single transaction with just one change
// output, so the next change address will be on the same series with the
// index incremented by 1.
nextChangeAddr := status.NextChangeAddr()
if nextChangeAddr.SeriesID() != changeStart.SeriesID() {
t.Fatalf("Wrong nextChangeStart series; got %d, want %d", nextChangeAddr.SeriesID(),
changeStart.SeriesID())
}
if nextChangeAddr.Index() != changeStart.Index()+1 {
t.Fatalf("Wrong nextChangeStart index; got %d, want %d", nextChangeAddr.Index(),
changeStart.Index()+1)
}
// NOTE: The ntxid is deterministic so we hardcode it here, but if the test
// or the code is changed in a way that causes the generated transaction to
// change (e.g. different inputs/outputs), the ntxid will change too and
// this will have to be updated.
ntxid := vp.Ntxid("eb753083db55bd0ad2eb184bfd196a7ea8b90eaa000d9293e892999695af2519")
txSigs := status.Sigs()[ntxid]
// Finally we use SignTx() to construct the SignatureScripts (using the raw
// signatures). Must unlock the manager as signing involves looking up the
// redeem script, which is stored encrypted.
msgtx := status.TstGetMsgTx(ntxid)
vp.TstRunWithManagerUnlocked(t, mgr, addrmgrNs, func() {
if err = vp.SignTx(msgtx, txSigs, mgr, addrmgrNs, store, txmgrNs); err != nil {
t.Fatal(err)
}
})
// Any subsequent StartWithdrawal() calls with the same parameters will
// return the previously stored WithdrawalStatus.
var status2 *vp.WithdrawalStatus
vp.TstRunWithManagerUnlocked(t, mgr, addrmgrNs, func() {
status2, err = pool.StartWithdrawal(ns, addrmgrNs, 0, requests, *startAddr, lastSeriesID, *changeStart,
store, txmgrNs, currentBlock, dustThreshold)
})
if err != nil {
t.Fatal(err)
}
vp.TstCheckWithdrawalStatusMatches(t, *status, *status2)
}
func checkWithdrawalOutputs(
t *testing.T, wStatus *vp.WithdrawalStatus, amounts map[string]btcutil.Amount) {
fulfilled := wStatus.Outputs()
if len(fulfilled) != 2 {
t.Fatalf("Unexpected number of outputs in WithdrawalStatus; got %d, want %d",
len(fulfilled), 2)
}
for _, output := range fulfilled {
addr := output.Address()
amount, ok := amounts[addr]
if !ok {
t.Fatalf("Unexpected output addr: %s", addr)
}
status := output.Status()
if status != "success" {
t.Fatalf(
"Unexpected status for output %v; got '%s', want 'success'", output, status)
}
outpoints := output.Outpoints()
if len(outpoints) != 1 {
t.Fatalf(
"Unexpected number of outpoints for output %v; got %d, want 1", output,
len(outpoints))
}
gotAmount := outpoints[0].Amount()
if gotAmount != amount {
t.Fatalf("Unexpected amount for output %v; got %v, want %v", output, gotAmount, amount)
}
}
}

File diff suppressed because it is too large Load diff