Implement the deposit side of Voting Pools

This contains the APIs to create and retrieve Voting Pools and Series (with
public/private keys) from a walletdb namespace, plus the generation of deposit
addresses (using m-of-n multi-sig P2SH scripts according to the series
configuration).
This commit is contained in:
Guilherme Salgado 2014-06-13 12:14:44 -05:00 committed by Dave Collins
parent 454d290b68
commit 24dcd206d2
17 changed files with 2913 additions and 34 deletions

View file

@ -31,6 +31,8 @@ func zero(b []byte) {
}
const (
// Expose secretbox's Overhead const here for convenience.
Overhead = secretbox.Overhead
KeySize = 32
NonceSize = 24
DefaultN = 16384 // 2^14

40
votingpool/README.md Normal file
View file

@ -0,0 +1,40 @@
votingpool
========
[![Build Status](https://travis-ci.org/conformal/btcwallet.png?branch=master)]
(https://travis-ci.org/conformal/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/conformal/btcwallet/votingpool?status.png)]
(http://godoc.org/github.com/conformal/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/conformal/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/conformal/btcwallet/votingpool
Package votingpool is licensed under the [copyfree](http://copyfree.org) ISC
License.

17
votingpool/cov_report.sh Normal file
View file

@ -0,0 +1,17 @@
#!/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

283
votingpool/db.go Normal file
View file

@ -0,0 +1,283 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool
import (
"bytes"
"encoding/binary"
"fmt"
"github.com/conformal/btcwallet/snacl"
"github.com/conformal/btcwallet/waddrmgr"
"github.com/conformal/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 (
// string representing a non-existent private key
seriesNullPrivKey = [seriesKeyLength]byte{}
)
type dbSeriesRow struct {
version uint32
active bool
reqSigs uint32
pubKeysEncrypted [][]byte
privKeysEncrypted [][]byte
}
// putPool stores a voting pool in the database, creating a bucket named
// after the voting pool id.
func putPool(tx walletdb.Tx, votingPoolID []byte) error {
_, err := tx.RootBucket().CreateBucket(votingPoolID)
if err != nil {
str := fmt.Sprintf("cannot create voting pool %v", votingPoolID)
return managerError(waddrmgr.ErrDatabase, str, err)
}
return nil
}
// loadAllSeries returns a map of all the series stored inside a voting pool
// bucket, keyed by id.
func loadAllSeries(tx walletdb.Tx, votingPoolID []byte) (map[uint32]*dbSeriesRow, error) {
bucket := tx.RootBucket().Bucket(votingPoolID)
allSeries := make(map[uint32]*dbSeriesRow)
err := bucket.ForEach(
func(k, v []byte) error {
seriesID := bytesToUint32(k)
series, err := deserializeSeriesRow(v)
if err != nil {
str := fmt.Sprintf("cannot deserialize series %v", v)
return managerError(waddrmgr.ErrSeriesStorage, str, err)
}
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(tx walletdb.Tx, votingPoolID []byte) bool {
bucket := tx.RootBucket().Bucket(votingPoolID)
return bucket != nil
}
// putSeries stores the given series inside a voting pool bucket named after
// votingPoolID. The voting pool bucket does not need to be created beforehand.
func putSeries(tx walletdb.Tx, votingPoolID []byte, version, ID uint32, active bool, reqSigs uint32, pubKeysEncrypted, privKeysEncrypted [][]byte) error {
row := &dbSeriesRow{
version: version,
active: active,
reqSigs: reqSigs,
pubKeysEncrypted: pubKeysEncrypted,
privKeysEncrypted: privKeysEncrypted,
}
return putSeriesRow(tx, votingPoolID, ID, row)
}
// putSeriesRow stores the given series row inside a voting pool bucket named
// after votingPoolID. The voting pool bucket does not need to be created
// beforehand.
func putSeriesRow(tx walletdb.Tx, votingPoolID []byte, ID uint32, row *dbSeriesRow) error {
bucket, err := tx.RootBucket().CreateBucketIfNotExists(votingPoolID)
if err != nil {
str := fmt.Sprintf("cannot create bucket %v", votingPoolID)
return managerError(waddrmgr.ErrDatabase, str, err)
}
serialized, err := serializeSeriesRow(row)
if err != nil {
str := fmt.Sprintf("cannot serialize series %v", row)
return managerError(waddrmgr.ErrSeriesStorage, str, err)
}
err = bucket.Put(uint32ToBytes(ID), serialized)
if err != nil {
str := fmt.Sprintf("cannot put series %v into bucket %v", serialized, votingPoolID)
return managerError(waddrmgr.ErrSeriesStorage, str, err)
}
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, managerError(waddrmgr.ErrSeriesStorage, str, nil)
}
// Maximum number of public keys is 15 and the same for public keys
// this gives us an upper bound.
if len(serializedSeries) > seriesMaxSerial {
str := fmt.Sprintf("serialized series is too long: %v",
serializedSeries)
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
}
// 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, managerError(waddrmgr.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, managerError(waddrmgr.ErrSeriesStorage, str, nil)
} else if len(serializedSeries) > current+int(nKeys)*seriesKeyLength*2 {
str := fmt.Sprintf("serialized series has too much data: %v",
serializedSeries)
return nil, managerError(waddrmgr.ErrSeriesStorage, str, nil)
}
// 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, managerError(waddrmgr.ErrSeriesStorage, str, nil)
}
if row.version > seriesMaxVersion {
str := fmt.Sprintf("serialization supports up to version %v, not %v",
seriesMaxVersion, row.version)
return nil, managerError(waddrmgr.ErrSeriesVersion, str, nil)
}
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, managerError(waddrmgr.ErrSeriesStorage, 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, managerError(waddrmgr.ErrSeriesStorage, str, nil)
} else {
serialized = append(serialized, privKeyEncrypted...)
}
}
return serialized, nil
}
// 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)
}

70
votingpool/doc.go Normal file
View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
Package votingpool provides voting pool functionality for btcwallet.
Overview
The purpose of the voting pool package is to make it possible to store
bitcoins using m-of-n multisig transactions. Each member of the pool
holds one of the n private keys needed to create a transaction and can
only create transactions that can spend the bitcoins if m - 1 other
members of the pool agree to it.
This package depends on the waddrmgr package, and in particular
instances of the waddrgmgr.Manager structure.
Creating a voting pool
A voting pool is created via the Create function. This function
accepts a database namespace which will be used to store all
information about the pool as well as a poolID.
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 based on a seriesID a branch number and an index
creates a pay-to-script-hash address, where the script is a multisig
script. The public keys used as inputs for generating the address are
generated from the public keys passed to CreateSeries. In [1] the
generated public keys correspend to the lowest level or the
'address_index' in the hierarchy.
Replacing a series
A series can be replaced via the ReplaceSeries method. It accepts
the same parameters as the CreateSeries method.
Documentation
[1] https://github.com/justusranvier/bips/blob/master/bip-draft-Hierarchy%20for%20Non-Colored%20Voting%20Pool%20Deterministic%20Multisig%20Wallets.mediawiki
*/
package votingpool

125
votingpool/example_test.go Normal file
View file

@ -0,0 +1,125 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool_test
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/conformal/btcnet"
"github.com/conformal/btcwallet/votingpool"
"github.com/conformal/btcwallet/waddrmgr"
"github.com/conformal/btcwallet/walletdb"
_ "github.com/conformal/btcwallet/walletdb/bdb"
)
func Example_basic() {
// This example demonstrates how to create a voting pool, create a
// series, get a deposit address from a series and lastly how to
// replace a series.
// Create a new wallet DB.
dir, err := ioutil.TempDir("", "pool_test")
if err != nil {
fmt.Printf("Failed to create db dir: %v\n", err)
return
}
db, err := walletdb.Create("bdb", filepath.Join(dir, "wallet.db"))
if err != nil {
fmt.Printf("Failed to create wallet DB: %v\n", err)
return
}
defer os.RemoveAll(dir)
defer db.Close()
// Create a new walletdb namespace for the address manager.
mgrNamespace, err := db.Namespace([]byte("waddrmgr"))
if err != nil {
fmt.Printf("Failed to create addr manager DB namespace: %v\n", err)
return
}
// Create the address manager
mgr, err := waddrmgr.Create(mgrNamespace, seed, pubPassphrase, privPassphrase,
&btcnet.MainNetParams, nil)
if err != nil {
fmt.Printf("Failed to create addr manager: %v\n", err)
return
}
defer mgr.Close()
// Create a walletdb for votingpools.
vpNamespace, err := db.Namespace([]byte("votingpool"))
if err != nil {
fmt.Printf("Failed to create VotingPool DB namespace: %v\n", err)
return
}
// Create the voting pool.
pool, err := votingpool.Create(vpNamespace, mgr, []byte{0x00})
if err != nil {
fmt.Printf("Voting Pool creation failed: %v\n", err)
return
}
// Create a 2-of-3 series.
apiVersion := uint32(1)
seriesID := uint32(1)
requiredSignatures := uint32(2)
pubKeys := []string{
"xpub661MyMwAqRbcFDDrR5jY7LqsRioFDwg3cLjc7tML3RRcfYyhXqqgCH5SqMSQdpQ1Xh8EtVwcfm8psD8zXKPcRaCVSY4GCqbb3aMEs27GitE",
"xpub661MyMwAqRbcGsxyD8hTmJFtpmwoZhy4NBBVxzvFU8tDXD2ME49A6JjQCYgbpSUpHGP1q4S2S1Pxv2EqTjwfERS5pc9Q2yeLkPFzSgRpjs9",
"xpub661MyMwAqRbcEbc4uYVXvQQpH9L3YuZLZ1gxCmj59yAhNy33vXxbXadmRpx5YZEupNSqWRrR7PqU6duS2FiVCGEiugBEa5zuEAjsyLJjKCh",
}
err = pool.CreateSeries(apiVersion, seriesID, requiredSignatures, pubKeys)
if err != nil {
fmt.Printf("Cannot create series: %v\n", err)
return
}
// Create a deposit address.
branch := uint32(0) // The change branch
index := uint32(1)
addr, err := pool.DepositScriptAddress(seriesID, branch, index)
if err != nil {
fmt.Printf("DepositScriptAddress failed for series: %d, branch: %d, index: %d\n",
seriesID, branch, index)
return
}
fmt.Println("Generated deposit address:", addr.EncodeAddress())
// Replace the existing series with a 3-of-5 series.
pubKeys = []string{
"xpub661MyMwAqRbcFQfXKHwz8ZbTtePwAKu8pmGYyVrWEM96DYUTWDYipMnHrFcemZHn13jcRMfsNU3UWQUudiaE7mhkWCHGFRMavF167DQM4Va",
"xpub661MyMwAqRbcGnTEXx3ehjx8EiqQGnL4uhwZw3ZxvZAa2E6E4YVAp63UoVtvm2vMDDF8BdPpcarcf7PWcEKvzHhxzAYw1zG23C2egeh82AR",
"xpub661MyMwAqRbcG83KwFyr1RVrNUmqVwYxV6nzxbqoRTNc8fRnWxq1yQiTBifTHhevcEM9ucZ1TqFS7Kv17Gd81cesv6RDrrvYS9SLPjPXhV5",
"xpub661MyMwAqRbcFGJbLPhMjtpC1XntFpg6jjQWjr6yXN8b9wfS1RiU5EhJt5L7qoFuidYawc3XJoLjT2PcjVpXryS3hn1WmSPCyvQDNuKsfgM",
"xpub661MyMwAqRbcGJDX4GYocn7qCzvMJwNisxpzkYZAakcvXtWV6CanXuz9xdfe5kTptFMJ4hDt2iTiT11zyN14u8R5zLvoZ1gnEVqNLxp1r3v",
"xpub661MyMwAqRbcG13FtwvZVaA15pTerP4JdAGvytPykqDr2fKXePqw3wLhCALPAixsE176jFkc2ac9K3tnF4KwaTRKUqFF5apWD6XL9LHCu7E",
}
requiredSignatures = 3
err = pool.ReplaceSeries(apiVersion, seriesID, requiredSignatures, pubKeys)
if err != nil {
fmt.Printf("Cannot replace series: %v\n", err)
return
}
// Output:
// Generated deposit address: 3QTzpc9d3tTbNLJLB7xwt87nWM38boAhAw
}

117
votingpool/internal_test.go Normal file
View file

@ -0,0 +1,117 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool
import (
"github.com/conformal/btcutil/hdkeychain"
"github.com/conformal/btcwallet/waddrmgr"
"github.com/conformal/btcwallet/walletdb"
)
// TstPutSeries transparently wraps the voting pool putSeries method.
func (vp *Pool) TstPutSeries(version, seriesID, reqSigs uint32, inRawPubKeys []string) error {
return vp.putSeries(version, seriesID, reqSigs, inRawPubKeys)
}
var TstBranchOrder = branchOrder
// TstExistsSeries checks whether a series is stored in the database.
func (vp *Pool) TstExistsSeries(seriesID uint32) (bool, error) {
return vp.existsSeries(seriesID)
}
// TstNamespace exposes the Pool's namespace as it's needed in some tests.
func (vp *Pool) TstNamespace() walletdb.Namespace {
return vp.namespace
}
// 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)
}
// SeriesRow mimics dbSeriesRow defined in db.go .
type SeriesRow struct {
Version uint32
Active bool
ReqSigs uint32
PubKeysEncrypted [][]byte
PrivKeysEncrypted [][]byte
}
// SerializeSeries wraps serializeSeriesRow by passing it a freshly-built
// dbSeriesRow.
func SerializeSeries(version uint32, active bool, reqSigs uint32, pubKeys, privKeys [][]byte) ([]byte, error) {
row := &dbSeriesRow{
version: version,
active: active,
reqSigs: reqSigs,
pubKeysEncrypted: pubKeys,
privKeysEncrypted: privKeys,
}
return serializeSeriesRow(row)
}
// DeserializeSeries wraps deserializeSeriesRow and returns a freshly-built
// SeriesRow.
func DeserializeSeries(serializedSeries []byte) (*SeriesRow, error) {
row, err := deserializeSeriesRow(serializedSeries)
if err != nil {
return nil, err
}
return &SeriesRow{
Version: row.version,
Active: row.active,
ReqSigs: row.reqSigs,
PubKeysEncrypted: row.pubKeysEncrypted,
PrivKeysEncrypted: row.privKeysEncrypted,
}, nil
}
var TstValidateAndDecryptKeys = validateAndDecryptKeys

620
votingpool/pool.go Normal file
View file

@ -0,0 +1,620 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool
import (
"fmt"
"sort"
"github.com/conformal/btcscript"
"github.com/conformal/btcutil"
"github.com/conformal/btcutil/hdkeychain"
"github.com/conformal/btcwallet/waddrmgr"
"github.com/conformal/btcwallet/walletdb"
)
const (
minSeriesPubKeys = 3
)
// 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
namespace walletdb.Namespace
}
// Create creates a new entry in the database with the given ID
// and returns the Pool representing it.
func Create(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID []byte) (*Pool, error) {
err := namespace.Update(
func(tx walletdb.Tx) error {
return putPool(tx, poolID)
})
if err != nil {
str := fmt.Sprintf("unable to add voting pool %v to db", poolID)
return nil, managerError(waddrmgr.ErrVotingPoolAlreadyExists, str, err)
}
return newPool(namespace, m, poolID), nil
}
// Load fetches the entry in the database with the given ID and returns the Pool
// representing it.
func Load(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID []byte) (*Pool, error) {
err := namespace.View(
func(tx walletdb.Tx) error {
if exists := existsPool(tx, poolID); !exists {
str := fmt.Sprintf("unable to find voting pool %v in db", poolID)
return managerError(waddrmgr.ErrVotingPoolNotExists, str, nil)
}
return nil
})
if err != nil {
return nil, err
}
vp := newPool(namespace, m, poolID)
if err = vp.LoadAllSeries(); err != nil {
return nil, err
}
return vp, nil
}
// newPool creates a new Pool instance.
func newPool(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID []byte) *Pool {
return &Pool{
ID: poolID,
seriesLookup: make(map[uint32]*SeriesData),
manager: m,
namespace: namespace,
}
}
// LoadAndGetDepositScript generates and returns a deposit script for the given seriesID,
// branch and index of the Pool identified by poolID.
func LoadAndGetDepositScript(namespace walletdb.Namespace, m *waddrmgr.Manager, poolID string, seriesID, branch, index uint32) ([]byte, error) {
pid := []byte(poolID)
vp, err := Load(namespace, m, pid)
if err != nil {
return nil, err
}
script, err := vp.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(namespace walletdb.Namespace, m *waddrmgr.Manager, version uint32,
poolID string, seriesID, reqSigs uint32, rawPubKeys []string) error {
pid := []byte(poolID)
vp, err := Load(namespace, m, pid)
if err != nil {
managerErr := err.(waddrmgr.ManagerError)
if managerErr.ErrorCode == waddrmgr.ErrVotingPoolNotExists {
vp, err = Create(namespace, m, pid)
if err != nil {
return err
}
} else {
return err
}
}
return vp.CreateSeries(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(namespace walletdb.Namespace, m *waddrmgr.Manager, version uint32,
poolID string, seriesID, reqSigs uint32, rawPubKeys []string) error {
pid := []byte(poolID)
vp, err := Load(namespace, m, pid)
if err != nil {
return err
}
return vp.ReplaceSeries(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(namespace walletdb.Namespace, m *waddrmgr.Manager,
poolID string, seriesID uint32, rawPrivKey string) error {
pid := []byte(poolID)
pool, err := Load(namespace, m, pid)
if err != nil {
return err
}
return pool.EmpowerSeries(seriesID, rawPrivKey)
}
// GetSeries returns the series with the given ID, or nil if it doesn't
// exist.
func (vp *Pool) GetSeries(seriesID uint32) *SeriesData {
series, exists := vp.seriesLookup[seriesID]
if !exists {
return nil
}
return series
}
// saveSeriesToDisk stores the given series ID and data in the database,
// first encrypting the public/private extended keys.
func (vp *Pool) saveSeriesToDisk(seriesID uint32, data *SeriesData) error {
var err error
encryptedPubKeys := make([][]byte, len(data.publicKeys))
for i, pubKey := range data.publicKeys {
encryptedPubKeys[i], err = vp.manager.Encrypt(
waddrmgr.CKTPublic, []byte(pubKey.String()))
if err != nil {
str := fmt.Sprintf("key %v failed encryption", pubKey)
return managerError(waddrmgr.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 = vp.manager.Encrypt(
waddrmgr.CKTPrivate, []byte(privKey.String()))
}
if err != nil {
str := fmt.Sprintf("key %v failed encryption", privKey)
return managerError(waddrmgr.ErrCrypto, str, err)
}
}
err = vp.namespace.Update(func(tx walletdb.Tx) error {
return putSeries(tx, vp.ID, data.version, seriesID, data.active,
data.reqSigs, encryptedPubKeys, encryptedPrivKeys)
})
if err != nil {
str := fmt.Sprintf("cannot put series #%d into db", seriesID)
return managerError(waddrmgr.ErrSeriesStorage, str, err)
}
return 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, managerError(waddrmgr.ErrKeyDuplicate, str, nil)
}
seenKeys[rawPubKey] = true
key, err := hdkeychain.NewKeyFromString(rawPubKey)
if err != nil {
str := fmt.Sprintf("invalid extended public key %v", rawPubKey)
return nil, managerError(waddrmgr.ErrKeyChain, str, err)
}
if key.IsPrivate() {
str := fmt.Sprintf("private keys not accepted: %v", rawPubKey)
return nil, managerError(waddrmgr.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.
func (vp *Pool) putSeries(version, seriesID, reqSigs uint32, inRawPubKeys []string) error {
if len(inRawPubKeys) < minSeriesPubKeys {
str := fmt.Sprintf("need at least %d public keys to create a series", minSeriesPubKeys)
return managerError(waddrmgr.ErrTooFewPublicKeys, str, nil)
}
if reqSigs > uint32(len(inRawPubKeys)) {
str := fmt.Sprintf(
"the number of required signatures cannot be more than the number of keys")
return managerError(waddrmgr.ErrTooManyReqSignatures, str, nil)
}
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 = vp.saveSeriesToDisk(seriesID, data)
if err != nil {
return err
}
vp.seriesLookup[seriesID] = data
return nil
}
// CreateSeries will create and return a new non-existing series.
//
// - rawPubKeys has to contain three or more public keys;
// - reqSigs has to be less or equal than the number of public keys in rawPubKeys.
func (vp *Pool) CreateSeries(version, seriesID, reqSigs uint32, rawPubKeys []string) error {
if series := vp.GetSeries(seriesID); series != nil {
str := fmt.Sprintf("series #%d already exists", seriesID)
return managerError(waddrmgr.ErrSeriesAlreadyExists, str, nil)
}
return vp.putSeries(version, seriesID, reqSigs, rawPubKeys)
}
// ReplaceSeries will replace an already existing series.
//
// - rawPubKeys has to contain three or more public keys
// - reqSigs has to be less or equal than the number of public keys in rawPubKeys.
func (vp *Pool) ReplaceSeries(version, seriesID, reqSigs uint32, rawPubKeys []string) error {
series := vp.GetSeries(seriesID)
if series == nil {
str := fmt.Sprintf("series #%d does not exist, cannot replace it", seriesID)
return managerError(waddrmgr.ErrSeriesNotExists, str, nil)
}
if series.IsEmpowered() {
str := fmt.Sprintf("series #%d has private keys and cannot be replaced", seriesID)
return managerError(waddrmgr.ErrSeriesAlreadyEmpowered, str, nil)
}
return vp.putSeries(version, seriesID, reqSigs, rawPubKeys)
}
// decryptExtendedKey uses Manager.Decrypt() to decrypt the encrypted byte slice and return
// an extended (public or private) key representing it.
func (vp *Pool) decryptExtendedKey(keyType waddrmgr.CryptoKeyType, encrypted []byte) (*hdkeychain.ExtendedKey, error) {
decrypted, err := vp.manager.Decrypt(keyType, encrypted)
if err != nil {
str := fmt.Sprintf("cannot decrypt key %v", encrypted)
return nil, managerError(waddrmgr.ErrCrypto, str, err)
}
result, err := hdkeychain.NewKeyFromString(string(decrypted))
zero(decrypted)
if err != nil {
str := fmt.Sprintf("cannot get key from string %v", decrypted)
return nil, managerError(waddrmgr.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.
func validateAndDecryptKeys(rawPubKeys, rawPrivKeys [][]byte, vp *Pool) (pubKeys, privKeys []*hdkeychain.ExtendedKey, err error) {
pubKeys = make([]*hdkeychain.ExtendedKey, len(rawPubKeys))
privKeys = make([]*hdkeychain.ExtendedKey, len(rawPrivKeys))
if len(pubKeys) != len(privKeys) {
return nil, nil, managerError(waddrmgr.ErrKeysPrivatePublicMismatch,
"the pub key and priv key arrays should have the same number of elements",
nil)
}
for i, encryptedPub := range rawPubKeys {
pubKey, err := vp.decryptExtendedKey(waddrmgr.CKTPublic, encryptedPub)
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 = vp.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, managerError(waddrmgr.ErrKeyNeuter, str, err)
}
if pubKey.String() != checkPubKey.String() {
str := fmt.Sprintf("public key %v different than expected %v",
pubKey, checkPubKey)
return nil, nil, managerError(waddrmgr.ErrKeyMismatch, str, nil)
}
}
}
return 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.
func (vp *Pool) LoadAllSeries() error {
var series map[uint32]*dbSeriesRow
err := vp.namespace.View(func(tx walletdb.Tx) error {
var err error
series, err = loadAllSeries(tx, vp.ID)
return err
})
if err != nil {
return err
}
for id, series := range series {
pubKeys, privKeys, err := validateAndDecryptKeys(
series.pubKeysEncrypted, series.privKeysEncrypted, vp)
if err != nil {
return err
}
vp.seriesLookup[id] = &SeriesData{
publicKeys: pubKeys,
privateKeys: privKeys,
reqSigs: series.reqSigs,
}
}
return nil
}
// existsSeries checks whether a series is stored in the database.
// Used solely by the series creation test.
func (vp *Pool) existsSeries(seriesID uint32) (bool, error) {
var exists bool
err := vp.namespace.View(
func(tx walletdb.Tx) error {
bucket := tx.RootBucket().Bucket(vp.ID)
if bucket == nil {
exists = false
return nil
}
exists = bucket.Get(uint32ToBytes(seriesID)) != nil
return nil
})
if err != nil {
return false, err
}
return exists, nil
}
// Change the order of the pubkeys based on branch number.
// Given the three pubkeys ABC, this would mean:
// - branch 0: CBA (reversed)
// - branch 1: ABC (first key priority)
// - branch 2: BAC (second key priority)
// - branch 3: CAB (third key priority)
func branchOrder(pks []*hdkeychain.ExtendedKey, branch uint32) ([]*hdkeychain.ExtendedKey, error) {
if pks == nil {
// This really shouldn't happen, but we want to be good citizens, so we
// return an error instead of crashing.
return nil, managerError(waddrmgr.ErrInvalidValue, "pks cannot be nil", nil)
}
if branch > uint32(len(pks)) {
return nil, managerError(waddrmgr.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 constructs a multi-signature redemption script using DepositScript
// and returns the pay-to-script-hash-address for that script.
func (vp *Pool) DepositScriptAddress(seriesID, branch, index uint32) (btcutil.Address, error) {
script, err := vp.DepositScript(seriesID, branch, index)
if err != nil {
return nil, err
}
scriptHash := btcutil.Hash160(script)
return btcutil.NewAddressScriptHashFromHash(scriptHash, vp.manager.Net())
}
// DepositScript constructs and returns a multi-signature redemption script where
// a certain number (Series.reqSigs) of the public keys belonging to the series
// with the given ID are required to sign the transaction for it to be successful.
func (vp *Pool) DepositScript(seriesID, branch, index uint32) ([]byte, error) {
series := vp.GetSeries(seriesID)
if series == nil {
str := fmt.Sprintf("series #%d does not exist", seriesID)
return nil, managerError(waddrmgr.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(index)
// TODO: implement getting the next index until we find a valid one,
// in case there is a hdkeychain.ErrInvalidChild.
if err != nil {
str := fmt.Sprintf("child #%d for this pubkey %d does not exist", index, i)
return nil, managerError(waddrmgr.ErrKeyChain, str, err)
}
pubkey, err := child.ECPubKey()
if err != nil {
str := fmt.Sprintf("child #%d for this pubkey %d does not exist", index, i)
return nil, managerError(waddrmgr.ErrKeyChain, str, err)
}
pks[i], err = btcutil.NewAddressPubKey(pubkey.SerializeCompressed(), vp.manager.Net())
if err != nil {
str := fmt.Sprintf(
"child #%d for this pubkey %d could not be converted to an address",
index, i)
return nil, managerError(waddrmgr.ErrKeyChain, str, err)
}
}
script, err := btcscript.MultiSigScript(pks, int(series.reqSigs))
if err != nil {
str := fmt.Sprintf("error while making multisig script hash, %d", len(pks))
return nil, managerError(waddrmgr.ErrScriptCreation, str, err)
}
return script, nil
}
// EmpowerSeries adds the given extended private key (in raw format) to the
// series with the given ID, thus allowing it to sign deposit/withdrawal
// scripts. The series with the given ID must exist, the key must be a valid
// private extended key and must match one of the series' extended public keys.
func (vp *Pool) EmpowerSeries(seriesID uint32, rawPrivKey string) error {
// make sure this series exists
series := vp.GetSeries(seriesID)
if series == nil {
str := fmt.Sprintf("series %d does not exist for this voting pool",
seriesID)
return managerError(waddrmgr.ErrSeriesNotExists, str, nil)
}
// Check that the private key is valid.
privKey, err := hdkeychain.NewKeyFromString(rawPrivKey)
if err != nil {
str := fmt.Sprintf("invalid extended private key %v", rawPrivKey)
return managerError(waddrmgr.ErrKeyChain, str, err)
}
if !privKey.IsPrivate() {
str := fmt.Sprintf(
"to empower a series you need the extended private key, not an extended public key %v",
privKey)
return managerError(waddrmgr.ErrKeyIsPublic, str, err)
}
pubKey, err := privKey.Neuter()
if err != nil {
str := fmt.Sprintf("invalid extended private key %v, can't convert to public key",
rawPrivKey)
return managerError(waddrmgr.ErrKeyNeuter, str, err)
}
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 managerError(waddrmgr.ErrKeysPrivatePublicMismatch, str, nil)
}
err = vp.saveSeriesToDisk(seriesID, series)
if err != nil {
return err
}
return nil
}
// 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
}
// managerError creates a waddrmgr.ManagerError given a set of arguments.
// XXX(lars): We should probably make our own votingpoolError function.
func managerError(c waddrmgr.ErrorCode, desc string, err error) waddrmgr.ManagerError {
return waddrmgr.ManagerError{ErrorCode: c, Description: desc, Err: err}
}
// zero sets all bytes in the passed slice to zero. This is used to
// explicitly clear private key material from memory.
//
// XXX(lars) there exists currently around 4-5 other zero functions
// with at least 3 different implementations. We should try to
// consolidate these.
func zero(b []byte) {
for i := range b {
b[i] ^= b[i]
}
}

1385
votingpool/pool_test.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,92 @@
github.com/conformal/btcwallet/votingpool/db.go serializeSeriesRow 100.00% (29/29)
github.com/conformal/btcwallet/votingpool/pool.go branchOrder 100.00% (19/19)
github.com/conformal/btcwallet/votingpool/pool.go convertAndValidatePubKeys 100.00% (16/16)
github.com/conformal/btcwallet/votingpool/input_selection.go Credits.Less 100.00% (12/12)
github.com/conformal/btcwallet/votingpool/pool.go Pool.decryptExtendedKey 100.00% (10/10)
github.com/conformal/btcwallet/votingpool/pool.go Pool.ReplaceSeries 100.00% (8/8)
github.com/conformal/btcwallet/votingpool/input_selection.go AddressRange.NumAddresses 100.00% (7/7)
github.com/conformal/btcwallet/votingpool/pool.go Create 100.00% (5/5)
github.com/conformal/btcwallet/votingpool/db.go putPool 100.00% (5/5)
github.com/conformal/btcwallet/votingpool/pool.go Pool.DepositScriptAddress 100.00% (5/5)
github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.addTxIn 100.00% (4/4)
github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.addTxOut 100.00% (4/4)
github.com/conformal/btcwallet/votingpool/pool.go CanonicalKeyOrder 100.00% (4/4)
github.com/conformal/btcwallet/votingpool/pool.go @81:3 100.00% (4/4)
github.com/conformal/btcwallet/votingpool/pool.go Pool.GetSeries 100.00% (4/4)
github.com/conformal/btcwallet/votingpool/pool.go seriesData.IsEmpowered 100.00% (4/4)
github.com/conformal/btcwallet/votingpool/pool.go Pool.CreateSeries 100.00% (4/4)
github.com/conformal/btcwallet/votingpool/pool.go Pool.existsSeries 100.00% (3/3)
github.com/conformal/btcwallet/votingpool/db.go uint32ToBytes 100.00% (3/3)
github.com/conformal/btcwallet/votingpool/pool.go @398:27 100.00% (3/3)
github.com/conformal/btcwallet/votingpool/pool.go zero 100.00% (2/2)
github.com/conformal/btcwallet/votingpool/db.go putSeries 100.00% (2/2)
github.com/conformal/btcwallet/votingpool/db.go existsPool 100.00% (2/2)
github.com/conformal/btcwallet/votingpool/withdrawal.go Ntxid 100.00% (2/2)
github.com/conformal/btcwallet/votingpool/withdrawal.go NewOutputRequest 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.Index 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go init 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go estimateSize 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go calculateFee 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/db.go bytesToUint32 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.isTooBig 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/error.go newError 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/input_selection.go Credit.TxSha 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/input_selection.go Credit.OutputIndex 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/input_selection.go Credit.Address 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/input_selection.go newCredit 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/input_selection.go Credits.Len 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/input_selection.go Credits.Swap 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Outpoints 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Address 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/input_selection.go Pool.isCharterOutput 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/pool.go @67:3 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/pool.go newPool 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Status 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.addOutpoint 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalStatus.Outputs 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go WithdrawalOutput.Amount 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go ChangeAddress.Next 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/pool.go @205:28 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.Addr 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.SeriesID 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/pool.go managerError 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/withdrawal.go votingPoolAddress.Branch 100.00% (1/1)
github.com/conformal/btcwallet/votingpool/pool.go Pool.EmpowerSeries 96.43% (27/28)
github.com/conformal/btcwallet/votingpool/db.go deserializeSeriesRow 94.87% (37/39)
github.com/conformal/btcwallet/votingpool/pool.go Pool.putSeries 93.75% (15/16)
github.com/conformal/btcwallet/votingpool/pool.go validateAndDecryptKeys 92.31% (24/26)
github.com/conformal/btcwallet/votingpool/input_selection.go Pool.getEligibleInputsFromSeries 86.36% (19/22)
github.com/conformal/btcwallet/votingpool/input_selection.go Pool.getEligibleInputs 85.71% (6/7)
github.com/conformal/btcwallet/votingpool/pool.go Load 85.71% (6/7)
github.com/conformal/btcwallet/votingpool/input_selection.go Pool.isCreditEligible 85.71% (6/7)
github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.finalizeCurrentTx 84.21% (16/19)
github.com/conformal/btcwallet/votingpool/input_selection.go groupCreditsByAddr 83.33% (10/12)
github.com/conformal/btcwallet/votingpool/db.go loadAllSeries 83.33% (5/6)
github.com/conformal/btcwallet/votingpool/withdrawal.go Pool.ChangeAddress 83.33% (5/6)
github.com/conformal/btcwallet/votingpool/pool.go LoadAndCreateSeries 80.00% (8/10)
github.com/conformal/btcwallet/votingpool/pool.go Pool.LoadAllSeries 80.00% (8/10)
github.com/conformal/btcwallet/votingpool/pool.go LoadAndEmpowerSeries 80.00% (4/5)
github.com/conformal/btcwallet/votingpool/pool.go LoadAndReplaceSeries 80.00% (4/5)
github.com/conformal/btcwallet/votingpool/withdrawal.go Pool.WithdrawalAddress 80.00% (4/5)
github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.sign 75.76% (25/33)
github.com/conformal/btcwallet/votingpool/pool.go LoadAndGetDepositScript 75.00% (6/8)
github.com/conformal/btcwallet/votingpool/withdrawal.go OutputRequest.pkScript 75.00% (3/4)
github.com/conformal/btcwallet/votingpool/pool.go Pool.DepositScript 73.08% (19/26)
github.com/conformal/btcwallet/votingpool/withdrawal.go ValidateSigScripts 72.73% (8/11)
github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.fulfilNextOutput 72.41% (21/29)
github.com/conformal/btcwallet/votingpool/withdrawal.go SignMultiSigUTXO 71.43% (15/21)
github.com/conformal/btcwallet/votingpool/db.go @77:3 71.43% (5/7)
github.com/conformal/btcwallet/votingpool/withdrawal.go getRedeemScript 71.43% (5/7)
github.com/conformal/btcwallet/votingpool/pool.go Pool.saveSeriesToDisk 70.00% (14/20)
github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.fulfilOutputs 70.00% (7/10)
github.com/conformal/btcwallet/votingpool/withdrawal.go getPrivKey 70.00% (7/10)
github.com/conformal/btcwallet/votingpool/withdrawal.go Pool.Withdrawal 66.67% (12/18)
github.com/conformal/btcwallet/votingpool/pool.go @426:3 66.67% (4/6)
github.com/conformal/btcwallet/votingpool/error.go ErrorCode.String 66.67% (2/3)
github.com/conformal/btcwallet/votingpool/db.go putSeriesRow 53.85% (7/13)
github.com/conformal/btcwallet/votingpool/error.go Error.Error 0.00% (0/3)
github.com/conformal/btcwallet/votingpool/withdrawal.go withdrawal.updateStatusFor 0.00% (0/0)
github.com/conformal/btcwallet/votingpool/withdrawal.go currentTx.rollBackLastOutput 0.00% (0/0)
github.com/conformal/btcwallet/votingpool -------------------------------- 85.36% (554/649)

View file

@ -125,7 +125,7 @@ type managedAddress struct {
privKeyMutex sync.Mutex
}
// Enforce mangedAddress satisfies the ManagedPubKeyAddress interface.
// Enforce managedAddress satisfies the ManagedPubKeyAddress interface.
var _ ManagedPubKeyAddress = (*managedAddress)(nil)
// unlock decrypts and stores a pointer to the associated private key. It will

View file

@ -302,17 +302,17 @@ func putWatchingOnly(tx walletdb.Tx, watchingOnly bool) error {
}
if err := bucket.Put(watchingOnlyName, []byte{encoded}); err != nil {
str := "failed to store wathcing only flag"
str := "failed to store watching only flag"
return managerError(ErrDatabase, str, err)
}
return nil
}
// accountKey returns the account key to use in the database for a given account
// number.
func accountKey(account uint32) []byte {
// 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, account)
binary.LittleEndian.PutUint32(buf, number)
return buf
}
@ -404,7 +404,6 @@ func deserializeBIP0044AccountRow(accountID []byte, row *dbAccountRow) (*dbBIP00
func serializeBIP0044AccountRow(encryptedPubKey,
encryptedPrivKey []byte, nextExternalIndex, nextInternalIndex uint32,
name string) []byte {
// The serialized BIP0044 account raw data format is:
// <encpubkeylen><encpubkey><encprivkeylen><encprivkey><nextextidx>
// <nextintidx><namelen><name>
@ -438,7 +437,7 @@ func serializeBIP0044AccountRow(encryptedPubKey,
func fetchAccountInfo(tx walletdb.Tx, account uint32) (interface{}, error) {
bucket := tx.RootBucket().Bucket(acctBucketName)
accountID := accountKey(account)
accountID := uint32ToBytes(account)
serializedRow := bucket.Get(accountID)
if serializedRow == nil {
str := fmt.Sprintf("account %d not found", account)
@ -465,7 +464,7 @@ func putAccountRow(tx walletdb.Tx, account uint32, row *dbAccountRow) error {
bucket := tx.RootBucket().Bucket(acctBucketName)
// Write the serialized value keyed by the account number.
err := bucket.Put(accountKey(account), serializeAccountRow(row))
err := bucket.Put(uint32ToBytes(account), serializeAccountRow(row))
if err != nil {
str := fmt.Sprintf("failed to store account %d", account)
return managerError(ErrDatabase, str, err)
@ -781,12 +780,13 @@ func putChainedAddress(tx walletdb.Tx, addressID []byte, account uint32,
// Update the next index for the appropriate internal or external
// branch.
accountID := accountKey(account)
accountID := uint32ToBytes(account)
bucket := tx.RootBucket().Bucket(acctBucketName)
serializedAccount := bucket.Get(accountID)
// Deserialize the account row.
row, err := deserializeAccountRow(accountID, serializedAccount)
if err != nil {
return err
}
@ -1228,7 +1228,7 @@ func upgradeManager(namespace walletdb.Namespace) error {
return managerError(ErrDatabase, str, err)
}
// Save the most recent manager version if it isn't already
// Save the most recent database version if it isn't already
// there, otherwise keep track of it for potential upgrades.
verBytes := mainBucket.Get(mgrVersionName)
if verBytes == nil {

View file

@ -117,7 +117,7 @@ Requesting Existing Addresses
In addition to generating new addresses, access to old addresses is often
required. Most notably, to sign transactions in order to redeem them. The
Address function provides this capability and returns a ManagedAddress
Address function provides this capability and returns a ManagedAddress.
Importing Addresses

View file

@ -58,7 +58,7 @@ const (
ErrDatabase ErrorCode = iota
// ErrKeyChain indicates an error with the key chain typically either
// due to the inability to create and extended key or deriving a child
// due to the inability to create an extended key or deriving a child
// extended key. When this error code is set, the Err field of the
// ManagerError will be set to the underlying error.
ErrKeyChain
@ -74,54 +74,54 @@ const (
// key type has been selected.
ErrInvalidKeyType
// ErrNoExist indicates the manager does not exist.
// ErrNoExist indicates that the specified database does not exist.
ErrNoExist
// ErrAlreadyExists indicates the specified manager already exists.
// ErrAlreadyExists indicates that the specified database already exists.
ErrAlreadyExists
// ErrCoinTypeTooHigh indicates the coin type specified in the provided
// ErrCoinTypeTooHigh indicates that the coin type specified in the provided
// network parameters is higher than the max allowed value as defined
// by the maxCoinType constant.
ErrCoinTypeTooHigh
// ErrAccountNumTooHigh indicates the specified account number is higher
// ErrAccountNumTooHigh indicates that the specified account number is higher
// than the max allowed value as defined by the MaxAccountNum constant.
ErrAccountNumTooHigh
// ErrLocked indicates the an operation which requires the address
// manager to be unlocked was requested on a locked address manager.
// ErrLocked indicates that an operation, which requires the account
// manager to be unlocked, was requested on a locked account manager.
ErrLocked
// ErrWatchingOnly indicates the an operation which requires the address
// manager to have access to private data was requested on a
// watching-only address manager.
// ErrWatchingOnly indicates that an operation, which requires the
// account manager to have access to private data, was requested on
// a watching-only account manager.
ErrWatchingOnly
// ErrInvalidAccount indicates the requested account is not valid.
// ErrInvalidAccount indicates that the requested account is not valid.
ErrInvalidAccount
// ErrAddressNotFound indicates the requested address is not known to
// the address manager.
// ErrAddressNotFound indicates that the requested address is not known to
// the account manager.
ErrAddressNotFound
// ErrAccountNotFound indicates the requested account is not known to
// the address manager.
// ErrAccountNotFound indicates that the requested account is not known to
// the account manager.
ErrAccountNotFound
// ErrDuplicate indicates an address already exists.
// ErrDuplicate indicates that an address already exists.
ErrDuplicate
// ErrTooManyAddresses indicates more than the maximum allowed number of
// ErrTooManyAddresses indicates that more than the maximum allowed number of
// addresses per account have been requested.
ErrTooManyAddresses
// ErrWrongPassphrase inidicates the specified password is incorrect.
// This could be for either the public and private master keys.
// ErrWrongPassphrase indicates that the specified passphrase is incorrect.
// This could be for either public or private master keys.
ErrWrongPassphrase
// ErrWrongNet indicates the private key to be imported is not for the
// the same network the account mangaer is configured for.
// ErrWrongNet indicates that the private key to be imported is not for the
// the same network the account manager is configured for.
ErrWrongNet
)
@ -144,6 +144,26 @@ var errorCodeStrings = map[ErrorCode]string{
ErrTooManyAddresses: "ErrTooManyAddresses",
ErrWrongPassphrase: "ErrWrongPassphrase",
ErrWrongNet: "ErrWrongNet",
// The following error codes are defined in pool_error.go.
ErrSeriesStorage: "ErrSeriesStorage",
ErrSeriesVersion: "ErrSeriesVersion",
ErrSeriesNotExists: "ErrSeriesNotExists",
ErrSeriesAlreadyExists: "ErrSeriesAlreadyExists",
ErrSeriesAlreadyEmpowered: "ErrSeriesAlreadyEmpowered",
ErrKeyIsPrivate: "ErrKeyIsPrivate",
ErrKeyIsPublic: "ErrKeyIsPublic",
ErrKeyNeuter: "ErrKeyNeuter",
ErrKeyMismatch: "ErrKeyMismatch",
ErrKeysPrivatePublicMismatch: "ErrKeysPrivatePublicMismatch",
ErrKeyDuplicate: "ErrKeyDuplicate",
ErrTooFewPublicKeys: "ErrTooFewPublicKeys",
ErrVotingPoolAlreadyExists: "ErrVotingPoolAlreadyExists",
ErrVotingPoolNotExists: "ErrVotingPoolNotExists",
ErrScriptCreation: "ErrScriptCreation",
ErrTooManyReqSignatures: "ErrTooManyReqSignatures",
ErrInvalidBranch: "ErrInvalidBranch",
ErrInvalidValue: "ErrInvalidValue",
}
// String returns the ErrorCode as a human-readable name.

View file

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

View file

@ -47,7 +47,7 @@ func TstRunWithReplacedNewSecretKey(callback func()) {
callback()
}
// TstCheckPublicPassphrase return true if the provided public passphrase is
// TstCheckPublicPassphrase returns true if the provided public passphrase is
// correct for the manager.
func (m *Manager) TstCheckPublicPassphrase(pubPassphrase []byte) bool {
secretKey := snacl.SecretKey{Key: &snacl.CryptoKey{}}

92
waddrmgr/pool_error.go Normal file
View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package waddrmgr
// XXX: All errors defined here will soon be moved to the votingpool package, where they
// belong.
// Constants that identify voting pool-related errors.
// The codes start from 1000 to avoid confusion with the ones in error.go.
const (
// ErrSeriesStorage indicates that an error occurred while serializing
// or deserializing one or more series for storing into database.
ErrSeriesStorage ErrorCode = iota + 1000
// ErrSeriesVersion indicates that we've been asked to deal with
// a series whose version is unsupported
ErrSeriesVersion
// ErrSeriesNotExists indicates that an attempt has been made to access
// a series that does not exist.
ErrSeriesNotExists
// ErrSeriesAlreadyExists indicates that an attempt has been made to create
// a series that already exists.
ErrSeriesAlreadyExists
// ErrSeriesAlreadyEmpowered indicates that an already empowered series
// was used where a not empowered one was expected.
ErrSeriesAlreadyEmpowered
// ErrKeyIsPrivate indicates that a private key was used where a public
// one was expected.
ErrKeyIsPrivate
// ErrKeyIsPublic indicates that a public key was used where a private
// one was expected.
ErrKeyIsPublic
// ErrKeyNeuter indicates a problem when trying to neuter a private key.
ErrKeyNeuter
// ErrKeyMismatch indicates that the key is not the expected one.
ErrKeyMismatch
// ErrKeysPrivatePublicMismatch indicates that the number of private and
// public keys is not the same.
ErrKeysPrivatePublicMismatch
// ErrKeyDuplicate indicates that a key is duplicated.
ErrKeyDuplicate
// ErrTooFewPublicKeys indicates that a required minimum of public
// keys was not met.
ErrTooFewPublicKeys
// ErrVotingPoolAlreadyExists indicates that an attempt has been made to
// create a voting pool that already exists.
ErrVotingPoolAlreadyExists
// ErrVotingPoolNotExists indicates that an attempt has been made to access
// a voting pool that does not exist.
ErrVotingPoolNotExists
// ErrScriptCreation indicates that the creation of a deposit script failed.
ErrScriptCreation
// ErrTooManyReqSignatures indicates that too many required
// signatures are requested.
ErrTooManyReqSignatures
// ErrInvalidBranch indicates that the given branch number is not valid
// for a given set of public keys.
ErrInvalidBranch
// ErrInvalidValue indicates that the value of a given function argument
// is invalid.
ErrInvalidValue
)