920 lines
32 KiB
Go
920 lines
32 KiB
Go
|
/*
|
||
|
* Copyright (c) 2015 Conformal Systems LLC <info@conformal.com>
|
||
|
*
|
||
|
* Permission to use, copy, modify, and distribute this software for any
|
||
|
* purpose with or without fee is hereby granted, provided that the above
|
||
|
* copyright notice and this permission notice appear in all copies.
|
||
|
*
|
||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||
|
*/
|
||
|
|
||
|
package votingpool
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"math"
|
||
|
"sort"
|
||
|
"strconv"
|
||
|
|
||
|
"github.com/btcsuite/btcd/txscript"
|
||
|
"github.com/btcsuite/btcd/wire"
|
||
|
"github.com/btcsuite/btcutil"
|
||
|
"github.com/btcsuite/btcwallet/txstore"
|
||
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
||
|
"github.com/btcsuite/fastsha256"
|
||
|
)
|
||
|
|
||
|
// 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 fulfil 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
|
||
|
}
|
||
|
|
||
|
// 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]
|
||
|
}
|
||
|
|
||
|
// 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 := fastsha256.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
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
|
||
|
func newWithdrawalTx() *withdrawalTx {
|
||
|
return &withdrawalTx{}
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
// Ignore the error as TxSha() can't fail.
|
||
|
sha, _ := msgtx.TxSha()
|
||
|
return Ntxid(sha.String())
|
||
|
}
|
||
|
|
||
|
// 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()
|
||
|
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 = calculateTxFee(tx)
|
||
|
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()+calculateTxFee(tx) {
|
||
|
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 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,
|
||
|
current: newWithdrawalTx(),
|
||
|
pendingRequests: requests,
|
||
|
eligibleInputs: inputs,
|
||
|
status: status,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
func (p *Pool) StartWithdrawal(roundID uint32, requests []OutputRequest,
|
||
|
startAddress WithdrawalAddress, lastSeriesID uint32, changeStart ChangeAddress,
|
||
|
txStore *txstore.Store, chainHeight int32, dustThreshold btcutil.Amount) (
|
||
|
*WithdrawalStatus, error) {
|
||
|
|
||
|
eligible, err := p.getEligibleInputs(txStore, 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
|
||
|
}
|
||
|
|
||
|
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[0]
|
||
|
w.eligibleInputs = w.eligibleInputs[1:]
|
||
|
return input
|
||
|
}
|
||
|
|
||
|
// pushInput adds a new input to the top of the stack of eligible inputs.
|
||
|
// TODO: Reverse the stack semantics here as the current one generates a lot of
|
||
|
// extra garbage since it always creates a new single-element slice and append
|
||
|
// the rest of the items to it.
|
||
|
func (w *withdrawal) pushInput(input Credit) {
|
||
|
w.eligibleInputs = append([]Credit{input}, w.eligibleInputs...)
|
||
|
}
|
||
|
|
||
|
// 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 isTxTooBig(w.current) {
|
||
|
return w.handleOversizeTx()
|
||
|
}
|
||
|
|
||
|
fee := calculateTxFee(w.current)
|
||
|
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 = calculateTxFee(w.current)
|
||
|
|
||
|
if isTxTooBig(w.current) {
|
||
|
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()
|
||
|
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))
|
||
|
|
||
|
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()+calculateTxFee(tx) {
|
||
|
// 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() + calculateTxFee(tx) - 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
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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.Address()
|
||
|
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, store *txstore.Store) error {
|
||
|
credits, err := store.FindPreviousCredits(btcutil.NewTx(msgtx))
|
||
|
for i, credit := range credits {
|
||
|
if err = signMultiSigUTXO(mgr, msgtx, i, credit.TxOut().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, addr *btcutil.AddressScriptHash) ([]byte, error) {
|
||
|
address, err := mgr.Address(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, 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, 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 {
|
||
|
txIn := msgtx.TxIn[idx]
|
||
|
engine, err := txscript.NewScript(
|
||
|
txIn.SignatureScript, pkScript, idx, msgtx, txscript.StandardVerifyFlags)
|
||
|
if err != nil {
|
||
|
return newError(ErrTxSigning, "cannot create script engine", err)
|
||
|
}
|
||
|
if err = engine.Execute(); err != nil {
|
||
|
return newError(ErrTxSigning, "cannot validate tx signature", err)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// calculateTxFee calculates the expected network fees for a given tx. We use
|
||
|
// a variable instead of a function so that it can be replaced in tests.
|
||
|
var calculateTxFee = func(tx *withdrawalTx) btcutil.Amount {
|
||
|
return btcutil.Amount(1+calculateTxSize(tx)/1000) * feeIncrement
|
||
|
}
|
||
|
|
||
|
// isTxTooBig returns true if the size (in bytes) of the given tx is greater
|
||
|
// than or equal to txMaxSize. It is defined as a variable so it can be
|
||
|
// replaced for testing purposes.
|
||
|
var isTxTooBig = func(tx *withdrawalTx) 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 calculateTxSize(tx) >= txMaxSize
|
||
|
}
|
||
|
|
||
|
// calculateTxSize returns an estimate of the serialized size (in bytes) of the
|
||
|
// given transaction. It assumes all tx inputs are P2SH multi-sig. We use a
|
||
|
// variable instead of a function so that it can be replaced in tests.
|
||
|
var calculateTxSize = func(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].Address()
|
||
|
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
|
||
|
}
|