lbcwallet/votingpool/withdrawal.go

1047 lines
36 KiB
Go
Raw Normal View History

// 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"
2015-04-07 17:40:13 +02:00
"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
2015-04-07 17:40:13 +02:00
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 {
2015-04-07 17:40:13 +02:00
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 {
2015-04-07 17:40:13 +02:00
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 {
2015-04-07 17:40:13 +02:00
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.
2015-04-07 17:40:13 +02:00
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.
2015-04-07 17:40:13 +02:00
func (tx *withdrawalTx) removeInput() credit {
removed := tx.inputs[len(tx.inputs)-1]
tx.inputs = tx.inputs[:len(tx.inputs)-1]
2015-04-07 17:40:13 +02:00
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).
2015-04-07 17:40:13 +02:00
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()
2015-04-07 17:40:13 +02:00
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) {}
2015-04-07 17:40:13 +02:00
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.
2015-04-07 17:40:13 +02:00
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.
2015-04-07 17:40:13 +02:00
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 {
2015-04-07 17:40:13 +02:00
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 {
2015-04-07 17:40:13 +02:00
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 {
2015-04-07 17:40:13 +02:00
// 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)
2015-04-07 17:40:13 +02:00
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
2015-04-07 17:40:13 +02:00
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
}