-
Notifications
You must be signed in to change notification settings - Fork 88
/
spv.go
1960 lines (1718 loc) · 61.7 KB
/
spv.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
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.
// spvWallet implements a Wallet backed by a built-in btcwallet + Neutrino.
//
// There are a few challenges presented in using an SPV wallet for DEX.
// 1. Finding non-wallet related blockchain data requires possession of the
// pubkey script, not just transaction hash and output index
// 2. Finding non-wallet related blockchain data can often entail extensive
// scanning of compact filters. We can limit these scans with more
// information, such as the match time, which would be the earliest a
// transaction could be found on-chain.
// 3. We don't see a mempool. We're blind to new transactions until they are
// mined. This requires special handling by the caller. We've been
// anticipating this, so Core and Swapper are permissive of missing acks for
// audit requests.
package btc
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math"
"os"
"path/filepath"
"sort"
"sync"
"sync/atomic"
"time"
"decred.org/dcrdex/client/asset"
"decred.org/dcrdex/dex"
dexbtc "decred.org/dcrdex/dex/networks/btc"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btclog"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/gcs"
"github.com/btcsuite/btcutil/psbt"
"github.com/btcsuite/btcwallet/chain"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/wallet/txauthor"
"github.com/btcsuite/btcwallet/walletdb"
_ "github.com/btcsuite/btcwallet/walletdb/bdb" // bdb init() registers a driver
"github.com/btcsuite/btcwallet/wtxmgr"
"github.com/jrick/logrotate/rotator"
"github.com/lightninglabs/neutrino"
"github.com/lightninglabs/neutrino/headerfs"
)
const (
WalletTransactionNotFound = dex.ErrorKind("wallet transaction not found")
SpentStatusUnknown = dex.ErrorKind("spend status not known")
// defaultBroadcastWait is long enough for btcwallet's PublishTransaction
// method to record the outgoing transaction and queue it for broadcasting.
// This rough duration is necessary since with neutrino as the wallet's
// chain service, its chainClient.SendRawTransaction call is blocking for up
// to neutrino.Config.BroadcastTimeout while peers either respond to the inv
// request with a getdata or time out. However, in virtually all cases, we
// just need to know that btcwallet was able to create and store the
// transaction record, and pass it to the chain service.
defaultBroadcastWait = 2 * time.Second
maxFutureBlockTime = 2 * time.Hour // see MaxTimeOffsetSeconds in btcd/blockchain/validate.go
neutrinoDBName = "neutrino.db"
logDirName = "logs"
logFileName = "neutrino.log"
defaultAcctNum = 0
defaultAcctName = "default"
)
// btcWallet is satisfied by *btcwallet.Wallet -> *walletExtender.
type btcWallet interface {
PublishTransaction(tx *wire.MsgTx, label string) error
CalculateAccountBalances(account uint32, confirms int32) (wallet.Balances, error)
ListUnspent(minconf, maxconf int32, acctName string) ([]*btcjson.ListUnspentResult, error)
FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, *wire.TxOut, *psbt.Bip32Derivation, int64, error)
ResetLockedOutpoints()
LockOutpoint(op wire.OutPoint)
UnlockOutpoint(op wire.OutPoint)
LockedOutpoints() []btcjson.TransactionInput
NewChangeAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error)
NewAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error)
SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, additionalPrevScriptsadditionalPrevScripts map[wire.OutPoint][]byte,
additionalKeysByAddress map[string]*btcutil.WIF, p2shRedeemScriptsByAddress map[string][]byte) ([]wallet.SignatureError, error)
PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error)
Database() walletdb.DB
Unlock(passphrase []byte, lock <-chan time.Time) error
Lock()
Locked() bool
SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope, account uint32, minconf int32, satPerKb btcutil.Amount, label string) (*wire.MsgTx, error)
HaveAddress(a btcutil.Address) (bool, error)
Stop()
WaitForShutdown()
ChainSynced() bool // currently unused
SynchronizeRPC(chainClient chain.Interface)
// walletExtender methods
walletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error)
syncedTo() waddrmgr.BlockStamp
signTransaction(*wire.MsgTx) error
txNotifications() wallet.TransactionNotificationsClient
}
var _ btcWallet = (*walletExtender)(nil)
// neutrinoService is satisfied by *neutrino.ChainService.
type neutrinoService interface {
GetBlockHash(int64) (*chainhash.Hash, error)
BestBlock() (*headerfs.BlockStamp, error)
Peers() []*neutrino.ServerPeer
GetBlockHeight(hash *chainhash.Hash) (int32, error)
GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, error)
GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, options ...neutrino.QueryOption) (*gcs.Filter, error)
GetBlock(blockHash chainhash.Hash, options ...neutrino.QueryOption) (*btcutil.Block, error)
Stop() error
}
var _ neutrinoService = (*neutrino.ChainService)(nil)
// createSPVWallet creates a new SPV wallet.
func createSPVWallet(privPass []byte, seed []byte, dbDir string, log dex.Logger, net *chaincfg.Params) error {
netDir := filepath.Join(dbDir, net.Name)
if err := logNeutrino(netDir); err != nil {
return fmt.Errorf("error initializing btcwallet+neutrino logging: %v", err)
}
logDir := filepath.Join(netDir, logDirName)
err := os.MkdirAll(logDir, 0744)
if err != nil {
return fmt.Errorf("error creating wallet directories: %v", err)
}
loader := wallet.NewLoader(net, netDir, true, 60*time.Second, 250)
pubPass := []byte(wallet.InsecurePubPassphrase)
_, err = loader.CreateNewWallet(pubPass, privPass, seed, walletBirthday)
if err != nil {
return fmt.Errorf("CreateNewWallet error: %w", err)
}
bailOnWallet := func() {
if err := loader.UnloadWallet(); err != nil {
log.Errorf("Error unloading wallet after createSPVWallet error: %v", err)
}
}
neutrinoDBPath := filepath.Join(netDir, neutrinoDBName)
db, err := walletdb.Create("bdb", neutrinoDBPath, true, 5*time.Second)
if err != nil {
bailOnWallet()
return fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err)
}
if err = db.Close(); err != nil {
bailOnWallet()
return fmt.Errorf("error closing newly created wallet database: %w", err)
}
if err := loader.UnloadWallet(); err != nil {
return fmt.Errorf("error unloading wallet: %w", err)
}
return nil
}
var (
// loggingInited will be set when the log rotator has been initialized.
loggingInited uint32
)
// logRotator initializes a rotating file logger.
func logRotator(netDir string) (*rotator.Rotator, error) {
const maxLogRolls = 8
logDir := filepath.Join(netDir, logDirName)
if err := os.MkdirAll(logDir, 0744); err != nil {
return nil, fmt.Errorf("error creating log directory: %w", err)
}
logFilename := filepath.Join(logDir, logFileName)
return rotator.New(logFilename, 32*1024, false, maxLogRolls)
}
// logNeutrino initializes logging in the neutrino + wallet packages. Logging
// only has to be initialized once, so an atomic flag is used internally to
// return early on subsequent invocations.
//
// In theory, the the rotating file logger must be Close'd at some point, but
// there are concurrency issues with that since btcd and btcwallet have
// unsupervised goroutines still running after shutdown. So we leave the rotator
// running at the risk of losing some logs.
func logNeutrino(netDir string) error {
if !atomic.CompareAndSwapUint32(&loggingInited, 0, 1) {
return nil
}
logSpinner, err := logRotator(netDir)
if err != nil {
return fmt.Errorf("error initializing log rotator: %w", err)
}
backendLog := btclog.NewBackend(logWriter{logSpinner})
logger := func(name string, lvl btclog.Level) btclog.Logger {
l := backendLog.Logger(name)
l.SetLevel(lvl)
return l
}
neutrino.UseLogger(logger("NTRNO", btclog.LevelDebug))
wallet.UseLogger(logger("BTCW", btclog.LevelInfo))
wtxmgr.UseLogger(logger("TXMGR", btclog.LevelInfo))
chain.UseLogger(logger("CHAIN", btclog.LevelInfo))
return nil
}
// spendingInput is added to a filterScanResult if a spending input is found.
type spendingInput struct {
txHash chainhash.Hash
vin uint32
blockHash chainhash.Hash
blockHeight uint32
}
// filterScanResult is the result from a filter scan.
type filterScanResult struct {
// blockHash is the block that the output was found in.
blockHash *chainhash.Hash
// blockHeight is the height of the block that the output was found in.
blockHeight uint32
// txOut is the output itself.
txOut *wire.TxOut
// spend will be set if a spending input is found.
spend *spendingInput
// checkpoint is used to track the last block scanned so that future scans
// can skip scanned blocks.
checkpoint chainhash.Hash
}
// hashEntry stores a chainhash.Hash with a last-access time that can be used
// for cache maintenance.
type hashEntry struct {
hash chainhash.Hash
lastAccess time.Time
}
// scanCheckpoint is a cached, incomplete filterScanResult. When another scan
// is requested for an outpoint with a cached *scanCheckpoint, the scan can
// pick up where it left off.
type scanCheckpoint struct {
res *filterScanResult
lastAccess time.Time
}
// logWriter implements an io.Writer that outputs to a rotating log file.
type logWriter struct {
*rotator.Rotator
}
// Write writes the data in p to the log file.
func (w logWriter) Write(p []byte) (n int, err error) {
return w.Rotator.Write(p)
}
// spvWallet is an in-process btcwallet.Wallet + neutrino light-filter-based
// Bitcoin wallet. spvWallet controls an instance of btcwallet.Wallet directly
// and does not run or connect to the RPC server.
type spvWallet struct {
chainParams *chaincfg.Params
wallet btcWallet
cl neutrinoService
chainClient *chain.NeutrinoClient
birthday time.Time
acctNum uint32
acctName string
netDir string
neutrinoDB walletdb.DB
connectPeers []string
txBlocksMtx sync.Mutex
txBlocks map[chainhash.Hash]*hashEntry
checkpointMtx sync.Mutex
checkpoints map[outPoint]*scanCheckpoint
log dex.Logger
loader *wallet.Loader
tipChan chan *block
syncTarget int32
lastPrenatalHeight int32
// rescanStarting is set while reloading the wallet and dropping
// transactions from the wallet db.
rescanStarting uint32 // atomic
}
var _ Wallet = (*spvWallet)(nil)
var _ tipNotifier = (*spvWallet)(nil)
// loadSPVWallet loads an existing wallet.
func loadSPVWallet(dbDir string, logger dex.Logger, connectPeers []string, chainParams *chaincfg.Params) *spvWallet {
return &spvWallet{
chainParams: chainParams,
acctNum: defaultAcctNum,
acctName: defaultAcctName,
netDir: filepath.Join(dbDir, chainParams.Name),
txBlocks: make(map[chainhash.Hash]*hashEntry),
checkpoints: make(map[outPoint]*scanCheckpoint),
log: logger,
connectPeers: connectPeers,
tipChan: make(chan *block, 8),
}
}
// tipFeed satisfies the tipNotifier interface, signaling that *spvWallet
// will take precedence in sending block notifications.
func (w *spvWallet) tipFeed() <-chan *block {
return w.tipChan
}
// storeTxBlock stores the block hash for the tx in the cache.
func (w *spvWallet) storeTxBlock(txHash, blockHash chainhash.Hash) {
w.txBlocksMtx.Lock()
defer w.txBlocksMtx.Unlock()
w.txBlocks[txHash] = &hashEntry{
hash: blockHash,
lastAccess: time.Now(),
}
}
// txBlock attempts to retrieve the block hash for the tx from the cache.
func (w *spvWallet) txBlock(txHash chainhash.Hash) (chainhash.Hash, bool) {
w.txBlocksMtx.Lock()
defer w.txBlocksMtx.Unlock()
entry, found := w.txBlocks[txHash]
if !found {
return chainhash.Hash{}, false
}
entry.lastAccess = time.Now()
return entry.hash, true
}
// cacheCheckpoint caches a *filterScanResult so that future scans can be
// skipped or shortened.
func (w *spvWallet) cacheCheckpoint(txHash *chainhash.Hash, vout uint32, res *filterScanResult) {
if res.spend != nil && res.blockHash == nil {
// Probably set the start time too late. Don't cache anything
return
}
w.checkpointMtx.Lock()
defer w.checkpointMtx.Unlock()
w.checkpoints[newOutPoint(txHash, vout)] = &scanCheckpoint{
res: res,
lastAccess: time.Now(),
}
}
// unvalidatedCheckpoint returns any cached *filterScanResult for the outpoint.
func (w *spvWallet) unvalidatedCheckpoint(txHash *chainhash.Hash, vout uint32) *filterScanResult {
w.checkpointMtx.Lock()
defer w.checkpointMtx.Unlock()
check, found := w.checkpoints[newOutPoint(txHash, vout)]
if !found {
return nil
}
check.lastAccess = time.Now()
res := *check.res
return &res
}
// checkpoint returns a filterScanResult and the checkpoint block hash. If a
// result is found with an orphaned checkpoint block hash, it is cleared from
// the cache and not returned.
func (w *spvWallet) checkpoint(txHash *chainhash.Hash, vout uint32) *filterScanResult {
res := w.unvalidatedCheckpoint(txHash, vout)
if res == nil {
return nil
}
if !w.blockIsMainchain(&res.checkpoint, -1) {
// reorg detected, abandon the checkpoint.
w.log.Debugf("abandoning checkpoint %s because checkpoint block %q is orphaned",
newOutPoint(txHash, vout), res.checkpoint)
w.checkpointMtx.Lock()
delete(w.checkpoints, newOutPoint(txHash, vout))
w.checkpointMtx.Unlock()
return nil
}
return res
}
func (w *spvWallet) RawRequest(method string, params []json.RawMessage) (json.RawMessage, error) {
// Not needed for spv wallet.
return nil, errors.New("RawRequest not available on spv")
}
func (w *spvWallet) estimateSmartFee(confTarget int64, mode *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error) {
return nil, errors.New("EstimateSmartFee not available on spv")
}
func (w *spvWallet) ownsAddress(addr btcutil.Address) (bool, error) {
return w.wallet.HaveAddress(addr)
}
func (w *spvWallet) sendRawTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) {
// Publish the transaction in a goroutine so the caller may wait for a given
// period before it goes asynchronous and it is assumed that btcwallet at
// least succeeded with its DB updates and queueing of the transaction for
// rebroadcasting. In the future, a new btcwallet method should be added
// that returns after performing its internal actions, but broadcasting
// asynchronously and sending the outcome in a channel or promise.
res := make(chan error, 1)
go func() {
tStart := time.Now()
defer close(res)
if err := w.wallet.PublishTransaction(tx, ""); err != nil {
w.log.Errorf("PublishTransaction(%v) failure: %v", tx.TxHash(), err)
res <- err
return
}
defer w.log.Tracef("PublishTransaction(%v) completed in %v", tx.TxHash(),
time.Since(tStart)) // after outpoint unlocking and signalling
// bitcoind would unlock these, but it seems that btcwallet does not.
// However, it seems like they are no longer returned from ListUnspent
// even if we unlock the outpoint before the transaction is mined, so
// this is just housekeeping for btcwallet's lockedOutpoints map.
for _, txIn := range tx.TxIn {
w.wallet.UnlockOutpoint(txIn.PreviousOutPoint)
}
res <- nil
}()
select {
case err := <-res:
if err != nil {
return nil, err
}
case <-time.After(defaultBroadcastWait):
w.log.Debugf("No error from PublishTransaction after %v for txn %v. "+
"Assuming wallet accepted it.", defaultBroadcastWait, tx.TxHash())
}
txHash := tx.TxHash() // down here in case... the msgTx was mutated?
return &txHash, nil
}
func (w *spvWallet) getBlock(blockHash chainhash.Hash) (*wire.MsgBlock, error) {
block, err := w.cl.GetBlock(blockHash)
if err != nil {
return nil, fmt.Errorf("neutrino GetBlock error: %v", err)
}
return block.MsgBlock(), nil
}
func (w *spvWallet) getBlockHash(blockHeight int64) (*chainhash.Hash, error) {
return w.cl.GetBlockHash(blockHeight)
}
func (w *spvWallet) getBlockHeight(h *chainhash.Hash) (int32, error) {
return w.cl.GetBlockHeight(h)
}
func (w *spvWallet) getBestBlockHash() (*chainhash.Hash, error) {
blk := w.wallet.syncedTo()
return &blk.Hash, nil
}
// getBestBlockHeight returns the height of the best block processed by the
// wallet, which indicates the height at which the compact filters have been
// retrieved and scanned for wallet addresses. This is may be less than
// getChainHeight, which indicates the height that the chain service has reached
// in its retrieval of block headers and compact filter headers.
func (w *spvWallet) getBestBlockHeight() (int32, error) {
return w.wallet.syncedTo().Height, nil
}
// getChainHeight is only for confirmations since it does not reflect the wallet
// manager's sync height, just the chain service.
func (w *spvWallet) getChainHeight() (int32, error) {
blk, err := w.cl.BestBlock()
if err != nil {
return -1, err
}
return blk.Height, err
}
func (w *spvWallet) peerCount() (uint32, error) {
return uint32(len(w.cl.Peers())), nil
}
// syncHeight is the best known sync height among peers.
func (w *spvWallet) syncHeight() int32 {
var maxHeight int32
for _, p := range w.cl.Peers() {
tipHeight := p.StartingHeight()
lastBlockHeight := p.LastBlock()
if lastBlockHeight > tipHeight {
tipHeight = lastBlockHeight
}
if tipHeight > maxHeight {
maxHeight = tipHeight
}
}
return maxHeight
}
// syncStatus is information about the wallet's sync status.
//
// The neutrino wallet has a two stage sync:
// 1. chain service fetching block headers and filter headers
// 2. wallet address manager retrieving and scanning filters
//
// We only report a single sync height, so we are going to show some progress in
// the chain service sync stage that comes before the wallet has performed any
// address recovery/rescan, and switch to the wallet's sync height when it
// reports non-zero height.
func (w *spvWallet) syncStatus() (*syncStatus, error) {
// Chain service headers (block and filter) height.
chainBlk, err := w.cl.BestBlock()
if err != nil {
return nil, err
}
target := w.syncHeight()
currentHeight := chainBlk.Height
var synced bool
var blk *block
// Wallet address manager sync height.
if chainBlk.Timestamp.After(w.birthday) {
// After the wallet's birthday, the wallet address manager should begin
// syncing. Although block time stamps are not necessarily monotonically
// increasing, this is a reasonable condition at which the wallet's sync
// height should be consulted instead of the chain service's height.
walletBlock := w.wallet.syncedTo()
if walletBlock.Height == 0 {
// The wallet is about to start its sync, so just return the last
// chain service height prior to wallet birthday until it begins.
return &syncStatus{
Target: target,
Height: atomic.LoadInt32(&w.lastPrenatalHeight),
Syncing: true,
}, nil
}
blk = &block{
height: int64(walletBlock.Height),
hash: walletBlock.Hash,
}
currentHeight = walletBlock.Height
synced = currentHeight >= target // maybe && w.wallet.ChainSynced()
} else {
// Chain service still syncing.
blk = &block{
height: int64(currentHeight),
hash: chainBlk.Hash,
}
atomic.StoreInt32(&w.lastPrenatalHeight, currentHeight)
}
if target > 0 && atomic.SwapInt32(&w.syncTarget, target) == 0 {
w.tipChan <- blk
}
return &syncStatus{
Target: target,
Height: int32(blk.height),
Syncing: !synced,
}, nil
}
// Balances retrieves a wallet's balance details.
func (w *spvWallet) balances() (*GetBalancesResult, error) {
bals, err := w.wallet.CalculateAccountBalances(w.acctNum, 0 /* confs */)
if err != nil {
return nil, err
}
return &GetBalancesResult{
Mine: Balances{
Trusted: bals.Spendable.ToBTC(),
Untrusted: 0, // ? do we need to scan utxos instead ?
Immature: bals.ImmatureReward.ToBTC(),
},
}, nil
}
// ListUnspent retrieves list of the wallet's UTXOs.
func (w *spvWallet) listUnspent() ([]*ListUnspentResult, error) {
unspents, err := w.wallet.ListUnspent(0, math.MaxInt32, w.acctName)
if err != nil {
return nil, err
}
res := make([]*ListUnspentResult, 0, len(unspents))
for _, utxo := range unspents {
// If the utxo is unconfirmed, we should determine whether it's "safe"
// by seeing if we control the inputs of its transaction.
var safe bool
if utxo.Confirmations > 0 {
safe = true
} else {
txHash, err := chainhash.NewHashFromStr(utxo.TxID)
if err != nil {
return nil, fmt.Errorf("error decoding txid %q: %v", utxo.TxID, err)
}
txDetails, err := w.wallet.walletTransaction(txHash)
if err != nil {
return nil, fmt.Errorf("walletTransaction error: %v", err)
}
// To be "safe", we need to show that we own the inputs for the
// utxo's transaction. We'll just try to find one.
safe = true
// TODO: Keep a cache of our redemption outputs and allow those as
// safe inputs.
for _, txIn := range txDetails.MsgTx.TxIn {
_, _, _, _, err := w.wallet.FetchInputInfo(&txIn.PreviousOutPoint)
if err != nil {
if !errors.Is(err, wallet.ErrNotMine) {
w.log.Warnf("FetchInputInfo error: %v", err)
}
safe = false
break
}
}
}
pkScript, err := hex.DecodeString(utxo.ScriptPubKey)
if err != nil {
return nil, err
}
redeemScript, err := hex.DecodeString(utxo.RedeemScript)
if err != nil {
return nil, err
}
res = append(res, &ListUnspentResult{
TxID: utxo.TxID,
Vout: utxo.Vout,
Address: utxo.Address,
// Label: ,
ScriptPubKey: pkScript,
Amount: utxo.Amount,
Confirmations: uint32(utxo.Confirmations),
RedeemScript: redeemScript,
Spendable: utxo.Spendable,
// Solvable: ,
Safe: safe,
})
}
return res, nil
}
// lockUnspent locks and unlocks outputs for spending. An output that is part of
// an order, but not yet spent, should be locked until spent or until the order
// is canceled or fails.
func (w *spvWallet) lockUnspent(unlock bool, ops []*output) error {
switch {
case unlock && len(ops) == 0:
w.wallet.ResetLockedOutpoints()
default:
for _, op := range ops {
op := wire.OutPoint{Hash: op.pt.txHash, Index: op.pt.vout}
if unlock {
w.wallet.UnlockOutpoint(op)
} else {
w.wallet.LockOutpoint(op)
}
}
}
return nil
}
// listLockUnspent returns a slice of outpoints for all unspent outputs marked
// as locked by a wallet.
func (w *spvWallet) listLockUnspent() ([]*RPCOutpoint, error) {
outpoints := w.wallet.LockedOutpoints()
pts := make([]*RPCOutpoint, 0, len(outpoints))
for _, pt := range outpoints {
pts = append(pts, &RPCOutpoint{
TxID: pt.Txid,
Vout: pt.Vout,
})
}
return pts, nil
}
// changeAddress gets a new internal address from the wallet. The address will
// be bech32-encoded (P2WPKH).
func (w *spvWallet) changeAddress() (btcutil.Address, error) {
return w.wallet.NewChangeAddress(w.acctNum, waddrmgr.KeyScopeBIP0084)
}
// AddressPKH gets a new base58-encoded (P2PKH) external address from the
// wallet.
func (w *spvWallet) addressPKH() (btcutil.Address, error) {
return nil, errors.New("unimplemented")
}
// addressWPKH gets a new bech32-encoded (P2WPKH) external address from the
// wallet.
func (w *spvWallet) addressWPKH() (btcutil.Address, error) {
return w.wallet.NewAddress(w.acctNum, waddrmgr.KeyScopeBIP0084)
}
// signTx attempts to have the wallet sign the transaction inputs.
func (w *spvWallet) signTx(tx *wire.MsgTx) (*wire.MsgTx, error) {
// Can't use btcwallet.Wallet.SignTransaction, because it doesn't work for
// segwit transactions (for real?).
return tx, w.wallet.signTransaction(tx)
}
// privKeyForAddress retrieves the private key associated with the specified
// address.
func (w *spvWallet) privKeyForAddress(addr string) (*btcec.PrivateKey, error) {
a, err := btcutil.DecodeAddress(addr, w.chainParams)
if err != nil {
return nil, err
}
return w.wallet.PrivKeyForAddress(a)
}
// Unlock unlocks the wallet.
func (w *spvWallet) Unlock(pw []byte) error {
return w.wallet.Unlock(pw, nil)
}
// Lock locks the wallet.
func (w *spvWallet) Lock() error {
w.wallet.Lock()
return nil
}
// sendToAddress sends the amount to the address. feeRate is in units of
// sats/byte.
func (w *spvWallet) sendToAddress(address string, value, feeRate uint64, subtract bool) (*chainhash.Hash, error) {
addr, err := btcutil.DecodeAddress(address, w.chainParams)
if err != nil {
return nil, err
}
pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
return nil, err
}
if subtract {
return w.sendWithSubtract(pkScript, value, feeRate)
}
wireOP := wire.NewTxOut(int64(value), pkScript)
// converting sats/vB -> sats/kvB
feeRateAmt := btcutil.Amount(feeRate * 1e3)
tx, err := w.wallet.SendOutputs([]*wire.TxOut{wireOP}, nil, w.acctNum, 0, feeRateAmt, "")
if err != nil {
return nil, err
}
txHash := tx.TxHash()
return &txHash, nil
}
func (w *spvWallet) sendWithSubtract(pkScript []byte, value, feeRate uint64) (*chainhash.Hash, error) {
txOutSize := dexbtc.TxOutOverhead + uint64(len(pkScript)) // send-to address
var unfundedTxSize uint64 = dexbtc.MinimumTxOverhead + dexbtc.P2WPKHOutputSize /* change */ + txOutSize
unspents, err := w.listUnspent()
if err != nil {
return nil, fmt.Errorf("error listing unspent outputs: %w", err)
}
utxos, _, _, err := convertUnspent(0, unspents, w.chainParams)
if err != nil {
return nil, fmt.Errorf("error converting unspent outputs: %w", err)
}
// With sendWithSubtract, fees are subtracted from the sent amount, so we
// target an input sum, not an output value. Makes the math easy.
enough := func(_, inputsVal uint64) bool {
return inputsVal >= value
}
sum, inputsSize, _, fundingCoins, _, _, err := fund(utxos, enough)
if err != nil {
return nil, fmt.Errorf("error funding sendWithSubtract value of %s: %v", amount(value), err)
}
fees := (unfundedTxSize + uint64(inputsSize)) * feeRate
send := value - fees
extra := sum - send
switch {
case fees > sum:
return nil, fmt.Errorf("fees > sum")
case fees > value:
return nil, fmt.Errorf("fees > value")
case send > sum:
return nil, fmt.Errorf("send > sum")
}
tx := wire.NewMsgTx(wire.TxVersion)
for op := range fundingCoins {
wireOP := wire.NewOutPoint(&op.txHash, op.vout)
txIn := wire.NewTxIn(wireOP, []byte{}, nil)
tx.AddTxIn(txIn)
}
change := extra - fees
changeAddr, err := w.changeAddress()
if err != nil {
return nil, fmt.Errorf("error retrieving change address: %w", err)
}
changeScript, err := txscript.PayToAddrScript(changeAddr)
if err != nil {
return nil, fmt.Errorf("error generating pubkey script: %w", err)
}
changeOut := wire.NewTxOut(int64(change), changeScript)
// One last check for dust.
if dexbtc.IsDust(changeOut, feeRate) {
// Re-calculate fees and change
fees = (unfundedTxSize - dexbtc.P2WPKHOutputSize + uint64(inputsSize)) * feeRate
send = sum - fees
} else {
tx.AddTxOut(changeOut)
}
wireOP := wire.NewTxOut(int64(send), pkScript)
tx.AddTxOut(wireOP)
if err := w.wallet.signTransaction(tx); err != nil {
return nil, fmt.Errorf("signing error: %w", err)
}
return w.sendRawTransaction(tx)
}
// swapConfirmations attempts to get the number of confirmations and the spend
// status for the specified tx output. For swap outputs that were not generated
// by this wallet, startTime must be supplied to limit the search. Use the match
// time assigned by the server.
func (w *spvWallet) swapConfirmations(txHash *chainhash.Hash, vout uint32, pkScript []byte,
startTime time.Time) (confs uint32, spent bool, err error) {
// First, check if it's a wallet transaction. We probably won't be able
// to see the spend status, since the wallet doesn't track the swap contract
// output, but we can get the block if it's been mined.
blockHash, confs, spent, err := w.confirmations(txHash, vout)
if err == nil {
return confs, spent, nil
}
var assumedMempool bool
switch err {
case WalletTransactionNotFound:
w.log.Tracef("swapConfirmations - WalletTransactionNotFound: %v:%d", txHash, vout)
case SpentStatusUnknown:
w.log.Tracef("swapConfirmations - SpentStatusUnknown: %v:%d (block %v, confs %d)",
txHash, vout, blockHash, confs)
if blockHash == nil {
// We generated this swap, but it probably hasn't been mined yet.
// It's SpentStatusUnknown because the wallet doesn't track the
// spend status of the swap contract output itself, since it's not
// recognized as a wallet output. We'll still try to find the
// confirmations with other means, but if we can't find it, we'll
// report it as a zero-conf unspent output. This ignores the remote
// possibility that the output could be both in mempool and spent.
assumedMempool = true
}
default:
return 0, false, err
}
// If we still don't have the block hash, we may have it stored. Check the
// dex database first. This won't give us the confirmations and spent
// status, but it will allow us to short circuit a longer scan if we already
// know the output is spent.
if blockHash == nil {
blockHash, _ = w.mainchainBlockForStoredTx(txHash)
}
// Our last option is neutrino.
w.log.Tracef("swapConfirmations - scanFilters: %v:%d (block %v, start time %v)",
txHash, vout, blockHash, startTime)
utxo, err := w.scanFilters(txHash, vout, pkScript, startTime, blockHash)
if err != nil {
return 0, false, err
}
if utxo.spend == nil && utxo.blockHash == nil {
if assumedMempool {
w.log.Tracef("swapConfirmations - scanFilters did not find %v:%d, assuming in mempool.",
txHash, vout)
// NOT asset.CoinNotFoundError since this is normal for mempool
// transactions with an SPV wallet.
return 0, false, nil
}
return 0, false, fmt.Errorf("output %s:%v not found with search parameters startTime = %s, pkScript = %x",
txHash, vout, startTime, pkScript)
}
if utxo.blockHash != nil {
bestHeight, err := w.getChainHeight()
if err != nil {
return 0, false, fmt.Errorf("getBestBlockHeight error: %v", err)
}
confs = uint32(bestHeight) - utxo.blockHeight + 1
}
if utxo.spend != nil {
// In the off-chance that a spend was found but not the output itself,
// confs will be incorrect here.
// In situations where we're looking for the counter-party's swap, we
// revoke if it's found to be spent, without inspecting the confs, so
// accuracy of confs is not significant. When it's our output, we'll
// know the block and won't end up here. (even if we did, we just end up
// sending out some inaccurate Data-severity notifications to the UI
// until the match progresses)
return confs, true, nil
}
// unspent
return confs, false, nil
}
func (w *spvWallet) locked() bool {
return w.wallet.Locked()
}
func (w *spvWallet) walletLock() error {
w.wallet.Lock()
return nil
}
func (w *spvWallet) walletUnlock(pw []byte) error {
return w.Unlock(pw)
}
func (w *spvWallet) getBlockHeader(blockHash *chainhash.Hash) (*blockHeader, error) {
hdr, err := w.cl.GetBlockHeader(blockHash)
if err != nil {
return nil, err
}
medianTime, err := w.calcMedianTime(blockHash)
if err != nil {
return nil, err
}
tip, err := w.cl.BestBlock()
if err != nil {
return nil, fmt.Errorf("BestBlock error: %v", err)
}
blockHeight, err := w.cl.GetBlockHeight(blockHash)
if err != nil {
return nil, err
}
return &blockHeader{
Hash: hdr.BlockHash().String(),
Confirmations: int64(confirms(blockHeight, tip.Height)),
Height: int64(blockHeight),
Time: hdr.Timestamp.Unix(),
MedianTime: medianTime.Unix(),
}, nil
}
const medianTimeBlocks = 11
// calcMedianTime calculates the median time of the previous 11 block headers.
// The median time is used for validating time-locked transactions. See notes in
// btcd/blockchain (*blockNode).CalcPastMedianTime() regarding incorrectly
// calculated median time for blocks 1, 3, 5, 7, and 9.
func (w *spvWallet) calcMedianTime(blockHash *chainhash.Hash) (time.Time, error) {
timestamps := make([]int64, 0, medianTimeBlocks)
zeroHash := chainhash.Hash{}
h := blockHash
for i := 0; i < medianTimeBlocks; i++ {
hdr, err := w.cl.GetBlockHeader(h)
if err != nil {
return time.Time{}, fmt.Errorf("BlockHeader error for hash %q: %v", h, err)
}
timestamps = append(timestamps, hdr.Timestamp.Unix())