lbcwallet/votingpool/factory_test.go
2018-05-23 19:38:56 -07:00

472 lines
15 KiB
Go

/*
* 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/roasbeef/btcd/chaincfg"
"github.com/roasbeef/btcd/chaincfg/chainhash"
"github.com/roasbeef/btcd/txscript"
"github.com/roasbeef/btcd/wire"
"github.com/roasbeef/btcutil"
"github.com/roasbeef/btcutil/hdkeychain"
"github.com/roasbeef/btcwallet/waddrmgr"
"github.com/roasbeef/btcwallet/walletdb"
"github.com/roasbeef/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)
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,
}
}