497ffc11f0
This is a rather monolithic commit that moves the old RPC server to its own package (rpc/legacyrpc), introduces a new RPC server using gRPC (rpc/rpcserver), and provides the ability to defer wallet loading until request at a later time by an RPC (--noinitialload). The legacy RPC server remains the default for now while the new gRPC server is not enabled by default. Enabling the new server requires setting a listen address (--experimenalrpclisten). This experimental flag is used to effectively feature gate the server until it is ready to use as a default. Both RPC servers can be run at the same time, but require binding to different listen addresses. In theory, with the legacy RPC server now living in its own package it should become much easier to unit test the handlers. This will be useful for any future changes to the package, as compatibility with Core's wallet is still desired. Type safety has also been improved in the legacy RPC server. Multiple handler types are now used for methods that do and do not require the RPC client as a dependency. This can statically help prevent nil pointer dereferences, and was very useful for catching bugs during refactoring. To synchronize the wallet loading process between the main package (the default) and through the gRPC WalletLoader service (with the --noinitialload option), as well as increasing the loose coupling of packages, a new wallet.Loader type has been added. All creating and loading of existing wallets is done through a single Loader instance, and callbacks can be attached to the instance to run after the wallet has been opened. This is how the legacy RPC server is associated with a loaded wallet, even after the wallet is loaded by a gRPC method in a completely unrelated package. Documentation for the new RPC server has been added to the rpc/documentation directory. The documentation includes a specification for the new RPC API, addresses how to make changes to the server implementation, and provides short example clients in several different languages. Some of the new RPC methods are not implementated exactly as described by the specification. These are considered bugs with the implementation, not the spec. Known bugs are commented as such.
953 lines
26 KiB
Go
953 lines
26 KiB
Go
/*
|
|
* Copyright (c) 2013-2015 The btcsuite developers
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
|
|
package wtxmgr
|
|
|
|
import (
|
|
"bytes"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/blockchain"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/btcsuite/btcwallet/walletdb"
|
|
)
|
|
|
|
// Block contains the minimum amount of data to uniquely identify any block on
|
|
// either the best or side chain.
|
|
type Block struct {
|
|
Hash wire.ShaHash
|
|
Height int32
|
|
}
|
|
|
|
// BlockMeta contains the unique identification for a block and any metadata
|
|
// pertaining to the block. At the moment, this additional metadata only
|
|
// includes the block time from the block header.
|
|
type BlockMeta struct {
|
|
Block
|
|
Time time.Time
|
|
}
|
|
|
|
// blockRecord is an in-memory representation of the block record saved in the
|
|
// database.
|
|
type blockRecord struct {
|
|
Block
|
|
Time time.Time
|
|
transactions []wire.ShaHash
|
|
}
|
|
|
|
// incidence records the block hash and blockchain height of a mined transaction.
|
|
// Since a transaction hash alone is not enough to uniquely identify a mined
|
|
// transaction (duplicate transaction hashes are allowed), the incidence is used
|
|
// instead.
|
|
type incidence struct {
|
|
txHash wire.ShaHash
|
|
block Block
|
|
}
|
|
|
|
// indexedIncidence records the transaction incidence and an input or output
|
|
// index.
|
|
type indexedIncidence struct {
|
|
incidence
|
|
index uint32
|
|
}
|
|
|
|
// debit records the debits a transaction record makes from previous wallet
|
|
// transaction credits.
|
|
type debit struct {
|
|
txHash wire.ShaHash
|
|
index uint32
|
|
amount btcutil.Amount
|
|
spends indexedIncidence
|
|
}
|
|
|
|
// credit describes a transaction output which was or is spendable by wallet.
|
|
type credit struct {
|
|
outPoint wire.OutPoint
|
|
block Block
|
|
amount btcutil.Amount
|
|
change bool
|
|
spentBy indexedIncidence // Index == ^uint32(0) if unspent
|
|
}
|
|
|
|
// TxRecord represents a transaction managed by the Store.
|
|
type TxRecord struct {
|
|
MsgTx wire.MsgTx
|
|
Hash wire.ShaHash
|
|
Received time.Time
|
|
SerializedTx []byte // Optional: may be nil
|
|
}
|
|
|
|
// NewTxRecord creates a new transaction record that may be inserted into the
|
|
// store. It uses memoization to save the transaction hash and the serialized
|
|
// transaction.
|
|
func NewTxRecord(serializedTx []byte, received time.Time) (*TxRecord, error) {
|
|
rec := &TxRecord{
|
|
Received: received,
|
|
SerializedTx: serializedTx,
|
|
}
|
|
err := rec.MsgTx.Deserialize(bytes.NewReader(serializedTx))
|
|
if err != nil {
|
|
str := "failed to deserialize transaction"
|
|
return nil, storeError(ErrInput, str, err)
|
|
}
|
|
copy(rec.Hash[:], wire.DoubleSha256(serializedTx))
|
|
return rec, nil
|
|
}
|
|
|
|
// NewTxRecordFromMsgTx creates a new transaction record that may be inserted
|
|
// into the store.
|
|
func NewTxRecordFromMsgTx(msgTx *wire.MsgTx, received time.Time) (*TxRecord, error) {
|
|
buf := bytes.NewBuffer(make([]byte, 0, msgTx.SerializeSize()))
|
|
err := msgTx.Serialize(buf)
|
|
if err != nil {
|
|
str := "failed to serialize transaction"
|
|
return nil, storeError(ErrInput, str, err)
|
|
}
|
|
rec := &TxRecord{
|
|
MsgTx: *msgTx,
|
|
Received: received,
|
|
SerializedTx: buf.Bytes(),
|
|
}
|
|
copy(rec.Hash[:], wire.DoubleSha256(rec.SerializedTx))
|
|
return rec, nil
|
|
}
|
|
|
|
// Credit is the type representing a transaction output which was spent or
|
|
// is still spendable by wallet. A UTXO is an unspent Credit, but not all
|
|
// Credits are UTXOs.
|
|
type Credit struct {
|
|
wire.OutPoint
|
|
BlockMeta
|
|
Amount btcutil.Amount
|
|
PkScript []byte
|
|
Received time.Time
|
|
FromCoinBase bool
|
|
}
|
|
|
|
// Store implements a transaction store for storing and managing wallet
|
|
// transactions.
|
|
type Store struct {
|
|
namespace walletdb.Namespace
|
|
|
|
// Event callbacks. These execute in the same goroutine as the wtxmgr
|
|
// caller.
|
|
NotifyUnspent func(hash *wire.ShaHash, index uint32)
|
|
}
|
|
|
|
// Open opens the wallet transaction store from a walletdb namespace. If the
|
|
// store does not exist, ErrNoExist is returned. Existing stores will be
|
|
// upgraded to new database formats as necessary.
|
|
func Open(namespace walletdb.Namespace) (*Store, error) {
|
|
// Open the store, upgrading to the latest version as needed.
|
|
err := openStore(namespace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Store{namespace, nil}, nil // TODO: set callbacks
|
|
}
|
|
|
|
// Create creates and opens a new persistent transaction store in the walletdb
|
|
// namespace. Creating the store when one already exists in this namespace will
|
|
// error with ErrAlreadyExists.
|
|
func Create(namespace walletdb.Namespace) (*Store, error) {
|
|
err := createStore(namespace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Store{namespace, nil}, nil // TODO: set callbacks
|
|
}
|
|
|
|
// moveMinedTx moves a transaction record from the unmined buckets to block
|
|
// buckets.
|
|
func (s *Store) moveMinedTx(ns walletdb.Bucket, rec *TxRecord, recKey, recVal []byte, block *BlockMeta) error {
|
|
log.Infof("Marking unconfirmed transaction %v mined in block %d",
|
|
&rec.Hash, block.Height)
|
|
|
|
// Insert block record as needed.
|
|
blockKey, blockVal := existsBlockRecord(ns, block.Height)
|
|
var err error
|
|
if blockVal == nil {
|
|
blockVal = valueBlockRecord(block, &rec.Hash)
|
|
} else {
|
|
blockVal, err = appendRawBlockRecord(blockVal, &rec.Hash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
err = putRawBlockRecord(ns, blockKey, blockVal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = putRawTxRecord(ns, recKey, recVal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
minedBalance, err := fetchMinedBalance(ns)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// For all mined transactions with unspent credits spent by this
|
|
// transaction, mark each spent, remove from the unspents map, and
|
|
// insert a debit record for the spent credit.
|
|
debitIncidence := indexedIncidence{
|
|
incidence: incidence{txHash: rec.Hash, block: block.Block},
|
|
// index set for each rec input below.
|
|
}
|
|
for i, input := range rec.MsgTx.TxIn {
|
|
unspentKey, credKey := existsUnspent(ns, &input.PreviousOutPoint)
|
|
if credKey == nil {
|
|
continue
|
|
}
|
|
debitIncidence.index = uint32(i)
|
|
amt, err := spendCredit(ns, credKey, &debitIncidence)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
minedBalance -= amt
|
|
err = deleteRawUnspent(ns, unspentKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = putDebit(ns, &rec.Hash, uint32(i), amt, &block.Block, credKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = deleteRawUnminedInput(ns, unspentKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// For each output of the record that is marked as a credit, if the
|
|
// output is marked as a credit by the unconfirmed store, remove the
|
|
// marker and mark the output as a credit in the db.
|
|
//
|
|
// Moved credits are added as unspents, even if there is another
|
|
// unconfirmed transaction which spends them.
|
|
cred := credit{
|
|
outPoint: wire.OutPoint{Hash: rec.Hash},
|
|
block: block.Block,
|
|
spentBy: indexedIncidence{index: ^uint32(0)},
|
|
}
|
|
it := makeUnminedCreditIterator(ns, &rec.Hash)
|
|
for it.next() {
|
|
// TODO: This should use the raw apis. The credit value (it.cv)
|
|
// can be moved from unmined directly to the credits bucket.
|
|
// The key needs a modification to include the block
|
|
// height/hash.
|
|
index, err := fetchRawUnminedCreditIndex(it.ck)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
amount, change, err := fetchRawUnminedCreditAmountChange(it.cv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cred.outPoint.Index = index
|
|
cred.amount = amount
|
|
cred.change = change
|
|
|
|
err = it.delete()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = putUnspentCredit(ns, &cred)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = putUnspent(ns, &cred.outPoint, &block.Block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
minedBalance += amount
|
|
}
|
|
if it.err != nil {
|
|
return it.err
|
|
}
|
|
|
|
err = putMinedBalance(ns, minedBalance)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return deleteRawUnmined(ns, rec.Hash[:])
|
|
}
|
|
|
|
// InsertTx records a transaction as belonging to a wallet's transaction
|
|
// history. If block is nil, the transaction is considered unspent, and the
|
|
// transaction's index must be unset.
|
|
func (s *Store) InsertTx(rec *TxRecord, block *BlockMeta) error {
|
|
return scopedUpdate(s.namespace, func(ns walletdb.Bucket) error {
|
|
if block == nil {
|
|
return s.insertMemPoolTx(ns, rec)
|
|
}
|
|
return s.insertMinedTx(ns, rec, block)
|
|
})
|
|
}
|
|
|
|
// insertMinedTx inserts a new transaction record for a mined transaction into
|
|
// the database. It is expected that the exact transation does not already
|
|
// exist in the unmined buckets, but unmined double spends (including mutations)
|
|
// are removed.
|
|
func (s *Store) insertMinedTx(ns walletdb.Bucket, rec *TxRecord, block *BlockMeta) error {
|
|
// If a transaction record for this tx hash and block already exist,
|
|
// there is nothing left to do.
|
|
k, v := existsTxRecord(ns, &rec.Hash, &block.Block)
|
|
if v != nil {
|
|
return nil
|
|
}
|
|
|
|
// If the exact tx (not a double spend) is already included but
|
|
// unconfirmed, move it to a block.
|
|
v = existsRawUnmined(ns, rec.Hash[:])
|
|
if v != nil {
|
|
return s.moveMinedTx(ns, rec, k, v, block)
|
|
}
|
|
|
|
// As there may be unconfirmed transactions that are invalidated by this
|
|
// transaction (either being duplicates, or double spends), remove them
|
|
// from the unconfirmed set. This also handles removing unconfirmed
|
|
// transaction spend chains if any other unconfirmed transactions spend
|
|
// outputs of the removed double spend.
|
|
err := s.removeDoubleSpends(ns, rec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If a block record does not yet exist for any transactions from this
|
|
// block, insert the record. Otherwise, update it by adding the
|
|
// transaction hash to the set of transactions from this block.
|
|
blockKey, blockValue := existsBlockRecord(ns, block.Height)
|
|
if blockValue == nil {
|
|
err = putBlockRecord(ns, block, &rec.Hash)
|
|
} else {
|
|
blockValue, err = appendRawBlockRecord(blockValue, &rec.Hash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = putRawBlockRecord(ns, blockKey, blockValue)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = putTxRecord(ns, rec, &block.Block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
minedBalance, err := fetchMinedBalance(ns)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add a debit record for each unspent credit spent by this tx.
|
|
spender := indexedIncidence{
|
|
incidence: incidence{
|
|
txHash: rec.Hash,
|
|
block: block.Block,
|
|
},
|
|
// index set for each iteration below
|
|
}
|
|
for i, input := range rec.MsgTx.TxIn {
|
|
unspentKey, credKey := existsUnspent(ns, &input.PreviousOutPoint)
|
|
if credKey == nil {
|
|
// Debits for unmined transactions are not explicitly
|
|
// tracked. Instead, all previous outputs spent by any
|
|
// unmined transaction are added to a map for quick
|
|
// lookups when it must be checked whether a mined
|
|
// output is unspent or not.
|
|
//
|
|
// Tracking individual debits for unmined transactions
|
|
// could be added later to simplify (and increase
|
|
// performance of) determining some details that need
|
|
// the previous outputs (e.g. determining a fee), but at
|
|
// the moment that is not done (and a db lookup is used
|
|
// for those cases instead). There is also a good
|
|
// chance that all unmined transaction handling will
|
|
// move entirely to the db rather than being handled in
|
|
// memory for atomicity reasons, so the simplist
|
|
// implementation is currently used.
|
|
continue
|
|
}
|
|
spender.index = uint32(i)
|
|
amt, err := spendCredit(ns, credKey, &spender)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = putDebit(ns, &rec.Hash, uint32(i), amt, &block.Block,
|
|
credKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
minedBalance -= amt
|
|
|
|
err = deleteRawUnspent(ns, unspentKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return putMinedBalance(ns, minedBalance)
|
|
}
|
|
|
|
// AddCredit marks a transaction record as containing a transaction output
|
|
// spendable by wallet. The output is added unspent, and is marked spent
|
|
// when a new transaction spending the output is inserted into the store.
|
|
//
|
|
// TODO(jrick): This should not be necessary. Instead, pass the indexes
|
|
// that are known to contain credits when a transaction or merkleblock is
|
|
// inserted into the store.
|
|
func (s *Store) AddCredit(rec *TxRecord, block *BlockMeta, index uint32, change bool) error {
|
|
if int(index) >= len(rec.MsgTx.TxOut) {
|
|
str := "transaction output does not exist"
|
|
return storeError(ErrInput, str, nil)
|
|
}
|
|
|
|
var isNew bool
|
|
err := scopedUpdate(s.namespace, func(ns walletdb.Bucket) error {
|
|
var err error
|
|
isNew, err = s.addCredit(ns, rec, block, index, change)
|
|
return err
|
|
})
|
|
if err == nil && isNew && s.NotifyUnspent != nil {
|
|
s.NotifyUnspent(&rec.Hash, index)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// addCredit is an AddCredit helper that runs in an update transaction. The
|
|
// bool return specifies whether the unspent output is newly added (true) or a
|
|
// duplicate (false).
|
|
func (s *Store) addCredit(ns walletdb.Bucket, rec *TxRecord, block *BlockMeta, index uint32, change bool) (bool, error) {
|
|
if block == nil {
|
|
k := canonicalOutPoint(&rec.Hash, index)
|
|
if existsRawUnminedCredit(ns, k) != nil {
|
|
return false, nil
|
|
}
|
|
v := valueUnminedCredit(btcutil.Amount(rec.MsgTx.TxOut[index].Value), change)
|
|
return true, putRawUnminedCredit(ns, k, v)
|
|
}
|
|
|
|
k, v := existsCredit(ns, &rec.Hash, index, &block.Block)
|
|
if v != nil {
|
|
return false, nil
|
|
}
|
|
|
|
txOutAmt := btcutil.Amount(rec.MsgTx.TxOut[index].Value)
|
|
log.Debugf("Marking transaction %v output %d (%v) spendable",
|
|
rec.Hash, index, txOutAmt)
|
|
|
|
cred := credit{
|
|
outPoint: wire.OutPoint{
|
|
Hash: rec.Hash,
|
|
Index: index,
|
|
},
|
|
block: block.Block,
|
|
amount: txOutAmt,
|
|
change: change,
|
|
spentBy: indexedIncidence{index: ^uint32(0)},
|
|
}
|
|
v = valueUnspentCredit(&cred)
|
|
err := putRawCredit(ns, k, v)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
minedBalance, err := fetchMinedBalance(ns)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
err = putMinedBalance(ns, minedBalance+txOutAmt)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return true, putUnspent(ns, &cred.outPoint, &block.Block)
|
|
}
|
|
|
|
// Rollback removes all blocks at height onwards, moving any transactions within
|
|
// each block to the unconfirmed pool.
|
|
func (s *Store) Rollback(height int32) error {
|
|
return scopedUpdate(s.namespace, func(ns walletdb.Bucket) error {
|
|
return s.rollback(ns, height)
|
|
})
|
|
}
|
|
|
|
func (s *Store) rollback(ns walletdb.Bucket, height int32) error {
|
|
minedBalance, err := fetchMinedBalance(ns)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Keep track of all credits that were removed from coinbase
|
|
// transactions. After detaching all blocks, if any transaction record
|
|
// exists in unmined that spends these outputs, remove them and their
|
|
// spend chains.
|
|
//
|
|
// It is necessary to keep these in memory and fix the unmined
|
|
// transactions later since blocks are removed in increasing order.
|
|
var coinBaseCredits []wire.OutPoint
|
|
|
|
it := makeBlockIterator(ns, height)
|
|
for it.next() {
|
|
b := &it.elem
|
|
|
|
log.Infof("Rolling back %d transactions from block %v height %d",
|
|
len(b.transactions), b.Hash, b.Height)
|
|
|
|
for i := range b.transactions {
|
|
txHash := &b.transactions[i]
|
|
|
|
recKey := keyTxRecord(txHash, &b.Block)
|
|
recVal := existsRawTxRecord(ns, recKey)
|
|
var rec TxRecord
|
|
err = readRawTxRecord(txHash, recVal, &rec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = deleteTxRecord(ns, txHash, &b.Block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Handle coinbase transactions specially since they are
|
|
// not moved to the unconfirmed store. A coinbase cannot
|
|
// contain any debits, but all credits should be removed
|
|
// and the mined balance decremented.
|
|
if blockchain.IsCoinBaseTx(&rec.MsgTx) {
|
|
op := wire.OutPoint{Hash: rec.Hash}
|
|
for i, output := range rec.MsgTx.TxOut {
|
|
k, v := existsCredit(ns, &rec.Hash,
|
|
uint32(i), &b.Block)
|
|
if v == nil {
|
|
continue
|
|
}
|
|
op.Index = uint32(i)
|
|
|
|
coinBaseCredits = append(coinBaseCredits, op)
|
|
|
|
unspentKey, credKey := existsUnspent(ns, &op)
|
|
if credKey != nil {
|
|
minedBalance -= btcutil.Amount(output.Value)
|
|
err = deleteRawUnspent(ns, unspentKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
err = deleteRawCredit(ns, k)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
err = putRawUnmined(ns, txHash[:], recVal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// For each debit recorded for this transaction, mark
|
|
// the credit it spends as unspent (as long as it still
|
|
// exists) and delete the debit. The previous output is
|
|
// recorded in the unconfirmed store for every previous
|
|
// output, not just debits.
|
|
for i, input := range rec.MsgTx.TxIn {
|
|
prevOut := &input.PreviousOutPoint
|
|
prevOutKey := canonicalOutPoint(&prevOut.Hash,
|
|
prevOut.Index)
|
|
err = putRawUnminedInput(ns, prevOutKey, rec.Hash[:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If this input is a debit, remove the debit
|
|
// record and mark the credit that it spent as
|
|
// unspent, incrementing the mined balance.
|
|
debKey, credKey, err := existsDebit(ns,
|
|
&rec.Hash, uint32(i), &b.Block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if debKey == nil {
|
|
continue
|
|
}
|
|
|
|
// unspendRawCredit does not error in case the
|
|
// no credit exists for this key, but this
|
|
// behavior is correct. Since blocks are
|
|
// removed in increasing order, this credit
|
|
// may have already been removed from a
|
|
// previously removed transaction record in
|
|
// this rollback.
|
|
var amt btcutil.Amount
|
|
amt, err = unspendRawCredit(ns, credKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = deleteRawDebit(ns, debKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If the credit was previously removed in the
|
|
// rollback, the credit amount is zero. Only
|
|
// mark the previously spent credit as unspent
|
|
// if it still exists.
|
|
if amt == 0 {
|
|
continue
|
|
}
|
|
unspentVal, err := fetchRawCreditUnspentValue(credKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
minedBalance += amt
|
|
err = putRawUnspent(ns, prevOutKey, unspentVal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// For each detached non-coinbase credit, move the
|
|
// credit output to unmined. If the credit is marked
|
|
// unspent, it is removed from the utxo set and the
|
|
// mined balance is decremented.
|
|
//
|
|
// TODO: use a credit iterator
|
|
for i, output := range rec.MsgTx.TxOut {
|
|
k, v := existsCredit(ns, &rec.Hash, uint32(i),
|
|
&b.Block)
|
|
if v == nil {
|
|
continue
|
|
}
|
|
|
|
amt, change, err := fetchRawCreditAmountChange(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
outPointKey := canonicalOutPoint(&rec.Hash, uint32(i))
|
|
unminedCredVal := valueUnminedCredit(amt, change)
|
|
err = putRawUnminedCredit(ns, outPointKey, unminedCredVal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = deleteRawCredit(ns, k)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
credKey := existsRawUnspent(ns, outPointKey)
|
|
if credKey != nil {
|
|
minedBalance -= btcutil.Amount(output.Value)
|
|
err = deleteRawUnspent(ns, outPointKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
err = it.delete()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if it.err != nil {
|
|
return it.err
|
|
}
|
|
|
|
for _, op := range coinBaseCredits {
|
|
opKey := canonicalOutPoint(&op.Hash, op.Index)
|
|
unminedKey := existsRawUnminedInput(ns, opKey)
|
|
if unminedKey != nil {
|
|
unminedVal := existsRawUnmined(ns, unminedKey)
|
|
var unminedRec TxRecord
|
|
copy(unminedRec.Hash[:], unminedKey) // Silly but need an array
|
|
err = readRawTxRecord(&unminedRec.Hash, unminedVal, &unminedRec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debugf("Transaction %v spends a removed coinbase "+
|
|
"output -- removing as well", unminedRec.Hash)
|
|
err = s.removeConflict(ns, &unminedRec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return putMinedBalance(ns, minedBalance)
|
|
}
|
|
|
|
// UnspentOutputs returns all unspent received transaction outputs.
|
|
// The order is undefined.
|
|
func (s *Store) UnspentOutputs() ([]Credit, error) {
|
|
var credits []Credit
|
|
err := scopedView(s.namespace, func(ns walletdb.Bucket) error {
|
|
var err error
|
|
credits, err = s.unspentOutputs(ns)
|
|
return err
|
|
})
|
|
return credits, err
|
|
}
|
|
|
|
func (s *Store) unspentOutputs(ns walletdb.Bucket) ([]Credit, error) {
|
|
var unspent []Credit
|
|
|
|
var op wire.OutPoint
|
|
var block Block
|
|
err := ns.Bucket(bucketUnspent).ForEach(func(k, v []byte) error {
|
|
err := readCanonicalOutPoint(k, &op)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if existsRawUnminedInput(ns, k) != nil {
|
|
// Output is spent by an unmined transaction.
|
|
// Skip this k/v pair.
|
|
return nil
|
|
}
|
|
err = readUnspentBlock(v, &block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
blockTime, err := fetchBlockTime(ns, block.Height)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// TODO(jrick): reading the entire transaction should
|
|
// be avoidable. Creating the credit only requires the
|
|
// output amount and pkScript.
|
|
rec, err := fetchTxRecord(ns, &op.Hash, &block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
txOut := rec.MsgTx.TxOut[op.Index]
|
|
cred := Credit{
|
|
OutPoint: op,
|
|
BlockMeta: BlockMeta{
|
|
Block: block,
|
|
Time: blockTime,
|
|
},
|
|
Amount: btcutil.Amount(txOut.Value),
|
|
PkScript: txOut.PkScript,
|
|
Received: rec.Received,
|
|
FromCoinBase: blockchain.IsCoinBaseTx(&rec.MsgTx),
|
|
}
|
|
unspent = append(unspent, cred)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
if _, ok := err.(Error); ok {
|
|
return nil, err
|
|
}
|
|
str := "failed iterating unspent bucket"
|
|
return nil, storeError(ErrDatabase, str, err)
|
|
}
|
|
|
|
err = ns.Bucket(bucketUnminedCredits).ForEach(func(k, v []byte) error {
|
|
if existsRawUnminedInput(ns, k) != nil {
|
|
// Output is spent by an unmined transaction.
|
|
// Skip to next unmined credit.
|
|
return nil
|
|
}
|
|
|
|
err := readCanonicalOutPoint(k, &op)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO(jrick): Reading/parsing the entire transaction record
|
|
// just for the output amount and script can be avoided.
|
|
recVal := existsRawUnmined(ns, op.Hash[:])
|
|
var rec TxRecord
|
|
err = readRawTxRecord(&op.Hash, recVal, &rec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
txOut := rec.MsgTx.TxOut[op.Index]
|
|
cred := Credit{
|
|
OutPoint: op,
|
|
BlockMeta: BlockMeta{
|
|
Block: Block{Height: -1},
|
|
},
|
|
Amount: btcutil.Amount(txOut.Value),
|
|
PkScript: txOut.PkScript,
|
|
Received: rec.Received,
|
|
FromCoinBase: blockchain.IsCoinBaseTx(&rec.MsgTx),
|
|
}
|
|
unspent = append(unspent, cred)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
if _, ok := err.(Error); ok {
|
|
return nil, err
|
|
}
|
|
str := "failed iterating unmined credits bucket"
|
|
return nil, storeError(ErrDatabase, str, err)
|
|
}
|
|
|
|
return unspent, nil
|
|
}
|
|
|
|
// Balance returns the spendable wallet balance (total value of all unspent
|
|
// transaction outputs) given a minimum of minConf confirmations, calculated
|
|
// at a current chain height of curHeight. Coinbase outputs are only included
|
|
// in the balance if maturity has been reached.
|
|
//
|
|
// Balance may return unexpected results if syncHeight is lower than the block
|
|
// height of the most recent mined transaction in the store.
|
|
func (s *Store) Balance(minConf, syncHeight int32) (btcutil.Amount, error) {
|
|
var amt btcutil.Amount
|
|
err := scopedView(s.namespace, func(ns walletdb.Bucket) error {
|
|
var err error
|
|
amt, err = s.balance(ns, minConf, syncHeight)
|
|
return err
|
|
})
|
|
return amt, err
|
|
}
|
|
|
|
func (s *Store) balance(ns walletdb.Bucket, minConf int32, syncHeight int32) (btcutil.Amount, error) {
|
|
bal, err := fetchMinedBalance(ns)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Subtract the balance for each credit that is spent by an unmined
|
|
// transaction.
|
|
var op wire.OutPoint
|
|
var block Block
|
|
err = ns.Bucket(bucketUnspent).ForEach(func(k, v []byte) error {
|
|
err := readCanonicalOutPoint(k, &op)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = readUnspentBlock(v, &block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if existsRawUnminedInput(ns, k) != nil {
|
|
_, v := existsCredit(ns, &op.Hash, op.Index, &block)
|
|
amt, err := fetchRawCreditAmount(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bal -= amt
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
if _, ok := err.(Error); ok {
|
|
return 0, err
|
|
}
|
|
str := "failed iterating unspent outputs"
|
|
return 0, storeError(ErrDatabase, str, err)
|
|
}
|
|
|
|
// Decrement the balance for any unspent credit with less than
|
|
// minConf confirmations and any (unspent) immature coinbase credit.
|
|
stopConf := minConf
|
|
if blockchain.CoinbaseMaturity > stopConf {
|
|
stopConf = blockchain.CoinbaseMaturity
|
|
}
|
|
lastHeight := syncHeight - stopConf
|
|
blockIt := makeReverseBlockIterator(ns)
|
|
for blockIt.prev() {
|
|
block := &blockIt.elem
|
|
|
|
if block.Height < lastHeight {
|
|
break
|
|
}
|
|
|
|
for i := range block.transactions {
|
|
txHash := &block.transactions[i]
|
|
rec, err := fetchTxRecord(ns, txHash, &block.Block)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
numOuts := uint32(len(rec.MsgTx.TxOut))
|
|
for i := uint32(0); i < numOuts; i++ {
|
|
// Avoid double decrementing the credit amount
|
|
// if it was already removed for being spent by
|
|
// an unmined tx.
|
|
opKey := canonicalOutPoint(txHash, i)
|
|
if existsRawUnminedInput(ns, opKey) != nil {
|
|
continue
|
|
}
|
|
|
|
_, v := existsCredit(ns, txHash, i, &block.Block)
|
|
if v == nil {
|
|
continue
|
|
}
|
|
amt, spent, err := fetchRawCreditAmountSpent(v)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if spent {
|
|
continue
|
|
}
|
|
confs := syncHeight - block.Height + 1
|
|
if confs < minConf || (blockchain.IsCoinBaseTx(&rec.MsgTx) &&
|
|
confs < blockchain.CoinbaseMaturity) {
|
|
bal -= amt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if blockIt.err != nil {
|
|
return 0, blockIt.err
|
|
}
|
|
|
|
// If unmined outputs are included, increment the balance for each
|
|
// output that is unspent.
|
|
if minConf == 0 {
|
|
err = ns.Bucket(bucketUnminedCredits).ForEach(func(k, v []byte) error {
|
|
if existsRawUnminedInput(ns, k) != nil {
|
|
// Output is spent by an unmined transaction.
|
|
// Skip to next unmined credit.
|
|
return nil
|
|
}
|
|
|
|
amount, err := fetchRawUnminedCreditAmount(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bal += amount
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
if _, ok := err.(Error); ok {
|
|
return 0, err
|
|
}
|
|
str := "failed to iterate over unmined credits bucket"
|
|
return 0, storeError(ErrDatabase, str, err)
|
|
}
|
|
}
|
|
|
|
return bal, nil
|
|
}
|