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:
parent
454d290b68
commit
24dcd206d2
17 changed files with 2913 additions and 34 deletions
|
@ -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
40
votingpool/README.md
Normal 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
17
votingpool/cov_report.sh
Normal 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
283
votingpool/db.go
Normal 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
70
votingpool/doc.go
Normal 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
125
votingpool/example_test.go
Normal 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
117
votingpool/internal_test.go
Normal 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
620
votingpool/pool.go
Normal 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
1385
votingpool/pool_test.go
Normal file
File diff suppressed because it is too large
Load diff
92
votingpool/test_coverage.txt
Normal file
92
votingpool/test_coverage.txt
Normal 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)
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
92
waddrmgr/pool_error.go
Normal 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
|
||||
)
|
Loading…
Add table
Reference in a new issue