50869085eb
Add and test functions which can be used to write optional transaction labels to disk in their own bucket. These labels are keyed by txid and write the labels to disk using-length value encoding scheme. Although the length field is not required at present, it is added to allow future extensibility without a migration. This approach is chosen over adding this information to txRecords, Because a migration would be required to add a field after the variable Length serialized tx. The put label function will overwrite existing labels if called more than once for the same txid. User side validation of whether we want to override this label should be performed by calling code. Labels must be > 0 characters and <= 500 characters (an arbitrarily chosen limit).
1028 lines
29 KiB
Go
1028 lines
29 KiB
Go
// Copyright (c) 2013-2017 The btcsuite developers
|
|
// Copyright (c) 2015-2016 The Decred developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package wtxmgr
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/blockchain"
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/btcsuite/btcwallet/walletdb"
|
|
)
|
|
|
|
// TxLabelLimit is the length limit we impose on transaction labels.
|
|
const TxLabelLimit = 500
|
|
|
|
var (
|
|
// ErrEmptyLabel is returned when an attempt to write a label that is
|
|
// empty is made.
|
|
ErrEmptyLabel = errors.New("empty transaction label not allowed")
|
|
|
|
// ErrLabelTooLong is returned when an attempt to write a label that is
|
|
// to long is made.
|
|
ErrLabelTooLong = errors.New("transaction label exceeds limit")
|
|
|
|
// ErrNoLabelBucket is returned when the bucket holding optional
|
|
// transaction labels is not found. This occurs when no transactions
|
|
// have been labelled yet.
|
|
ErrNoLabelBucket = errors.New("labels bucket does not exist")
|
|
|
|
// ErrTxLabelNotFound is returned when no label is found for a
|
|
// transaction hash.
|
|
ErrTxLabelNotFound = errors.New("label for transaction not found")
|
|
)
|
|
|
|
// Block contains the minimum amount of data to uniquely identify any block on
|
|
// either the best or side chain.
|
|
type Block struct {
|
|
Hash chainhash.Hash
|
|
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 []chainhash.Hash
|
|
}
|
|
|
|
// 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 chainhash.Hash
|
|
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 chainhash.Hash
|
|
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 chainhash.Hash
|
|
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[:], chainhash.DoubleHashB(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(),
|
|
Hash: msgTx.TxHash(),
|
|
}
|
|
|
|
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 {
|
|
chainParams *chaincfg.Params
|
|
|
|
// Event callbacks. These execute in the same goroutine as the wtxmgr
|
|
// caller.
|
|
NotifyUnspent func(hash *chainhash.Hash, index uint32)
|
|
}
|
|
|
|
// Open opens the wallet transaction store from a walletdb namespace. If the
|
|
// store does not exist, ErrNoExist is returned.
|
|
func Open(ns walletdb.ReadBucket, chainParams *chaincfg.Params) (*Store, error) {
|
|
// Open the store.
|
|
err := openStore(ns)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s := &Store{chainParams, nil} // TODO: set callbacks
|
|
return s, nil
|
|
}
|
|
|
|
// Create creates 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(ns walletdb.ReadWriteBucket) error {
|
|
return createStore(ns)
|
|
}
|
|
|
|
// updateMinedBalance updates the mined balance within the store, if changed,
|
|
// after processing the given transaction record.
|
|
func (s *Store) updateMinedBalance(ns walletdb.ReadWriteBucket, rec *TxRecord,
|
|
block *BlockMeta) error {
|
|
|
|
// Fetch the mined balance in case we need to update it.
|
|
minedBalance, err := fetchMinedBalance(ns)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add a debit record for each unspent credit spent by this transaction.
|
|
// The index is set in each iteration below.
|
|
spender := indexedIncidence{
|
|
incidence: incidence{
|
|
txHash: rec.Hash,
|
|
block: block.Block,
|
|
},
|
|
}
|
|
|
|
newMinedBalance := minedBalance
|
|
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
|
|
}
|
|
|
|
// If this output is relevant to us, we'll mark the it as spent
|
|
// and remove its amount from the store.
|
|
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
|
|
}
|
|
if err := deleteRawUnspent(ns, unspentKey); err != nil {
|
|
return err
|
|
}
|
|
|
|
newMinedBalance -= amt
|
|
}
|
|
|
|
// 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
|
|
|
|
if err := putUnspentCredit(ns, &cred); err != nil {
|
|
return err
|
|
}
|
|
err = putUnspent(ns, &cred.outPoint, &block.Block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newMinedBalance += amount
|
|
}
|
|
if it.err != nil {
|
|
return it.err
|
|
}
|
|
|
|
// Update the balance if it has changed.
|
|
if newMinedBalance != minedBalance {
|
|
return putMinedBalance(ns, newMinedBalance)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// deleteUnminedTx deletes an unmined transaction from the store.
|
|
//
|
|
// NOTE: This should only be used once the transaction has been mined.
|
|
func (s *Store) deleteUnminedTx(ns walletdb.ReadWriteBucket, rec *TxRecord) error {
|
|
for _, input := range rec.MsgTx.TxIn {
|
|
prevOut := input.PreviousOutPoint
|
|
k := canonicalOutPoint(&prevOut.Hash, prevOut.Index)
|
|
if err := deleteRawUnminedInput(ns, k, rec.Hash); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for i := range rec.MsgTx.TxOut {
|
|
k := canonicalOutPoint(&rec.Hash, uint32(i))
|
|
if err := deleteRawUnminedCredit(ns, k); 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(ns walletdb.ReadWriteBucket, rec *TxRecord, block *BlockMeta) error {
|
|
if block == nil {
|
|
return s.insertMemPoolTx(ns, rec)
|
|
}
|
|
return s.insertMinedTx(ns, rec, block)
|
|
}
|
|
|
|
// RemoveUnminedTx attempts to remove an unmined transaction from the
|
|
// transaction store. This is to be used in the scenario that a transaction
|
|
// that we attempt to rebroadcast, turns out to double spend one of our
|
|
// existing inputs. This function we remove the conflicting transaction
|
|
// identified by the tx record, and also recursively remove all transactions
|
|
// that depend on it.
|
|
func (s *Store) RemoveUnminedTx(ns walletdb.ReadWriteBucket, rec *TxRecord) error {
|
|
// As we already have a tx record, we can directly call the
|
|
// removeConflict method. This will do the job of recursively removing
|
|
// this unmined transaction, and any transactions that depend on it.
|
|
return s.removeConflict(ns, rec)
|
|
}
|
|
|
|
// insertMinedTx inserts a new transaction record for a mined transaction into
|
|
// the database under the confirmed bucket. It guarantees that, if the
|
|
// tranasction was previously unconfirmed, then it will take care of cleaning up
|
|
// the unconfirmed state. All other unconfirmed double spend attempts will be
|
|
// removed as well.
|
|
func (s *Store) insertMinedTx(ns walletdb.ReadWriteBucket, rec *TxRecord,
|
|
block *BlockMeta) error {
|
|
|
|
// If a transaction record for this hash and block already exists, we
|
|
// can exit early.
|
|
if _, v := existsTxRecord(ns, &rec.Hash, &block.Block); v != nil {
|
|
return nil
|
|
}
|
|
|
|
// If a block record does not yet exist for any transactions from this
|
|
// block, insert a block record first. Otherwise, update it by adding
|
|
// the transaction hash to the set of transactions from this block.
|
|
var err error
|
|
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
|
|
}
|
|
if err := putTxRecord(ns, rec, &block.Block); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Determine if this transaction has affected our balance, and if so,
|
|
// update it.
|
|
if err := s.updateMinedBalance(ns, rec, block); err != nil {
|
|
return err
|
|
}
|
|
|
|
// If this transaction previously existed within the store as unmined,
|
|
// we'll need to remove it from the unmined bucket.
|
|
if v := existsRawUnmined(ns, rec.Hash[:]); v != nil {
|
|
log.Infof("Marking unconfirmed transaction %v mined in block %d",
|
|
&rec.Hash, block.Height)
|
|
|
|
if err := s.deleteUnminedTx(ns, rec); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
return s.removeDoubleSpends(ns, rec)
|
|
}
|
|
|
|
// 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(ns walletdb.ReadWriteBucket, 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)
|
|
}
|
|
|
|
isNew, err := s.addCredit(ns, rec, block, index, change)
|
|
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.ReadWriteBucket, rec *TxRecord, block *BlockMeta, index uint32, change bool) (bool, error) {
|
|
if block == nil {
|
|
// If the outpoint that we should mark as credit already exists
|
|
// within the store, either as unconfirmed or confirmed, then we
|
|
// have nothing left to do and can exit.
|
|
k := canonicalOutPoint(&rec.Hash, index)
|
|
if existsRawUnminedCredit(ns, k) != nil {
|
|
return false, nil
|
|
}
|
|
if _, tv := latestTxRecord(ns, &rec.Hash); tv != nil {
|
|
log.Tracef("Ignoring credit for existing confirmed transaction %v",
|
|
rec.Hash.String())
|
|
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(ns walletdb.ReadWriteBucket, height int32) error {
|
|
return s.rollback(ns, height)
|
|
}
|
|
|
|
func (s *Store) rollback(ns walletdb.ReadWriteBucket, 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
|
|
var heightsToRemove []int32
|
|
|
|
it := makeReverseBlockIterator(ns)
|
|
for it.prev() {
|
|
b := &it.elem
|
|
if it.elem.Height < height {
|
|
break
|
|
}
|
|
|
|
heightsToRemove = append(heightsToRemove, it.elem.Height)
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// reposition cursor before deleting this k/v pair and advancing to the
|
|
// previous.
|
|
it.reposition(it.elem.Height)
|
|
|
|
// Avoid cursor deletion until bolt issue #620 is resolved.
|
|
// err = it.delete()
|
|
// if err != nil {
|
|
// return err
|
|
// }
|
|
}
|
|
if it.err != nil {
|
|
return it.err
|
|
}
|
|
|
|
// Delete the block records outside of the iteration since cursor deletion
|
|
// is broken.
|
|
for _, h := range heightsToRemove {
|
|
err = deleteBlockRecord(ns, h)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, op := range coinBaseCredits {
|
|
opKey := canonicalOutPoint(&op.Hash, op.Index)
|
|
unminedSpendTxHashKeys := fetchUnminedInputSpendTxHashes(ns, opKey)
|
|
for _, unminedSpendTxHashKey := range unminedSpendTxHashKeys {
|
|
unminedVal := existsRawUnmined(ns, unminedSpendTxHashKey[:])
|
|
|
|
// If the spending transaction spends multiple outputs
|
|
// from the same transaction, we'll find duplicate
|
|
// entries within the store, so it's possible we're
|
|
// unable to find it if the conflicts have already been
|
|
// removed in a previous iteration.
|
|
if unminedVal == nil {
|
|
continue
|
|
}
|
|
|
|
var unminedRec TxRecord
|
|
unminedRec.Hash = unminedSpendTxHashKey
|
|
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(ns walletdb.ReadBucket) ([]Credit, error) {
|
|
var unspent []Credit
|
|
|
|
var op wire.OutPoint
|
|
var block Block
|
|
err := ns.NestedReadBucket(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 fmt.Errorf("unable to retrieve transaction %v: "+
|
|
"%v", op.Hash, 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.NestedReadBucket(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 fmt.Errorf("unable to retrieve raw transaction "+
|
|
"%v: %v", op.Hash, 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(ns walletdb.ReadBucket, 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.NestedReadBucket(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.
|
|
coinbaseMaturity := int32(s.chainParams.CoinbaseMaturity)
|
|
stopConf := minConf
|
|
if coinbaseMaturity > stopConf {
|
|
stopConf = coinbaseMaturity
|
|
}
|
|
lastHeight := syncHeight - stopConf
|
|
blockIt := makeReadReverseBlockIterator(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 < 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.NestedReadBucket(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
|
|
}
|
|
|
|
// PutTxLabel validates transaction labels and writes them to disk if they
|
|
// are non-zero and within the label length limit. The entry is keyed by the
|
|
// transaction hash:
|
|
// [0:32] Transaction hash (32 bytes)
|
|
//
|
|
// The label itself is written to disk in length value format:
|
|
// [0:2] Label length
|
|
// [2: +len] Label
|
|
func (s *Store) PutTxLabel(ns walletdb.ReadWriteBucket, txid chainhash.Hash,
|
|
label string) error {
|
|
|
|
if len(label) == 0 {
|
|
return ErrEmptyLabel
|
|
}
|
|
|
|
if len(label) > TxLabelLimit {
|
|
return ErrLabelTooLong
|
|
}
|
|
|
|
labelBucket, err := ns.CreateBucketIfNotExists(bucketTxLabels)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We expect the label length to be limited on creation, so we can
|
|
// store the label's length as a uint16.
|
|
labelLen := uint16(len(label))
|
|
|
|
var buf bytes.Buffer
|
|
|
|
var b [2]byte
|
|
binary.BigEndian.PutUint16(b[:], labelLen)
|
|
if _, err := buf.Write(b[:]); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := buf.WriteString(label); err != nil {
|
|
return err
|
|
}
|
|
|
|
return labelBucket.Put(txid[:], buf.Bytes())
|
|
}
|
|
|
|
// FetchTxLabel reads a transaction label from the tx labels bucket. If a label
|
|
// with 0 length was written, we return an error, since this is unexpected.
|
|
func FetchTxLabel(ns walletdb.ReadBucket, txid chainhash.Hash) (string, error) {
|
|
labelBucket := ns.NestedReadBucket(bucketTxLabels)
|
|
if labelBucket == nil {
|
|
return "", ErrNoLabelBucket
|
|
}
|
|
|
|
v := labelBucket.Get(txid[:])
|
|
if v == nil {
|
|
return "", ErrTxLabelNotFound
|
|
}
|
|
|
|
// If the label is empty, return an error.
|
|
length := binary.BigEndian.Uint16(v[0:2])
|
|
if length == 0 {
|
|
return "", ErrEmptyLabel
|
|
}
|
|
|
|
// Read the remainder of the bytes into a label string.
|
|
label := string(v[2:])
|
|
return label, nil
|
|
}
|