forked from btcsuite/btcwallet
-
Notifications
You must be signed in to change notification settings - Fork 0
/
withdrawal.go
1057 lines (942 loc) · 35.8 KB
/
withdrawal.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) 2015-2016 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"
"fmt"
"math"
"reflect"
"sort"
"strconv"
"time"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/btcwallet/wtxmgr"
"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 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) 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(rec, nil); err != nil {
return newError(ErrWithdrawalTxStorage, "error adding tx to store", err)
}
if tx.changeIdx != -1 {
if err = store.AddCredit(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 := 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
// 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()
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(roundID uint32, requests []OutputRequest,
startAddress WithdrawalAddress, lastSeriesID uint32, changeStart ChangeAddress,
txStore *wtxmgr.Store, chainHeight int32, dustThreshold btcutil.Amount) (
*WithdrawalStatus, error) {
status, err := getWithdrawalStatus(p, roundID, requests, startAddress, lastSeriesID,
changeStart, dustThreshold)
if err != nil {
return nil, err
}
if status != nil {
return status, nil
}
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
}
serialized, err := serializeWithdrawal(requests, startAddress, lastSeriesID, changeStart,
dustThreshold, *w.status)
if err != nil {
return nil, err
}
err = p.namespace.Update(
func(tx walletdb.Tx) error {
return putWithdrawal(tx, 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, roundID uint32, requests []OutputRequest,
startAddress WithdrawalAddress, lastSeriesID uint32, changeStart ChangeAddress,
dustThreshold btcutil.Amount) (*WithdrawalStatus, error) {
var serialized []byte
err := p.namespace.View(
func(tx walletdb.Tx) error {
serialized = getWithdrawal(tx, p.ID, roundID)
return nil
})
if err != nil {
return nil, err
}
if bytes.Equal(serialized, []byte{}) {
return nil, nil
}
wInfo, err := deserializeWithdrawal(p, 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, store *wtxmgr.Store) 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(rec, nil)
if err != nil {
return newError(ErrTxSigning, "failed to obtain pkScripts for signing", err)
}
for i, pkScript := range pkScripts {
if err = signMultiSigUTXO(mgr, 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, 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 {
vm, err := txscript.NewEngine(pkScript, msgtx, idx,
txscript.StandardVerifyFlags, nil)
if err != nil {
return newError(ErrTxSigning, "cannot create script engine", err)
}