Merge pull request #505 from cfromknecht/nuke-votingpool
votingpool: nuke package
This commit is contained in:
commit
b386e6385e
22 changed files with 0 additions and 8272 deletions
|
@ -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.
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
592
votingpool/db.go
592
votingpool/db.go
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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
|
@ -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
Loading…
Reference in a new issue