1046 lines
36 KiB
Go
1046 lines
36 KiB
Go
// Copyright (c) 2015-2017 The btcsuite developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package votingpool
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"math"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/roasbeef/btcd/txscript"
|
|
"github.com/roasbeef/btcd/wire"
|
|
"github.com/roasbeef/btcutil"
|
|
"github.com/roasbeef/btcwallet/waddrmgr"
|
|
"github.com/roasbeef/btcwallet/walletdb"
|
|
"github.com/roasbeef/btcwallet/wtxmgr"
|
|
)
|
|
|
|
// Maximum tx size (in bytes). This should be the same as bitcoind's
|
|
// MAX_STANDARD_TX_SIZE.
|
|
const txMaxSize = 100000
|
|
|
|
// feeIncrement is the minimum transation fee (0.00001 BTC, measured in satoshis)
|
|
// added to transactions requiring a fee.
|
|
const feeIncrement = 1e3
|
|
|
|
type outputStatus byte
|
|
|
|
const (
|
|
statusSuccess outputStatus = iota
|
|
statusPartial
|
|
statusSplit
|
|
)
|
|
|
|
// OutBailmentID is the unique ID of a user's outbailment, comprising the
|
|
// name of the server the user connected to, and the transaction number,
|
|
// internal to that server.
|
|
type OutBailmentID string
|
|
|
|
// Ntxid is the normalized ID of a given bitcoin transaction, which is generated
|
|
// by hashing the serialized tx with blank sig scripts on all inputs.
|
|
type Ntxid string
|
|
|
|
// OutputRequest represents one of the outputs (address/amount) requested by a
|
|
// withdrawal, and includes information about the user's outbailment request.
|
|
type OutputRequest struct {
|
|
Address btcutil.Address
|
|
Amount btcutil.Amount
|
|
PkScript []byte
|
|
|
|
// The notary server that received the outbailment request.
|
|
Server string
|
|
|
|
// The server-specific transaction number for the outbailment request.
|
|
Transaction uint32
|
|
|
|
// cachedHash is used to cache the hash of the outBailmentID so it
|
|
// only has to be calculated once.
|
|
cachedHash []byte
|
|
}
|
|
|
|
// WithdrawalOutput represents a possibly fulfilled OutputRequest.
|
|
type WithdrawalOutput struct {
|
|
request OutputRequest
|
|
status outputStatus
|
|
// The outpoints that fulfill the OutputRequest. There will be more than one in case we
|
|
// need to split the request across multiple transactions.
|
|
outpoints []OutBailmentOutpoint
|
|
}
|
|
|
|
// OutBailmentOutpoint represents one of the outpoints created to fulfill an OutputRequest.
|
|
type OutBailmentOutpoint struct {
|
|
ntxid Ntxid
|
|
index uint32
|
|
amount btcutil.Amount
|
|
}
|
|
|
|
// changeAwareTx is just a wrapper around wire.MsgTx that knows about its change
|
|
// output, if any.
|
|
type changeAwareTx struct {
|
|
*wire.MsgTx
|
|
changeIdx int32 // -1 if there's no change output.
|
|
}
|
|
|
|
// WithdrawalStatus contains the details of a processed withdrawal, including
|
|
// the status of each requested output, the total amount of network fees and the
|
|
// next input and change addresses to use in a subsequent withdrawal request.
|
|
type WithdrawalStatus struct {
|
|
nextInputAddr WithdrawalAddress
|
|
nextChangeAddr ChangeAddress
|
|
fees btcutil.Amount
|
|
outputs map[OutBailmentID]*WithdrawalOutput
|
|
sigs map[Ntxid]TxSigs
|
|
transactions map[Ntxid]changeAwareTx
|
|
}
|
|
|
|
// withdrawalInfo contains all the details of an existing withdrawal, including
|
|
// the original request parameters and the WithdrawalStatus returned by
|
|
// StartWithdrawal.
|
|
type withdrawalInfo struct {
|
|
requests []OutputRequest
|
|
startAddress WithdrawalAddress
|
|
changeStart ChangeAddress
|
|
lastSeriesID uint32
|
|
dustThreshold btcutil.Amount
|
|
status WithdrawalStatus
|
|
}
|
|
|
|
// TxSigs is list of raw signatures (one for every pubkey in the multi-sig
|
|
// script) for a given transaction input. They should match the order of pubkeys
|
|
// in the script and an empty RawSig should be used when the private key for a
|
|
// pubkey is not known.
|
|
type TxSigs [][]RawSig
|
|
|
|
// RawSig represents one of the signatures included in the unlocking script of
|
|
// inputs spending from P2SH UTXOs.
|
|
type RawSig []byte
|
|
|
|
// byAmount defines the methods needed to satisify sort.Interface to
|
|
// sort a slice of OutputRequests by their amount.
|
|
type byAmount []OutputRequest
|
|
|
|
func (u byAmount) Len() int { return len(u) }
|
|
func (u byAmount) Less(i, j int) bool { return u[i].Amount < u[j].Amount }
|
|
func (u byAmount) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
|
|
|
|
// byOutBailmentID defines the methods needed to satisify sort.Interface to sort
|
|
// a slice of OutputRequests by their outBailmentIDHash.
|
|
type byOutBailmentID []OutputRequest
|
|
|
|
func (s byOutBailmentID) Len() int { return len(s) }
|
|
func (s byOutBailmentID) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
|
func (s byOutBailmentID) Less(i, j int) bool {
|
|
return bytes.Compare(s[i].outBailmentIDHash(), s[j].outBailmentIDHash()) < 0
|
|
}
|
|
|
|
func (s outputStatus) String() string {
|
|
strings := map[outputStatus]string{
|
|
statusSuccess: "success",
|
|
statusPartial: "partial-",
|
|
statusSplit: "split",
|
|
}
|
|
return strings[s]
|
|
}
|
|
|
|
func (tx *changeAwareTx) addSelfToStore(store *wtxmgr.Store, txmgrNs walletdb.ReadWriteBucket) error {
|
|
rec, err := wtxmgr.NewTxRecordFromMsgTx(tx.MsgTx, time.Now())
|
|
if err != nil {
|
|
return newError(ErrWithdrawalTxStorage, "error constructing TxRecord for storing", err)
|
|
}
|
|
|
|
if err := store.InsertTx(txmgrNs, rec, nil); err != nil {
|
|
return newError(ErrWithdrawalTxStorage, "error adding tx to store", err)
|
|
}
|
|
if tx.changeIdx != -1 {
|
|
if err = store.AddCredit(txmgrNs, rec, nil, uint32(tx.changeIdx), true); err != nil {
|
|
return newError(ErrWithdrawalTxStorage, "error adding tx credits to store", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Outputs returns a map of outbailment IDs to WithdrawalOutputs for all outputs
|
|
// requested in this withdrawal.
|
|
func (s *WithdrawalStatus) Outputs() map[OutBailmentID]*WithdrawalOutput {
|
|
return s.outputs
|
|
}
|
|
|
|
// Sigs returns a map of ntxids to signature lists for every input in the tx
|
|
// with that ntxid.
|
|
func (s *WithdrawalStatus) Sigs() map[Ntxid]TxSigs {
|
|
return s.sigs
|
|
}
|
|
|
|
// Fees returns the total amount of network fees included in all transactions
|
|
// generated as part of a withdrawal.
|
|
func (s *WithdrawalStatus) Fees() btcutil.Amount {
|
|
return s.fees
|
|
}
|
|
|
|
// NextInputAddr returns the votingpool address that should be used as the
|
|
// startAddress of subsequent withdrawals.
|
|
func (s *WithdrawalStatus) NextInputAddr() WithdrawalAddress {
|
|
return s.nextInputAddr
|
|
}
|
|
|
|
// NextChangeAddr returns the votingpool address that should be used as the
|
|
// changeStart of subsequent withdrawals.
|
|
func (s *WithdrawalStatus) NextChangeAddr() ChangeAddress {
|
|
return s.nextChangeAddr
|
|
}
|
|
|
|
// String makes OutputRequest satisfy the Stringer interface.
|
|
func (r OutputRequest) String() string {
|
|
return fmt.Sprintf("OutputRequest %s to send %v to %s", r.outBailmentID(), r.Amount, r.Address)
|
|
}
|
|
|
|
func (r OutputRequest) outBailmentID() OutBailmentID {
|
|
return OutBailmentID(fmt.Sprintf("%s:%d", r.Server, r.Transaction))
|
|
}
|
|
|
|
// outBailmentIDHash returns a byte slice which is used when sorting
|
|
// OutputRequests.
|
|
func (r OutputRequest) outBailmentIDHash() []byte {
|
|
if r.cachedHash != nil {
|
|
return r.cachedHash
|
|
}
|
|
str := r.Server + strconv.Itoa(int(r.Transaction))
|
|
hasher := sha256.New()
|
|
// hasher.Write() always returns nil as the error, so it's safe to ignore it here.
|
|
_, _ = hasher.Write([]byte(str))
|
|
id := hasher.Sum(nil)
|
|
r.cachedHash = id
|
|
return id
|
|
}
|
|
|
|
func (o *WithdrawalOutput) String() string {
|
|
return fmt.Sprintf("WithdrawalOutput for %s", o.request)
|
|
}
|
|
|
|
func (o *WithdrawalOutput) addOutpoint(outpoint OutBailmentOutpoint) {
|
|
o.outpoints = append(o.outpoints, outpoint)
|
|
}
|
|
|
|
// Status returns the status of this WithdrawalOutput.
|
|
func (o *WithdrawalOutput) Status() string {
|
|
return o.status.String()
|
|
}
|
|
|
|
// Address returns the string representation of this WithdrawalOutput's address.
|
|
func (o *WithdrawalOutput) Address() string {
|
|
return o.request.Address.String()
|
|
}
|
|
|
|
// Outpoints returns a slice containing the OutBailmentOutpoints created to
|
|
// fulfill this output.
|
|
func (o *WithdrawalOutput) Outpoints() []OutBailmentOutpoint {
|
|
return o.outpoints
|
|
}
|
|
|
|
// Amount returns the amount (in satoshis) in this OutBailmentOutpoint.
|
|
func (o OutBailmentOutpoint) Amount() btcutil.Amount {
|
|
return o.amount
|
|
}
|
|
|
|
// withdrawal holds all the state needed for Pool.Withdrawal() to do its job.
|
|
type withdrawal struct {
|
|
roundID uint32
|
|
status *WithdrawalStatus
|
|
transactions []*withdrawalTx
|
|
pendingRequests []OutputRequest
|
|
eligibleInputs []credit
|
|
current *withdrawalTx
|
|
// txOptions is a function called for every new withdrawalTx created as
|
|
// part of this withdrawal. It is defined as a function field because it
|
|
// exists mainly so that tests can mock withdrawalTx fields.
|
|
txOptions func(tx *withdrawalTx)
|
|
}
|
|
|
|
// withdrawalTxOut wraps an OutputRequest and provides a separate amount field.
|
|
// It is necessary because some requests may be partially fulfilled or split
|
|
// across transactions.
|
|
type withdrawalTxOut struct {
|
|
// Notice that in the case of a split output, the OutputRequest here will
|
|
// be a copy of the original one with the amount being the remainder of the
|
|
// originally requested amount minus the amounts fulfilled by other
|
|
// withdrawalTxOut. The original OutputRequest, if needed, can be obtained
|
|
// from WithdrawalStatus.outputs.
|
|
request OutputRequest
|
|
amount btcutil.Amount
|
|
}
|
|
|
|
// String makes withdrawalTxOut satisfy the Stringer interface.
|
|
func (o *withdrawalTxOut) String() string {
|
|
return fmt.Sprintf("withdrawalTxOut fulfilling %v of %s", o.amount, o.request)
|
|
}
|
|
|
|
func (o *withdrawalTxOut) pkScript() []byte {
|
|
return o.request.PkScript
|
|
}
|
|
|
|
// withdrawalTx represents a transaction constructed by the withdrawal process.
|
|
type withdrawalTx struct {
|
|
inputs []credit
|
|
outputs []*withdrawalTxOut
|
|
fee btcutil.Amount
|
|
|
|
// changeOutput holds information about the change for this transaction.
|
|
changeOutput *wire.TxOut
|
|
|
|
// calculateSize returns the estimated serialized size (in bytes) of this
|
|
// tx. See calculateTxSize() for details on how that's done. We use a
|
|
// struct field instead of a method so that it can be replaced in tests.
|
|
calculateSize func() int
|
|
// calculateFee calculates the expected network fees for this tx. We use a
|
|
// struct field instead of a method so that it can be replaced in tests.
|
|
calculateFee func() btcutil.Amount
|
|
}
|
|
|
|
// newWithdrawalTx creates a new withdrawalTx and calls setOptions()
|
|
// passing the newly created tx.
|
|
func newWithdrawalTx(setOptions func(tx *withdrawalTx)) *withdrawalTx {
|
|
tx := &withdrawalTx{}
|
|
tx.calculateSize = func() int { return calculateTxSize(tx) }
|
|
tx.calculateFee = func() btcutil.Amount {
|
|
return btcutil.Amount(1+tx.calculateSize()/1000) * feeIncrement
|
|
}
|
|
setOptions(tx)
|
|
return tx
|
|
}
|
|
|
|
// ntxid returns the unique ID for this transaction.
|
|
func (tx *withdrawalTx) ntxid() Ntxid {
|
|
msgtx := tx.toMsgTx()
|
|
var empty []byte
|
|
for _, txin := range msgtx.TxIn {
|
|
txin.SignatureScript = empty
|
|
}
|
|
return Ntxid(msgtx.TxHash().String())
|
|
}
|
|
|
|
// isTooBig returns true if the size (in bytes) of the given tx is greater
|
|
// than or equal to txMaxSize.
|
|
func (tx *withdrawalTx) isTooBig() bool {
|
|
// In bitcoind a tx is considered standard only if smaller than
|
|
// MAX_STANDARD_TX_SIZE; that's why we consider anything >= txMaxSize to
|
|
// be too big.
|
|
return tx.calculateSize() >= txMaxSize
|
|
}
|
|
|
|
// inputTotal returns the sum amount of all inputs in this tx.
|
|
func (tx *withdrawalTx) inputTotal() (total btcutil.Amount) {
|
|
for _, input := range tx.inputs {
|
|
total += input.Amount
|
|
}
|
|
return total
|
|
}
|
|
|
|
// outputTotal returns the sum amount of all outputs in this tx. It does not
|
|
// include the amount for the change output, in case the tx has one.
|
|
func (tx *withdrawalTx) outputTotal() (total btcutil.Amount) {
|
|
for _, output := range tx.outputs {
|
|
total += output.amount
|
|
}
|
|
return total
|
|
}
|
|
|
|
// hasChange returns true if this transaction has a change output.
|
|
func (tx *withdrawalTx) hasChange() bool {
|
|
return tx.changeOutput != nil
|
|
}
|
|
|
|
// toMsgTx generates a btcwire.MsgTx with this tx's inputs and outputs.
|
|
func (tx *withdrawalTx) toMsgTx() *wire.MsgTx {
|
|
msgtx := wire.NewMsgTx(wire.TxVersion)
|
|
for _, o := range tx.outputs {
|
|
msgtx.AddTxOut(wire.NewTxOut(int64(o.amount), o.pkScript()))
|
|
}
|
|
|
|
if tx.hasChange() {
|
|
msgtx.AddTxOut(tx.changeOutput)
|
|
}
|
|
|
|
for _, i := range tx.inputs {
|
|
msgtx.AddTxIn(wire.NewTxIn(&i.OutPoint, []byte{}))
|
|
}
|
|
return msgtx
|
|
}
|
|
|
|
// addOutput adds a new output to this transaction.
|
|
func (tx *withdrawalTx) addOutput(request OutputRequest) {
|
|
log.Debugf("Added tx output sending %s to %s", request.Amount, request.Address)
|
|
tx.outputs = append(tx.outputs, &withdrawalTxOut{request: request, amount: request.Amount})
|
|
}
|
|
|
|
// removeOutput removes the last added output and returns it.
|
|
func (tx *withdrawalTx) removeOutput() *withdrawalTxOut {
|
|
removed := tx.outputs[len(tx.outputs)-1]
|
|
tx.outputs = tx.outputs[:len(tx.outputs)-1]
|
|
log.Debugf("Removed tx output sending %s to %s", removed.amount, removed.request.Address)
|
|
return removed
|
|
}
|
|
|
|
// addInput adds a new input to this transaction.
|
|
func (tx *withdrawalTx) addInput(input credit) {
|
|
log.Debugf("Added tx input with amount %v", input.Amount)
|
|
tx.inputs = append(tx.inputs, input)
|
|
}
|
|
|
|
// removeInput removes the last added input and returns it.
|
|
func (tx *withdrawalTx) removeInput() credit {
|
|
removed := tx.inputs[len(tx.inputs)-1]
|
|
tx.inputs = tx.inputs[:len(tx.inputs)-1]
|
|
log.Debugf("Removed tx input with amount %v", removed.Amount)
|
|
return removed
|
|
}
|
|
|
|
// addChange adds a change output if there are any satoshis left after paying
|
|
// all the outputs and network fees. It returns true if a change output was
|
|
// added.
|
|
//
|
|
// This method must be called only once, and no extra inputs/outputs should be
|
|
// added after it's called. Also, callsites must make sure adding a change
|
|
// output won't cause the tx to exceed the size limit.
|
|
func (tx *withdrawalTx) addChange(pkScript []byte) bool {
|
|
tx.fee = tx.calculateFee()
|
|
change := tx.inputTotal() - tx.outputTotal() - tx.fee
|
|
log.Debugf("addChange: input total %v, output total %v, fee %v", tx.inputTotal(),
|
|
tx.outputTotal(), tx.fee)
|
|
if change > 0 {
|
|
tx.changeOutput = wire.NewTxOut(int64(change), pkScript)
|
|
log.Debugf("Added change output with amount %v", change)
|
|
}
|
|
return tx.hasChange()
|
|
}
|
|
|
|
// rollBackLastOutput will roll back the last added output and possibly remove
|
|
// inputs that are no longer needed to cover the remaining outputs. The method
|
|
// returns the removed output and the removed inputs, in the reverse order they
|
|
// were added, if any.
|
|
//
|
|
// The tx needs to have two or more outputs. The case with only one output must
|
|
// be handled separately (by the split output procedure).
|
|
func (tx *withdrawalTx) rollBackLastOutput() ([]credit, *withdrawalTxOut, error) {
|
|
// Check precondition: At least two outputs are required in the transaction.
|
|
if len(tx.outputs) < 2 {
|
|
str := fmt.Sprintf("at least two outputs expected; got %d", len(tx.outputs))
|
|
return nil, nil, newError(ErrPreconditionNotMet, str, nil)
|
|
}
|
|
|
|
removedOutput := tx.removeOutput()
|
|
|
|
var removedInputs []credit
|
|
// Continue until sum(in) < sum(out) + fee
|
|
for tx.inputTotal() >= tx.outputTotal()+tx.calculateFee() {
|
|
removedInputs = append(removedInputs, tx.removeInput())
|
|
}
|
|
|
|
// Re-add the last item from removedInputs, which is the last popped input.
|
|
tx.addInput(removedInputs[len(removedInputs)-1])
|
|
removedInputs = removedInputs[:len(removedInputs)-1]
|
|
return removedInputs, removedOutput, nil
|
|
}
|
|
|
|
func defaultTxOptions(tx *withdrawalTx) {}
|
|
|
|
func newWithdrawal(roundID uint32, requests []OutputRequest, inputs []credit,
|
|
changeStart ChangeAddress) *withdrawal {
|
|
outputs := make(map[OutBailmentID]*WithdrawalOutput, len(requests))
|
|
for _, request := range requests {
|
|
outputs[request.outBailmentID()] = &WithdrawalOutput{request: request}
|
|
}
|
|
status := &WithdrawalStatus{
|
|
outputs: outputs,
|
|
nextChangeAddr: changeStart,
|
|
}
|
|
return &withdrawal{
|
|
roundID: roundID,
|
|
pendingRequests: requests,
|
|
eligibleInputs: inputs,
|
|
status: status,
|
|
txOptions: defaultTxOptions,
|
|
}
|
|
}
|
|
|
|
// StartWithdrawal uses a fully deterministic algorithm to construct
|
|
// transactions fulfilling as many of the given output requests as possible.
|
|
// It returns a WithdrawalStatus containing the outpoints fulfilling the
|
|
// requested outputs and a map of normalized transaction IDs (ntxid) to
|
|
// signature lists (one for every private key available to this wallet) for each
|
|
// of those transaction's inputs. More details about the actual algorithm can be
|
|
// found at http://opentransactions.org/wiki/index.php/Startwithdrawal
|
|
// This method must be called with the address manager unlocked.
|
|
func (p *Pool) StartWithdrawal(ns walletdb.ReadWriteBucket, addrmgrNs walletdb.ReadBucket, roundID uint32, requests []OutputRequest,
|
|
startAddress WithdrawalAddress, lastSeriesID uint32, changeStart ChangeAddress,
|
|
txStore *wtxmgr.Store, txmgrNs walletdb.ReadBucket, chainHeight int32, dustThreshold btcutil.Amount) (
|
|
*WithdrawalStatus, error) {
|
|
|
|
status, err := getWithdrawalStatus(p, ns, addrmgrNs, roundID, requests, startAddress, lastSeriesID,
|
|
changeStart, dustThreshold)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if status != nil {
|
|
return status, nil
|
|
}
|
|
|
|
eligible, err := p.getEligibleInputs(ns, addrmgrNs, txStore, txmgrNs, startAddress, lastSeriesID, dustThreshold,
|
|
chainHeight, eligibleInputMinConfirmations)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
w := newWithdrawal(roundID, requests, eligible, changeStart)
|
|
if err := w.fulfillRequests(); err != nil {
|
|
return nil, err
|
|
}
|
|
w.status.sigs, err = getRawSigs(w.transactions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
serialized, err := serializeWithdrawal(requests, startAddress, lastSeriesID, changeStart,
|
|
dustThreshold, *w.status)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = putWithdrawal(ns, p.ID, roundID, serialized)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return w.status, nil
|
|
}
|
|
|
|
// popRequest removes and returns the first request from the stack of pending
|
|
// requests.
|
|
func (w *withdrawal) popRequest() OutputRequest {
|
|
request := w.pendingRequests[0]
|
|
w.pendingRequests = w.pendingRequests[1:]
|
|
return request
|
|
}
|
|
|
|
// pushRequest adds a new request to the top of the stack of pending requests.
|
|
func (w *withdrawal) pushRequest(request OutputRequest) {
|
|
w.pendingRequests = append([]OutputRequest{request}, w.pendingRequests...)
|
|
}
|
|
|
|
// popInput removes and returns the first input from the stack of eligible
|
|
// inputs.
|
|
func (w *withdrawal) popInput() credit {
|
|
input := w.eligibleInputs[len(w.eligibleInputs)-1]
|
|
w.eligibleInputs = w.eligibleInputs[:len(w.eligibleInputs)-1]
|
|
return input
|
|
}
|
|
|
|
// pushInput adds a new input to the top of the stack of eligible inputs.
|
|
func (w *withdrawal) pushInput(input credit) {
|
|
w.eligibleInputs = append(w.eligibleInputs, input)
|
|
}
|
|
|
|
// If this returns it means we have added an output and the necessary inputs to fulfil that
|
|
// output plus the required fees. It also means the tx won't reach the size limit even
|
|
// after we add a change output and sign all inputs.
|
|
func (w *withdrawal) fulfillNextRequest() error {
|
|
request := w.popRequest()
|
|
output := w.status.outputs[request.outBailmentID()]
|
|
// We start with an output status of success and let the methods that deal
|
|
// with special cases change it when appropriate.
|
|
output.status = statusSuccess
|
|
w.current.addOutput(request)
|
|
|
|
if w.current.isTooBig() {
|
|
return w.handleOversizeTx()
|
|
}
|
|
|
|
fee := w.current.calculateFee()
|
|
for w.current.inputTotal() < w.current.outputTotal()+fee {
|
|
if len(w.eligibleInputs) == 0 {
|
|
log.Debug("Splitting last output because we don't have enough inputs")
|
|
if err := w.splitLastOutput(); err != nil {
|
|
return err
|
|
}
|
|
break
|
|
}
|
|
w.current.addInput(w.popInput())
|
|
fee = w.current.calculateFee()
|
|
|
|
if w.current.isTooBig() {
|
|
return w.handleOversizeTx()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// handleOversizeTx handles the case when a transaction has become too
|
|
// big by either rolling back an output or splitting it.
|
|
func (w *withdrawal) handleOversizeTx() error {
|
|
if len(w.current.outputs) > 1 {
|
|
log.Debug("Rolling back last output because tx got too big")
|
|
inputs, output, err := w.current.rollBackLastOutput()
|
|
if err != nil {
|
|
return newError(ErrWithdrawalProcessing, "failed to rollback last output", err)
|
|
}
|
|
for _, input := range inputs {
|
|
w.pushInput(input)
|
|
}
|
|
w.pushRequest(output.request)
|
|
} else if len(w.current.outputs) == 1 {
|
|
log.Debug("Splitting last output because tx got too big...")
|
|
w.pushInput(w.current.removeInput())
|
|
if err := w.splitLastOutput(); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
return newError(ErrPreconditionNotMet, "Oversize tx must have at least one output", nil)
|
|
}
|
|
return w.finalizeCurrentTx()
|
|
}
|
|
|
|
// finalizeCurrentTx finalizes the transaction in w.current, moves it to the
|
|
// list of finalized transactions and replaces w.current with a new empty
|
|
// transaction.
|
|
func (w *withdrawal) finalizeCurrentTx() error {
|
|
log.Debug("Finalizing current transaction")
|
|
tx := w.current
|
|
if len(tx.outputs) == 0 {
|
|
log.Debug("Current transaction has no outputs, doing nothing")
|
|
return nil
|
|
}
|
|
|
|
pkScript, err := txscript.PayToAddrScript(w.status.nextChangeAddr.addr)
|
|
if err != nil {
|
|
return newError(ErrWithdrawalProcessing, "failed to generate pkScript for change address", err)
|
|
}
|
|
if tx.addChange(pkScript) {
|
|
var err error
|
|
w.status.nextChangeAddr, err = nextChangeAddress(w.status.nextChangeAddr)
|
|
if err != nil {
|
|
return newError(ErrWithdrawalProcessing, "failed to get next change address", err)
|
|
}
|
|
}
|
|
|
|
ntxid := tx.ntxid()
|
|
for i, txOut := range tx.outputs {
|
|
outputStatus := w.status.outputs[txOut.request.outBailmentID()]
|
|
outputStatus.addOutpoint(
|
|
OutBailmentOutpoint{ntxid: ntxid, index: uint32(i), amount: txOut.amount})
|
|
}
|
|
|
|
// Check that WithdrawalOutput entries with status==success have the sum of
|
|
// their outpoint amounts matching the requested amount.
|
|
for _, txOut := range tx.outputs {
|
|
// Look up the original request we received because txOut.request may
|
|
// represent a split request and thus have a different amount from the
|
|
// original one.
|
|
outputStatus := w.status.outputs[txOut.request.outBailmentID()]
|
|
origRequest := outputStatus.request
|
|
amtFulfilled := btcutil.Amount(0)
|
|
for _, outpoint := range outputStatus.outpoints {
|
|
amtFulfilled += outpoint.amount
|
|
}
|
|
if outputStatus.status == statusSuccess && amtFulfilled != origRequest.Amount {
|
|
msg := fmt.Sprintf("%s was not completely fulfilled; only %v fulfilled", origRequest,
|
|
amtFulfilled)
|
|
return newError(ErrWithdrawalProcessing, msg, nil)
|
|
}
|
|
}
|
|
|
|
w.transactions = append(w.transactions, tx)
|
|
w.current = newWithdrawalTx(w.txOptions)
|
|
return nil
|
|
}
|
|
|
|
// maybeDropRequests will check the total amount we have in eligible inputs and drop
|
|
// requested outputs (in descending amount order) if we don't have enough to
|
|
// fulfill them all. For every dropped output request we update its entry in
|
|
// w.status.outputs with the status string set to statusPartial.
|
|
func (w *withdrawal) maybeDropRequests() {
|
|
inputAmount := btcutil.Amount(0)
|
|
for _, input := range w.eligibleInputs {
|
|
inputAmount += input.Amount
|
|
}
|
|
outputAmount := btcutil.Amount(0)
|
|
for _, request := range w.pendingRequests {
|
|
outputAmount += request.Amount
|
|
}
|
|
sort.Sort(sort.Reverse(byAmount(w.pendingRequests)))
|
|
for inputAmount < outputAmount {
|
|
request := w.popRequest()
|
|
log.Infof("Not fulfilling request to send %v to %v; not enough credits.",
|
|
request.Amount, request.Address)
|
|
outputAmount -= request.Amount
|
|
w.status.outputs[request.outBailmentID()].status = statusPartial
|
|
}
|
|
}
|
|
|
|
func (w *withdrawal) fulfillRequests() error {
|
|
w.maybeDropRequests()
|
|
if len(w.pendingRequests) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Sort outputs by outBailmentID (hash(server ID, tx #))
|
|
sort.Sort(byOutBailmentID(w.pendingRequests))
|
|
|
|
w.current = newWithdrawalTx(w.txOptions)
|
|
for len(w.pendingRequests) > 0 {
|
|
if err := w.fulfillNextRequest(); err != nil {
|
|
return err
|
|
}
|
|
tx := w.current
|
|
if len(w.eligibleInputs) == 0 && tx.inputTotal() <= tx.outputTotal()+tx.calculateFee() {
|
|
// We don't have more eligible inputs and all the inputs in the
|
|
// current tx have been spent.
|
|
break
|
|
}
|
|
}
|
|
|
|
if err := w.finalizeCurrentTx(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: Update w.status.nextInputAddr. Not yet implemented as in some
|
|
// conditions we need to know about un-thawed series.
|
|
|
|
w.status.transactions = make(map[Ntxid]changeAwareTx, len(w.transactions))
|
|
for _, tx := range w.transactions {
|
|
w.status.updateStatusFor(tx)
|
|
w.status.fees += tx.fee
|
|
msgtx := tx.toMsgTx()
|
|
changeIdx := -1
|
|
if tx.hasChange() {
|
|
// When withdrawalTx has a change, we know it will be the last entry
|
|
// in the generated MsgTx.
|
|
changeIdx = len(msgtx.TxOut) - 1
|
|
}
|
|
w.status.transactions[tx.ntxid()] = changeAwareTx{
|
|
MsgTx: msgtx,
|
|
changeIdx: int32(changeIdx),
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *withdrawal) splitLastOutput() error {
|
|
if len(w.current.outputs) == 0 {
|
|
return newError(ErrPreconditionNotMet,
|
|
"splitLastOutput requires current tx to have at least 1 output", nil)
|
|
}
|
|
|
|
tx := w.current
|
|
output := tx.outputs[len(tx.outputs)-1]
|
|
log.Debugf("Splitting tx output for %s", output.request)
|
|
origAmount := output.amount
|
|
spentAmount := tx.outputTotal() + tx.calculateFee() - output.amount
|
|
// This is how much we have left after satisfying all outputs except the last
|
|
// one. IOW, all we have left for the last output, so we set that as the
|
|
// amount of the tx's last output.
|
|
unspentAmount := tx.inputTotal() - spentAmount
|
|
output.amount = unspentAmount
|
|
log.Debugf("Updated output amount to %v", output.amount)
|
|
|
|
// Create a new OutputRequest with the amount being the difference between
|
|
// the original amount and what was left in the tx output above.
|
|
request := output.request
|
|
newRequest := OutputRequest{
|
|
Server: request.Server,
|
|
Transaction: request.Transaction,
|
|
Address: request.Address,
|
|
PkScript: request.PkScript,
|
|
Amount: origAmount - output.amount}
|
|
w.pushRequest(newRequest)
|
|
log.Debugf("Created a new pending output request with amount %v", newRequest.Amount)
|
|
|
|
w.status.outputs[request.outBailmentID()].status = statusPartial
|
|
return nil
|
|
}
|
|
|
|
func (s *WithdrawalStatus) updateStatusFor(tx *withdrawalTx) {
|
|
for _, output := range s.outputs {
|
|
if len(output.outpoints) > 1 {
|
|
output.status = statusSplit
|
|
}
|
|
// TODO: Update outputs with status=='partial-'. For this we need an API
|
|
// that gives us the amount of credits in a given series.
|
|
// http://opentransactions.org/wiki/index.php/Update_Status
|
|
}
|
|
}
|
|
|
|
// match returns true if the given arguments match the fields in this
|
|
// withdrawalInfo. For the requests slice, the order of the items does not
|
|
// matter.
|
|
func (wi *withdrawalInfo) match(requests []OutputRequest, startAddress WithdrawalAddress,
|
|
lastSeriesID uint32, changeStart ChangeAddress, dustThreshold btcutil.Amount) bool {
|
|
// Use reflect.DeepEqual to compare changeStart and startAddress as they're
|
|
// structs that contain pointers and we want to compare their content and
|
|
// not their address.
|
|
if !reflect.DeepEqual(changeStart, wi.changeStart) {
|
|
log.Debugf("withdrawal changeStart does not match: %v != %v", changeStart, wi.changeStart)
|
|
return false
|
|
}
|
|
if !reflect.DeepEqual(startAddress, wi.startAddress) {
|
|
log.Debugf("withdrawal startAddr does not match: %v != %v", startAddress, wi.startAddress)
|
|
return false
|
|
}
|
|
if lastSeriesID != wi.lastSeriesID {
|
|
log.Debugf("withdrawal lastSeriesID does not match: %v != %v", lastSeriesID,
|
|
wi.lastSeriesID)
|
|
return false
|
|
}
|
|
if dustThreshold != wi.dustThreshold {
|
|
log.Debugf("withdrawal dustThreshold does not match: %v != %v", dustThreshold,
|
|
wi.dustThreshold)
|
|
return false
|
|
}
|
|
r1 := make([]OutputRequest, len(requests))
|
|
copy(r1, requests)
|
|
r2 := make([]OutputRequest, len(wi.requests))
|
|
copy(r2, wi.requests)
|
|
sort.Sort(byOutBailmentID(r1))
|
|
sort.Sort(byOutBailmentID(r2))
|
|
if !reflect.DeepEqual(r1, r2) {
|
|
log.Debugf("withdrawal requests does not match: %v != %v", requests, wi.requests)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// getWithdrawalStatus returns the existing WithdrawalStatus for the given
|
|
// withdrawal parameters, if one exists. This function must be called with the
|
|
// address manager unlocked.
|
|
func getWithdrawalStatus(p *Pool, ns, addrmgrNs walletdb.ReadBucket, roundID uint32, requests []OutputRequest,
|
|
startAddress WithdrawalAddress, lastSeriesID uint32, changeStart ChangeAddress,
|
|
dustThreshold btcutil.Amount) (*WithdrawalStatus, error) {
|
|
|
|
serialized := getWithdrawal(ns, p.ID, roundID)
|
|
if bytes.Equal(serialized, []byte{}) {
|
|
return nil, nil
|
|
}
|
|
wInfo, err := deserializeWithdrawal(p, ns, addrmgrNs, serialized)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if wInfo.match(requests, startAddress, lastSeriesID, changeStart, dustThreshold) {
|
|
return &wInfo.status, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// getRawSigs iterates over the inputs of each transaction given, constructing the
|
|
// raw signatures for them using the private keys available to us.
|
|
// It returns a map of ntxids to signature lists.
|
|
func getRawSigs(transactions []*withdrawalTx) (map[Ntxid]TxSigs, error) {
|
|
sigs := make(map[Ntxid]TxSigs)
|
|
for _, tx := range transactions {
|
|
txSigs := make(TxSigs, len(tx.inputs))
|
|
msgtx := tx.toMsgTx()
|
|
ntxid := tx.ntxid()
|
|
for inputIdx, input := range tx.inputs {
|
|
creditAddr := input.addr
|
|
redeemScript := creditAddr.redeemScript()
|
|
series := creditAddr.series()
|
|
// The order of the raw signatures in the signature script must match the
|
|
// order of the public keys in the redeem script, so we sort the public keys
|
|
// here using the same API used to sort them in the redeem script and use
|
|
// series.getPrivKeyFor() to lookup the corresponding private keys.
|
|
pubKeys, err := branchOrder(series.publicKeys, creditAddr.Branch())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
txInSigs := make([]RawSig, len(pubKeys))
|
|
for i, pubKey := range pubKeys {
|
|
var sig RawSig
|
|
privKey, err := series.getPrivKeyFor(pubKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if privKey != nil {
|
|
childKey, err := privKey.Child(uint32(creditAddr.Index()))
|
|
if err != nil {
|
|
return nil, newError(ErrKeyChain, "failed to derive private key", err)
|
|
}
|
|
ecPrivKey, err := childKey.ECPrivKey()
|
|
if err != nil {
|
|
return nil, newError(ErrKeyChain, "failed to obtain ECPrivKey", err)
|
|
}
|
|
log.Debugf("Generating raw sig for input %d of tx %s with privkey of %s",
|
|
inputIdx, ntxid, pubKey.String())
|
|
sig, err = txscript.RawTxInSignature(
|
|
msgtx, inputIdx, redeemScript, txscript.SigHashAll, ecPrivKey)
|
|
if err != nil {
|
|
return nil, newError(ErrRawSigning, "failed to generate raw signature", err)
|
|
}
|
|
} else {
|
|
log.Debugf("Not generating raw sig for input %d of %s because private key "+
|
|
"for %s is not available: %v", inputIdx, ntxid, pubKey.String(), err)
|
|
sig = []byte{}
|
|
}
|
|
txInSigs[i] = sig
|
|
}
|
|
txSigs[inputIdx] = txInSigs
|
|
}
|
|
sigs[ntxid] = txSigs
|
|
}
|
|
return sigs, nil
|
|
}
|
|
|
|
// SignTx signs every input of the given MsgTx by looking up (on the addr
|
|
// manager) the redeem script for each of them and constructing the signature
|
|
// script using that and the given raw signatures.
|
|
// This function must be called with the manager unlocked.
|
|
func SignTx(msgtx *wire.MsgTx, sigs TxSigs, mgr *waddrmgr.Manager, addrmgrNs walletdb.ReadBucket, store *wtxmgr.Store, txmgrNs walletdb.ReadBucket) error {
|
|
// We use time.Now() here as we're not going to store the new TxRecord
|
|
// anywhere -- we just need it to pass to store.PreviousPkScripts().
|
|
rec, err := wtxmgr.NewTxRecordFromMsgTx(msgtx, time.Now())
|
|
if err != nil {
|
|
return newError(ErrTxSigning, "failed to construct TxRecord for signing", err)
|
|
}
|
|
pkScripts, err := store.PreviousPkScripts(txmgrNs, rec, nil)
|
|
if err != nil {
|
|
return newError(ErrTxSigning, "failed to obtain pkScripts for signing", err)
|
|
}
|
|
for i, pkScript := range pkScripts {
|
|
if err = signMultiSigUTXO(mgr, addrmgrNs, msgtx, i, pkScript, sigs[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getRedeemScript returns the redeem script for the given P2SH address. It must
|
|
// be called with the manager unlocked.
|
|
func getRedeemScript(mgr *waddrmgr.Manager, addrmgrNs walletdb.ReadBucket, addr *btcutil.AddressScriptHash) ([]byte, error) {
|
|
address, err := mgr.Address(addrmgrNs, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return address.(waddrmgr.ManagedScriptAddress).Script()
|
|
}
|
|
|
|
// signMultiSigUTXO signs the P2SH UTXO with the given index by constructing a
|
|
// script containing all given signatures plus the redeem (multi-sig) script. The
|
|
// redeem script is obtained by looking up the address of the given P2SH pkScript
|
|
// on the address manager.
|
|
// The order of the signatures must match that of the public keys in the multi-sig
|
|
// script as OP_CHECKMULTISIG expects that.
|
|
// This function must be called with the manager unlocked.
|
|
func signMultiSigUTXO(mgr *waddrmgr.Manager, addrmgrNs walletdb.ReadBucket, tx *wire.MsgTx, idx int, pkScript []byte, sigs []RawSig) error {
|
|
class, addresses, _, err := txscript.ExtractPkScriptAddrs(pkScript, mgr.ChainParams())
|
|
if err != nil {
|
|
return newError(ErrTxSigning, "unparseable pkScript", err)
|
|
}
|
|
if class != txscript.ScriptHashTy {
|
|
return newError(ErrTxSigning, fmt.Sprintf("pkScript is not P2SH: %s", class), nil)
|
|
}
|
|
redeemScript, err := getRedeemScript(mgr, addrmgrNs, addresses[0].(*btcutil.AddressScriptHash))
|
|
if err != nil {
|
|
return newError(ErrTxSigning, "unable to retrieve redeem script", err)
|
|
}
|
|
|
|
class, _, nRequired, err := txscript.ExtractPkScriptAddrs(redeemScript, mgr.ChainParams())
|
|
if err != nil {
|
|
return newError(ErrTxSigning, "unparseable redeem script", err)
|
|
}
|
|
if class != txscript.MultiSigTy {
|
|
return newError(ErrTxSigning, fmt.Sprintf("redeem script is not multi-sig: %v", class), nil)
|
|
}
|
|
if len(sigs) < nRequired {
|
|
errStr := fmt.Sprintf("not enough signatures; need %d but got only %d", nRequired,
|
|
len(sigs))
|
|
return newError(ErrTxSigning, errStr, nil)
|
|
}
|
|
|
|
// Construct the unlocking script.
|
|
// Start with an OP_0 because of the bug in bitcoind, then add nRequired signatures.
|
|
unlockingScript := txscript.NewScriptBuilder().AddOp(txscript.OP_FALSE)
|
|
for _, sig := range sigs[:nRequired] {
|
|
unlockingScript.AddData(sig)
|
|
}
|
|
|
|
// Combine the redeem script and the unlocking script to get the actual signature script.
|
|
sigScript := unlockingScript.AddData(redeemScript)
|
|
script, err := sigScript.Script()
|
|
if err != nil {
|
|
return newError(ErrTxSigning, "error building sigscript", err)
|
|
}
|
|
tx.TxIn[idx].SignatureScript = script
|
|
|
|
if err := validateSigScript(tx, idx, pkScript); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateSigScripts executes the signature script of the tx input with the
|
|
// given index, returning an error if it fails.
|
|
func validateSigScript(msgtx *wire.MsgTx, idx int, pkScript []byte) error {
|
|
vm, err := txscript.NewEngine(pkScript, msgtx, idx,
|
|
txscript.StandardVerifyFlags, nil)
|
|
if err != nil {
|
|
return newError(ErrTxSigning, "cannot create script engine", err)
|
|
}
|
|
if err = vm.Execute(); err != nil {
|
|
return newError(ErrTxSigning, "cannot validate tx signature", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// calculateTxSize returns an estimate of the serialized size (in bytes) of the
|
|
// given transaction. It assumes all tx inputs are P2SH multi-sig.
|
|
func calculateTxSize(tx *withdrawalTx) int {
|
|
msgtx := tx.toMsgTx()
|
|
// Assume that there will always be a change output, for simplicity. We
|
|
// simulate that by simply copying the first output as all we care about is
|
|
// the size of its serialized form, which should be the same for all of them
|
|
// as they're either P2PKH or P2SH..
|
|
if !tx.hasChange() {
|
|
msgtx.AddTxOut(msgtx.TxOut[0])
|
|
}
|
|
// Craft a SignatureScript with dummy signatures for every input in this tx
|
|
// so that we can use msgtx.SerializeSize() to get its size and don't need
|
|
// to rely on estimations.
|
|
for i, txin := range msgtx.TxIn {
|
|
// 1 byte for the OP_FALSE opcode, then 73+1 bytes for each signature
|
|
// with their OP_DATA opcode and finally the redeem script + 1 byte
|
|
// for its OP_PUSHDATA opcode and N bytes for the redeem script's size.
|
|
// Notice that we use 73 as the signature length as that's the maximum
|
|
// length they may have:
|
|
// https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm
|
|
addr := tx.inputs[i].addr
|
|
redeemScriptLen := len(addr.redeemScript())
|
|
n := wire.VarIntSerializeSize(uint64(redeemScriptLen))
|
|
sigScriptLen := 1 + (74 * int(addr.series().reqSigs)) + redeemScriptLen + 1 + n
|
|
txin.SignatureScript = bytes.Repeat([]byte{1}, sigScriptLen)
|
|
}
|
|
return msgtx.SerializeSize()
|
|
}
|
|
|
|
func nextChangeAddress(a ChangeAddress) (ChangeAddress, error) {
|
|
index := a.index
|
|
seriesID := a.seriesID
|
|
if index == math.MaxUint32 {
|
|
index = 0
|
|
seriesID++
|
|
} else {
|
|
index++
|
|
}
|
|
addr, err := a.pool.ChangeAddress(seriesID, index)
|
|
return *addr, err
|
|
}
|
|
|
|
func storeTransactions(store *wtxmgr.Store, txmgrNs walletdb.ReadWriteBucket, transactions []*changeAwareTx) error {
|
|
for _, tx := range transactions {
|
|
if err := tx.addSelfToStore(store, txmgrNs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|