diff --git a/chain/interface.go b/chain/interface.go index 32e0bc4..67e864e 100644 --- a/chain/interface.go +++ b/chain/interface.go @@ -1,10 +1,13 @@ package chain import ( + "time" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wtxmgr" ) // Interface allows more than one backing blockchain source, such as a @@ -23,3 +26,44 @@ type Interface interface { NotifyBlocks() error Notifications() <-chan interface{} } + +// Notification types. These are defined here and processed from from reading +// a notificationChan to avoid handling these notifications directly in +// btcrpcclient callbacks, which isn't very Go-like and doesn't allow +// blocking client calls. +type ( + // ClientConnected is a notification for when a client connection is + // opened or reestablished to the chain server. + ClientConnected struct{} + + // BlockConnected is a notification for a newly-attached block to the + // best chain. + BlockConnected wtxmgr.BlockMeta + + // BlockDisconnected is a notifcation that the block described by the + // BlockStamp was reorganized out of the best chain. + BlockDisconnected wtxmgr.BlockMeta + + // RelevantTx is a notification for a transaction which spends wallet + // inputs or pays to a watched address. + RelevantTx struct { + TxRecord *wtxmgr.TxRecord + Block *wtxmgr.BlockMeta // nil if unmined + } + + // RescanProgress is a notification describing the current status + // of an in-progress rescan. + RescanProgress struct { + Hash *chainhash.Hash + Height int32 + Time time.Time + } + + // RescanFinished is a notification that a previous rescan request + // has finished. + RescanFinished struct { + Hash *chainhash.Hash + Height int32 + Time time.Time + } +) diff --git a/chain/neutrino.go b/chain/neutrino.go new file mode 100644 index 0000000..4900a8c --- /dev/null +++ b/chain/neutrino.go @@ -0,0 +1,312 @@ +package chain + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcrpcclient" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/lightninglabs/neutrino" +) + +// SPVChain is an implementation of the btcwalet chain.Interface interface. +type SPVChain struct { + cs *neutrino.ChainService + + // We currently support one rescan/notifiction goroutine per client + rescan *neutrino.Rescan + + enqueueNotification chan interface{} + dequeueNotification chan interface{} + currentBlock chan *waddrmgr.BlockStamp + + quit chan struct{} + rescanQuit chan struct{} + wg sync.WaitGroup + started bool + scanning bool + + clientMtx sync.Mutex +} + +// NewSPVChain creates a new SPVChain struct with a backing ChainService +func NewSPVChain(chainService *neutrino.ChainService) *SPVChain { + return &SPVChain{cs: chainService} +} + +// Start replicates the RPC client's Start method. +func (s *SPVChain) Start() error { + s.cs.Start() + s.clientMtx.Lock() + defer s.clientMtx.Unlock() + if !s.started { + s.enqueueNotification = make(chan interface{}) + s.dequeueNotification = make(chan interface{}) + s.currentBlock = make(chan *waddrmgr.BlockStamp) + s.quit = make(chan struct{}) + s.started = true + s.wg.Add(1) + go s.notificationHandler() + } + return nil +} + +// Stop replicates the RPC client's Stop method. +func (s *SPVChain) Stop() { + s.clientMtx.Lock() + defer s.clientMtx.Unlock() + if !s.started { + return + } + close(s.quit) + s.started = false +} + +// WaitForShutdown replicates the RPC client's WaitForShutdown method. +func (s *SPVChain) WaitForShutdown() { + s.wg.Wait() +} + +// GetBlock replicates the RPC client's GetBlock command. +func (s *SPVChain) GetBlock(hash *chainhash.Hash) (*wire.MsgBlock, error) { + // TODO(roasbeef): add a block cache? + // * which evication strategy? depends on use case + block, err := s.cs.GetBlockFromNetwork(*hash) + if err != nil { + return nil, err + } + return block.MsgBlock(), nil +} + +// GetBestBlock replicates the RPC client's GetBestBlock command. +func (s *SPVChain) GetBestBlock() (*chainhash.Hash, int32, error) { + header, height, err := s.cs.LatestBlock() + if err != nil { + return nil, 0, err + } + hash := header.BlockHash() + return &hash, int32(height), nil +} + +// BlockStamp returns the latest block notified by the client, or an error +// if the client has been shut down. +func (s *SPVChain) BlockStamp() (*waddrmgr.BlockStamp, error) { + select { + case bs := <-s.currentBlock: + return bs, nil + case <-s.quit: + return nil, errors.New("disconnected") + } +} + +// SendRawTransaction replicates the RPC client's SendRawTransaction command. +func (s *SPVChain) SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) ( + *chainhash.Hash, error) { + err := s.cs.SendTransaction(tx) + if err != nil { + return nil, err + } + hash := tx.TxHash() + return &hash, nil +} + +// Rescan replicates the RPC client's Rescan command. +func (s *SPVChain) Rescan(startHash *chainhash.Hash, addrs []btcutil.Address, + outPoints []*wire.OutPoint) error { + s.clientMtx.Lock() + if !s.started { + s.clientMtx.Unlock() + return fmt.Errorf("can't do a rescan when the chain client " + + "is not started") + } + if s.scanning { + // Restart the rescan by killing the existing rescan. + close(s.rescanQuit) + } + s.rescanQuit = make(chan struct{}) + s.scanning = true + s.clientMtx.Unlock() + return s.cs.Rescan( + neutrino.NotificationHandlers(btcrpcclient.NotificationHandlers{ + OnFilteredBlockConnected: s.onFilteredBlockConnected, + OnBlockDisconnected: s.onBlockDisconnected, + }), + neutrino.QuitChan(s.rescanQuit), + ) +} + +// NotifyBlocks replicates the RPC client's NotifyBlocks command. +func (s *SPVChain) NotifyBlocks() error { + return nil +} + +// NotifyReceived replicates the RPC client's NotifyReceived command. +func (s *SPVChain) NotifyReceived() error { + return nil +} + +// Notifications replicates the RPC client's Notifications method. +func (s *SPVChain) Notifications() <-chan interface{} { + return s.dequeueNotification +} + +// onFilteredBlockConnected sends appropriate notifications to the notification +// channel. +func (s *SPVChain) onFilteredBlockConnected(height int32, + header *wire.BlockHeader, relevantTxs []*btcutil.Tx) { + blockMeta := wtxmgr.BlockMeta{ + Block: wtxmgr.Block{ + Hash: header.BlockHash(), + Height: height, + }, + Time: header.Timestamp, + } + select { + case s.enqueueNotification <- BlockConnected(blockMeta): + case <-s.quit: + return + case <-s.rescanQuit: + return + } + for _, tx := range relevantTxs { + rec, err := wtxmgr.NewTxRecordFromMsgTx(tx.MsgTx(), + blockMeta.Time) + if err != nil { + log.Errorf("Cannot create transaction record for "+ + "relevant tx: %s", err) + // TODO(aakselrod): Continue? + return + } + select { + case s.enqueueNotification <- RelevantTx{ + TxRecord: rec, + Block: &blockMeta, + }: + case <-s.quit: + return + case <-s.rescanQuit: + return + } + } + bs, err := s.cs.SyncedTo() + if err != nil { + log.Errorf("Can't get chain service's best block: %s", err) + return + } + if bs.Hash == header.BlockHash() { + select { + case s.enqueueNotification <- RescanFinished{ + Hash: &bs.Hash, + Height: bs.Height, + Time: header.Timestamp, + }: + case <-s.quit: + return + case <-s.rescanQuit: + return + + } + } +} + +// onBlockDisconnected sends appropriate notifications to the notification +// channel. +func (s *SPVChain) onBlockDisconnected(hash *chainhash.Hash, height int32, + t time.Time) { + select { + case s.enqueueNotification <- BlockDisconnected{ + Block: wtxmgr.Block{ + Hash: *hash, + Height: height, + }, + Time: t, + }: + case <-s.quit: + case <-s.rescanQuit: + } +} + +// notificationHandler queues and dequeues notifications. There are currently +// no bounds on the queue, so the dequeue channel should be read continually to +// avoid running out of memory. +func (s *SPVChain) notificationHandler() { + hash, height, err := s.GetBestBlock() + if err != nil { + log.Errorf("Failed to get best block from chain service: %s", + err) + s.Stop() + s.wg.Done() + return + } + + bs := &waddrmgr.BlockStamp{Hash: *hash, Height: height} + + // TODO: Rather than leaving this as an unbounded queue for all types of + // notifications, try dropping ones where a later enqueued notification + // can fully invalidate one waiting to be processed. For example, + // blockconnected notifications for greater block heights can remove the + // need to process earlier blockconnected notifications still waiting + // here. + + var notifications []interface{} + enqueue := s.enqueueNotification + var dequeue chan interface{} + var next interface{} +out: + for { + select { + case n, ok := <-enqueue: + if !ok { + // If no notifications are queued for handling, + // the queue is finished. + if len(notifications) == 0 { + break out + } + // nil channel so no more reads can occur. + enqueue = nil + continue + } + if len(notifications) == 0 { + next = n + dequeue = s.dequeueNotification + } + notifications = append(notifications, n) + + case dequeue <- next: + if n, ok := next.(BlockConnected); ok { + bs = &waddrmgr.BlockStamp{ + Height: n.Height, + Hash: n.Hash, + } + } + + notifications[0] = nil + notifications = notifications[1:] + if len(notifications) != 0 { + next = notifications[0] + } else { + // If no more notifications can be enqueued, the + // queue is finished. + if enqueue == nil { + break out + } + dequeue = nil + } + + case s.currentBlock <- bs: + + case <-s.quit: + break out + } + } + + s.Stop() + close(s.dequeueNotification) + s.wg.Done() +} diff --git a/chain/chain.go b/chain/rpc.go similarity index 89% rename from chain/chain.go rename to chain/rpc.go index 65c5874..4aae8d0 100644 --- a/chain/chain.go +++ b/chain/rpc.go @@ -138,47 +138,6 @@ func (c *RPCClient) WaitForShutdown() { c.wg.Wait() } -// Notification types. These are defined here and processed from from reading -// a notificationChan to avoid handling these notifications directly in -// btcrpcclient callbacks, which isn't very Go-like and doesn't allow -// blocking client calls. -type ( - // ClientConnected is a notification for when a client connection is - // opened or reestablished to the chain server. - ClientConnected struct{} - - // BlockConnected is a notification for a newly-attached block to the - // best chain. - BlockConnected wtxmgr.BlockMeta - - // BlockDisconnected is a notifcation that the block described by the - // BlockStamp was reorganized out of the best chain. - BlockDisconnected wtxmgr.BlockMeta - - // RelevantTx is a notification for a transaction which spends wallet - // inputs or pays to a watched address. - RelevantTx struct { - TxRecord *wtxmgr.TxRecord - Block *wtxmgr.BlockMeta // nil if unmined - } - - // RescanProgress is a notification describing the current status - // of an in-progress rescan. - RescanProgress struct { - Hash *chainhash.Hash - Height int32 - Time time.Time - } - - // RescanFinished is a notification that a previous rescan request - // has finished. - RescanFinished struct { - Hash *chainhash.Hash - Height int32 - Time time.Time - } -) - // Notifications returns a channel of parsed notifications sent by the remote // bitcoin RPC server. This channel must be continually read or the process // may abort for running out memory, as unread notifications are queued for diff --git a/config.go b/config.go index 5f47eba..64d187b 100644 --- a/config.go +++ b/config.go @@ -19,9 +19,9 @@ import ( "github.com/btcsuite/btcwallet/internal/cfgutil" "github.com/btcsuite/btcwallet/internal/legacy/keystore" "github.com/btcsuite/btcwallet/netparams" - "github.com/btcsuite/btcwallet/spvsvc/spvchain" "github.com/btcsuite/btcwallet/wallet" flags "github.com/jessevdk/go-flags" + "github.com/lightninglabs/neutrino" ) const ( @@ -270,9 +270,9 @@ func loadConfig() (*config, []string, error) { UseSPV: false, AddPeers: []string{}, ConnectPeers: []string{}, - MaxPeers: spvchain.MaxPeers, - BanDuration: spvchain.BanDuration, - BanThreshold: spvchain.BanThreshold, + MaxPeers: neutrino.MaxPeers, + BanDuration: neutrino.BanDuration, + BanThreshold: neutrino.BanThreshold, } // Pre-parse the command line options to see if an alternative config diff --git a/glide.lock b/glide.lock index bc66777..3a46534 100644 --- a/glide.lock +++ b/glide.lock @@ -1,28 +1,55 @@ +<<<<<<< HEAD hash: 2fe59efc96b0a2839297653da88cde89208f8f8cf4ced2bb1e828def57e3611b updated: 2017-07-19T11:33:58.0769452-04:00 +======= +hash: a567152c861b11d05c72b812d894a657b95ae39d08f85c0b7cbf1a235dd8a1a7 +updated: 2017-05-18T20:28:25.480256291-06:00 +>>>>>>> 4d479d4... Move spvchain into neutrino and start integration w/btcwallet imports: +- name: github.com/aead/siphash + version: e404fcfc888570cadd1610538e2dbc89f66af814 - name: github.com/boltdb/bolt version: 583e8937c61f1af6513608ccc75c97b6abdf4ff9 - name: github.com/btcsuite/btcd +<<<<<<< HEAD version: 47885ab8702485be6b6f87a03d4f3be0bc5c982c +======= + version: 58668c182103a00c5038c7130d7a23fad3a3bd34 + repo: git@github.com:companyzero/btcdln +>>>>>>> 4d479d4... Move spvchain into neutrino and start integration w/btcwallet subpackages: + - addrmgr - blockchain - btcec - btcjson - chaincfg - chaincfg/chainhash + - connmgr - database + - peer - txscript - wire - name: github.com/btcsuite/btclog version: 84c8d2346e9fc8c7b947e243b9c24e6df9fd206a - name: github.com/btcsuite/btcrpcclient +<<<<<<< HEAD version: c72658166ae09457e6beb14e9112241e352ebd35 - name: github.com/btcsuite/btcutil version: 5ffa719c3882fd2ec1e8b9f4978066701c31a343 +======= + version: e15bd09b466511d8836c4017bc4cbc2e0ff05f82 + repo: git@github.com:companyzero/btcrpcclientln +- name: github.com/btcsuite/btcutil + version: f814b35f15362b1122f55bd9034b6275552cc2f7 + repo: git@github.com:companyzero/btcutilln +>>>>>>> 4d479d4... Move spvchain into neutrino and start integration w/btcwallet subpackages: - base58 + - gcs + - gcs/builder - hdkeychain +- name: github.com/btcsuite/fastsha256 + version: 637e656429416087660c84436a2a035d69d54e2e - name: github.com/btcsuite/go-socks version: 4720035b7bfd2a9bb130b1c184f8bbe41b6f0d0f subpackages: @@ -37,8 +64,17 @@ imports: - salsa20/salsa - scrypt - ssh/terminal +<<<<<<< HEAD +======= +- name: github.com/btcsuite/seelog + version: ae8891d029dd3c269dcfd6f261ad23e761acd99f +>>>>>>> 4d479d4... Move spvchain into neutrino and start integration w/btcwallet - name: github.com/btcsuite/websocket version: 31079b6807923eb23992c421b114992b95131b55 +- name: github.com/davecgh/go-spew + version: 346938d642f2ec3594ed81d874461961cd0faa76 + subpackages: + - spew - name: github.com/golang/protobuf version: fec3b39b059c0f88fa6b20f5ed012b1aa203a8b4 subpackages: @@ -46,6 +82,7 @@ imports: - ptypes/any - name: github.com/jessevdk/go-flags version: 1679536dcc895411a9f5848d9a0250be7856448c +<<<<<<< HEAD - name: github.com/jrick/logrotate version: a93b200c26cbae3bb09dd0dc2c7c7fe1468a034a subpackages: @@ -56,6 +93,19 @@ imports: - ripemd160 - name: golang.org/x/net version: 8663ed5da4fd087c3cfb99a996e628b72e2f0948 +======= +- name: github.com/kkdai/bstream + version: f391b8402d23024e7c0f624b31267a89998fca95 +- name: github.com/lightninglabs/neutrino + version: 3a3b87d375c492f22de128f4f5df889e0340bd35 + repo: git@github.com:lightninglabs/neutrino +- name: golang.org/x/crypto + version: 0fe963104e9d1877082f8fb38f816fcd97eb1d10 + subpackages: + - ssh/terminal +- name: golang.org/x/net + version: 513929065c19401a1c7b76ecd942f9f86a0c061b +>>>>>>> 4d479d4... Move spvchain into neutrino and start integration w/btcwallet subpackages: - context - http2 @@ -65,11 +115,19 @@ imports: - lex/httplex - trace - name: golang.org/x/sys +<<<<<<< HEAD version: cd2c276457edda6df7fb04895d3fd6a6add42926 subpackages: - unix - name: golang.org/x/text version: 6353ef0f924300eea566d3438817aa4d3374817e +======= + version: e62c3de784db939836898e5c19ffd41bece347da + subpackages: + - unix +- name: golang.org/x/text + version: 19e51611da83d6be54ddafce4a4af510cb3e9ea4 +>>>>>>> 4d479d4... Move spvchain into neutrino and start integration w/btcwallet subpackages: - secure/bidirule - transform @@ -95,8 +153,4 @@ imports: - status - tap - transport -testImports: -- name: github.com/davecgh/go-spew - version: 346938d642f2ec3594ed81d874461961cd0faa76 - subpackages: - - spew +testImports: [] diff --git a/glide.yaml b/glide.yaml index 16c3bb8..7c99293 100644 --- a/glide.yaml +++ b/glide.yaml @@ -3,6 +3,8 @@ import: - package: github.com/boltdb/bolt version: ^1.3.0 - package: github.com/btcsuite/btcd + repo: git@github.com:companyzero/btcdln + version: segwit-cbf subpackages: - blockchain - btcec @@ -13,9 +15,15 @@ import: - wire - package: github.com/btcsuite/btclog - package: github.com/btcsuite/btcrpcclient + repo: git@github.com:companyzero/btcrpcclientln + version: pedro_cbf - package: github.com/btcsuite/btcutil + repo: git@github.com:companyzero/btcutilln + version: gcs subpackages: - hdkeychain +- package: github.com/lightninglabs/neutrino + repo: git@github.com:lightninglabs/neutrino - package: github.com/btcsuite/golangcrypto subpackages: - nacl/secretbox @@ -42,6 +50,5 @@ import: - rotator testImport: - package: github.com/davecgh/go-spew - version: ^1.1.0 subpackages: - spew diff --git a/log.go b/log.go index 8f55218..9315958 100644 --- a/log.go +++ b/log.go @@ -13,7 +13,6 @@ import ( "github.com/btcsuite/btcwallet/chain" "github.com/btcsuite/btcwallet/rpc/legacyrpc" "github.com/btcsuite/btcwallet/rpc/rpcserver" - "github.com/btcsuite/btcwallet/spvsvc/spvchain" "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wtxmgr" "github.com/jrick/logrotate/rotator" @@ -73,7 +72,7 @@ var subsystemLoggers = map[string]btclog.Logger{ "CHNS": chainLog, "GRPC": grpcLog, "RPCS": legacyRPCLog, - "SPVC": spvchainLog, + "BTCN": btcnLog, } // initLogRotator initializes the logging rotater to write logs to logFile and @@ -129,9 +128,9 @@ func useLogger(subsystemID string, logger btclog.Logger) { case "RPCS": legacyRPCLog = logger legacyrpc.UseLogger(logger) - case "SPVC": - spvchainLog = logger - spvchain.UseLogger(logger) + case "BTCN": + btcnLog = logger + neutrino.UseLogger(logger) } r, err := rotator.New(logFile, 10*1024, false, 3) if err != nil { diff --git a/spvsvc/log.go b/spvsvc/log.go deleted file mode 100644 index 05be319..0000000 --- a/spvsvc/log.go +++ /dev/null @@ -1,26 +0,0 @@ -package spvsvc - -import "github.com/btcsuite/btclog" - -// log is a logger that is initialized with no output filters. This -// means the package will not perform any logging by default until the caller -// requests it. -var log btclog.Logger - -// The default amount of logging is none. -func init() { - DisableLog() -} - -// DisableLog disables all library log output. Logging output is disabled -// by default until either UseLogger or SetLogWriter are called. -func DisableLog() { - log = btclog.Disabled -} - -// UseLogger uses a specified Logger to output package logging info. -// This should be used in preference to SetLogWriter if the caller is also -// using btclog. -func UseLogger(logger btclog.Logger) { - log = logger -} diff --git a/spvsvc/spvchain/blocklogger.go b/spvsvc/spvchain/blocklogger.go deleted file mode 100644 index dbc3c69..0000000 --- a/spvsvc/spvchain/blocklogger.go +++ /dev/null @@ -1,76 +0,0 @@ -package spvchain - -import ( - "sync" - "time" - - "github.com/btcsuite/btclog" - "github.com/btcsuite/btcutil" -) - -// blockProgressLogger provides periodic logging for other services in order -// to show users progress of certain "actions" involving some or all current -// blocks. Ex: syncing to best chain, indexing all blocks, etc. -type blockProgressLogger struct { - receivedLogBlocks int64 - receivedLogTx int64 - lastBlockLogTime time.Time - - subsystemLogger btclog.Logger - progressAction string - sync.Mutex -} - -// newBlockProgressLogger returns a new block progress logger. -// The progress message is templated as follows: -// {progressAction} {numProcessed} {blocks|block} in the last {timePeriod} -// ({numTxs}, height {lastBlockHeight}, {lastBlockTimeStamp}) -func newBlockProgressLogger(progressMessage string, logger btclog.Logger) *blockProgressLogger { - return &blockProgressLogger{ - lastBlockLogTime: time.Now(), - progressAction: progressMessage, - subsystemLogger: logger, - } -} - -// LogBlockHeight logs a new block height as an information message to show -// progress to the user. In order to prevent spam, it limits logging to one -// message every 10 seconds with duration and totals included. -func (b *blockProgressLogger) LogBlockHeight(block *btcutil.Block) { - b.Lock() - defer b.Unlock() - - b.receivedLogBlocks++ - b.receivedLogTx += int64(len(block.MsgBlock().Transactions)) - - now := time.Now() - duration := now.Sub(b.lastBlockLogTime) - if duration < time.Second*10 { - return - } - - // Truncate the duration to 10s of milliseconds. - durationMillis := int64(duration / time.Millisecond) - tDuration := 10 * time.Millisecond * time.Duration(durationMillis/10) - - // Log information about new block height. - blockStr := "blocks" - if b.receivedLogBlocks == 1 { - blockStr = "block" - } - txStr := "transactions" - if b.receivedLogTx == 1 { - txStr = "transaction" - } - b.subsystemLogger.Infof("%s %d %s in the last %s (%d %s, height %d, %s)", - b.progressAction, b.receivedLogBlocks, blockStr, tDuration, b.receivedLogTx, - txStr, block.Height(), block.MsgBlock().Header.Timestamp) - - b.receivedLogBlocks = 0 - b.receivedLogTx = 0 - b.lastBlockLogTime = now -} - -func (b *blockProgressLogger) SetLastLogTime(time time.Time) { - b.lastBlockLogTime = time -} diff --git a/spvsvc/spvchain/blockmanager.go b/spvsvc/spvchain/blockmanager.go deleted file mode 100644 index c7454e7..0000000 --- a/spvsvc/spvchain/blockmanager.go +++ /dev/null @@ -1,1465 +0,0 @@ -// NOTE: THIS API IS UNSTABLE RIGHT NOW AND WILL GO MOSTLY PRIVATE SOON. - -package spvchain - -import ( - "container/list" - "fmt" - "math/big" - "sync" - "sync/atomic" - "time" - - "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcutil" -) - -const ( - // minInFlightBlocks is the minimum number of blocks that should be - // in the request queue for headers-first mode before requesting - // more. - minInFlightBlocks = 10 - - // blockDbNamePrefix is the prefix for the block database name. The - // database type is appended to this value to form the full block - // database name. - blockDbNamePrefix = "blocks" - - // maxRequestedBlocks is the maximum number of requested block - // hashes to store in memory. - maxRequestedBlocks = wire.MaxInvPerMsg - - // maxTimeOffset is the maximum duration a block time is allowed to be - // ahead of the curent time. This is currently 2 hours. - maxTimeOffset = 2 * time.Hour -) - -// TODO: Redo this using query API. -var ( - // WaitForMoreCFHeaders is a configurable time to wait for CFHeaders - // messages from peers. It defaults to 3 seconds but can be increased - // for higher security and decreased for faster synchronization. - WaitForMoreCFHeaders = 3 * time.Second -) - -// zeroHash is the zero value hash (all zeros). It is defined as a convenience. -var zeroHash chainhash.Hash - -// newPeerMsg signifies a newly connected peer to the block handler. -type newPeerMsg struct { - peer *serverPeer -} - -// blockMsg packages a bitcoin block message and the peer it came from together -// so the block handler has access to that information. -type blockMsg struct { - block *btcutil.Block - peer *serverPeer -} - -// invMsg packages a bitcoin inv message and the peer it came from together -// so the block handler has access to that information. -type invMsg struct { - inv *wire.MsgInv - peer *serverPeer -} - -// headersMsg packages a bitcoin headers message and the peer it came from -// together so the block handler has access to that information. -type headersMsg struct { - headers *wire.MsgHeaders - peer *serverPeer -} - -// cfheadersMsg packages a bitcoin cfheaders message and the peer it came from -// together so the block handler has access to that information. -type cfheadersMsg struct { - cfheaders *wire.MsgCFHeaders - peer *serverPeer -} - -// cfheadersProcessedMsg tells the block manager to try to see if there are -// enough samples of cfheaders messages to process the committed filter header -// chain. This is kind of a hack until these get soft-forked in, but we do -// verification to avoid getting bamboozled by malicious nodes. -type processCFHeadersMsg struct { - earliestNode *headerNode - stopHash chainhash.Hash - extended bool -} - -// donePeerMsg signifies a newly disconnected peer to the block handler. -type donePeerMsg struct { - peer *serverPeer -} - -// txMsg packages a bitcoin tx message and the peer it came from together -// so the block handler has access to that information. -type txMsg struct { - tx *btcutil.Tx - peer *serverPeer -} - -// isCurrentMsg is a message type to be sent across the message channel for -// requesting whether or not the block manager believes it is synced with -// the currently connected peers. -type isCurrentMsg struct { - reply chan bool -} - -// headerNode is used as a node in a list of headers that are linked together -// between checkpoints. -type headerNode struct { - height int32 - header *wire.BlockHeader -} - -// blockManager provides a concurrency safe block manager for handling all -// incoming blocks. -type blockManager struct { - server *ChainService - started int32 - shutdown int32 - requestedBlocks map[chainhash.Hash]struct{} - progressLogger *blockProgressLogger - syncPeer *serverPeer - syncPeerMutex sync.Mutex - // Channel for messages that come from peers - peerChan chan interface{} - // Channel for messages that come from internal commands - intChan chan interface{} - wg sync.WaitGroup - quit chan struct{} - - headerList *list.List - reorgList *list.List - startHeader *list.Element - nextCheckpoint *chaincfg.Checkpoint - lastRequested chainhash.Hash - - basicHeaders map[chainhash.Hash]map[chainhash.Hash][]*serverPeer - lastBasicCFHeaderHeight int32 - numBasicCFHeadersMsgs int32 - extendedHeaders map[chainhash.Hash]map[chainhash.Hash][]*serverPeer - lastExtCFHeaderHeight int32 - numExtCFHeadersMsgs int32 - mapMutex sync.Mutex - - minRetargetTimespan int64 // target timespan / adjustment factor - maxRetargetTimespan int64 // target timespan * adjustment factor - blocksPerRetarget int32 // target timespan / target time per block -} - -// newBlockManager returns a new bitcoin block manager. -// Use Start to begin processing asynchronous block and inv updates. -func newBlockManager(s *ChainService) (*blockManager, error) { - targetTimespan := int64(s.chainParams.TargetTimespan / time.Second) - targetTimePerBlock := int64(s.chainParams.TargetTimePerBlock / time.Second) - adjustmentFactor := s.chainParams.RetargetAdjustmentFactor - - bm := blockManager{ - server: s, - requestedBlocks: make(map[chainhash.Hash]struct{}), - progressLogger: newBlockProgressLogger("Processed", log), - peerChan: make(chan interface{}, MaxPeers*3), - intChan: make(chan interface{}, 1), - headerList: list.New(), - reorgList: list.New(), - quit: make(chan struct{}), - blocksPerRetarget: int32(targetTimespan / targetTimePerBlock), - minRetargetTimespan: targetTimespan / adjustmentFactor, - maxRetargetTimespan: targetTimespan * adjustmentFactor, - basicHeaders: make( - map[chainhash.Hash]map[chainhash.Hash][]*serverPeer, - ), - extendedHeaders: make( - map[chainhash.Hash]map[chainhash.Hash][]*serverPeer, - ), - } - - // Initialize the next checkpoint based on the current height. - header, height, err := s.LatestBlock() - if err != nil { - return nil, err - } - bm.nextCheckpoint = bm.findNextHeaderCheckpoint(int32(height)) - bm.resetHeaderState(&header, int32(height)) - - return &bm, nil -} - -// Start begins the core block handler which processes block and inv messages. -func (b *blockManager) Start() { - // Already started? - if atomic.AddInt32(&b.started, 1) != 1 { - return - } - - log.Trace("Starting block manager") - b.wg.Add(1) - go b.blockHandler() -} - -// Stop gracefully shuts down the block manager by stopping all asynchronous -// handlers and waiting for them to finish. -func (b *blockManager) Stop() error { - if atomic.AddInt32(&b.shutdown, 1) != 1 { - log.Warnf("Block manager is already in the process of " + - "shutting down") - return nil - } - - log.Infof("Block manager shutting down") - close(b.quit) - b.wg.Wait() - return nil -} - -// NewPeer informs the block manager of a newly active peer. -func (b *blockManager) NewPeer(sp *serverPeer) { - // Ignore if we are shutting down. - if atomic.LoadInt32(&b.shutdown) != 0 { - return - } - b.peerChan <- &newPeerMsg{peer: sp} -} - -// handleNewPeerMsg deals with new peers that have signalled they may -// be considered as a sync peer (they have already successfully negotiated). It -// also starts syncing if needed. It is invoked from the syncHandler goroutine. -func (b *blockManager) handleNewPeerMsg(peers *list.List, sp *serverPeer) { - // Ignore if in the process of shutting down. - if atomic.LoadInt32(&b.shutdown) != 0 { - return - } - - log.Infof("New valid peer %s (%s)", sp, sp.UserAgent()) - - // Ignore the peer if it's not a sync candidate. - if !b.isSyncCandidate(sp) { - return - } - - // Add the peer as a candidate to sync from. - peers.PushBack(sp) - - // Start syncing by choosing the best candidate if needed. - b.startSync(peers) -} - -// DonePeer informs the blockmanager that a peer has disconnected. -func (b *blockManager) DonePeer(sp *serverPeer) { - // Ignore if we are shutting down. - if atomic.LoadInt32(&b.shutdown) != 0 { - return - } - - b.peerChan <- &donePeerMsg{peer: sp} -} - -// handleDonePeerMsg deals with peers that have signalled they are done. It -// removes the peer as a candidate for syncing and in the case where it was -// the current sync peer, attempts to select a new best peer to sync from. It -// is invoked from the syncHandler goroutine. -func (b *blockManager) handleDonePeerMsg(peers *list.List, sp *serverPeer) { - // Remove the peer from the list of candidate peers. - for e := peers.Front(); e != nil; e = e.Next() { - if e.Value == sp { - peers.Remove(e) - break - } - } - - log.Infof("Lost peer %s", sp) - - // Attempt to find a new peer to sync from if the quitting peer is the - // sync peer. Also, reset the header state. - if b.syncPeer != nil && b.syncPeer == sp { - b.syncPeerMutex.Lock() - b.syncPeer = nil - b.syncPeerMutex.Unlock() - header, height, err := b.server.LatestBlock() - if err != nil { - return - } - b.resetHeaderState(&header, int32(height)) - b.startSync(peers) - } -} - -// blockHandler is the main handler for the block manager. It must be run -// as a goroutine. It processes block and inv messages in a separate goroutine -// from the peer handlers so the block (MsgBlock) messages are handled by a -// single thread without needing to lock memory data structures. This is -// important because the block manager controls which blocks are needed and how -// the fetching should proceed. -func (b *blockManager) blockHandler() { - candidatePeers := list.New() -out: - for { - // Check internal messages channel first and continue if there's - // nothing to process. - select { - case m := <-b.intChan: - switch msg := m.(type) { - case *processCFHeadersMsg: - b.handleProcessCFHeadersMsg(msg) - - default: - log.Warnf("Invalid message type in block "+ - "handler: %T", msg) - } - default: - } - // Now check peer messages and quit channels. - select { - case m := <-b.peerChan: - switch msg := m.(type) { - case *newPeerMsg: - b.handleNewPeerMsg(candidatePeers, msg.peer) - - case *invMsg: - b.handleInvMsg(msg) - - case *headersMsg: - b.handleHeadersMsg(msg) - - case *cfheadersMsg: - b.handleCFHeadersMsg(msg) - - case *donePeerMsg: - b.handleDonePeerMsg(candidatePeers, msg.peer) - - case isCurrentMsg: - msg.reply <- b.current() - - default: - log.Warnf("Invalid message type in block "+ - "handler: %T", msg) - } - - case <-b.quit: - break out - } - } - - b.wg.Done() - log.Trace("Block handler done") -} - -// SyncPeer returns the current sync peer. -func (b *blockManager) SyncPeer() *serverPeer { - b.syncPeerMutex.Lock() - defer b.syncPeerMutex.Unlock() - return b.syncPeer -} - -// isSyncCandidate returns whether or not the peer is a candidate to consider -// syncing from. -func (b *blockManager) isSyncCandidate(sp *serverPeer) bool { - // The peer is not a candidate for sync if it's not a full node. - return sp.Services()&wire.SFNodeNetwork == wire.SFNodeNetwork -} - -// findNextHeaderCheckpoint returns the next checkpoint after the passed height. -// It returns nil when there is not one either because the height is already -// later than the final checkpoint or there are none for the current network. -func (b *blockManager) findNextHeaderCheckpoint(height int32) *chaincfg.Checkpoint { - // There is no next checkpoint if there are none for this current - // network. - checkpoints := b.server.chainParams.Checkpoints - if len(checkpoints) == 0 { - return nil - } - - // There is no next checkpoint if the height is already after the final - // checkpoint. - finalCheckpoint := &checkpoints[len(checkpoints)-1] - if height >= finalCheckpoint.Height { - return nil - } - - // Find the next checkpoint. - nextCheckpoint := finalCheckpoint - for i := len(checkpoints) - 2; i >= 0; i-- { - if height >= checkpoints[i].Height { - break - } - nextCheckpoint = &checkpoints[i] - } - return nextCheckpoint -} - -// findPreviousHeaderCheckpoint returns the last checkpoint before the passed -// height. It returns a checkpoint matching the genesis block when the height -// is earlier than the first checkpoint or there are no checkpoints for the -// current network. This is used for resettng state when a malicious peer sends -// us headers that don't lead up to a known checkpoint. -func (b *blockManager) findPreviousHeaderCheckpoint(height int32) *chaincfg.Checkpoint { - // Start with the genesis block - earliest checkpoint to which our - // code will want to reset - prevCheckpoint := &chaincfg.Checkpoint{ - Height: 0, - Hash: b.server.chainParams.GenesisHash, - } - - // Find the latest checkpoint lower than height or return genesis block - // if there are none. - checkpoints := b.server.chainParams.Checkpoints - for i := 0; i < len(checkpoints); i++ { - if height <= checkpoints[i].Height { - break - } - prevCheckpoint = &checkpoints[i] - } - return prevCheckpoint -} - -// resetHeaderState sets the headers-first mode state to values appropriate for -// syncing from a new peer. -func (b *blockManager) resetHeaderState(newestHeader *wire.BlockHeader, - newestHeight int32) { - b.headerList.Init() - b.startHeader = nil - b.mapMutex.Lock() - b.basicHeaders = make( - map[chainhash.Hash]map[chainhash.Hash][]*serverPeer, - ) - b.extendedHeaders = make( - map[chainhash.Hash]map[chainhash.Hash][]*serverPeer, - ) - b.mapMutex.Unlock() - - // Add an entry for the latest known block into the header pool. - // This allows the next downloaded header to prove it links to the chain - // properly. - node := headerNode{header: newestHeader, height: newestHeight} - b.headerList.PushBack(&node) - b.mapMutex.Lock() - b.basicHeaders[newestHeader.BlockHash()] = make( - map[chainhash.Hash][]*serverPeer, - ) - b.extendedHeaders[newestHeader.BlockHash()] = make( - map[chainhash.Hash][]*serverPeer, - ) - b.mapMutex.Unlock() -} - -// startSync will choose the best peer among the available candidate peers to -// download/sync the blockchain from. When syncing is already running, it -// simply returns. It also examines the candidates for any which are no longer -// candidates and removes them as needed. -func (b *blockManager) startSync(peers *list.List) { - // Return now if we're already syncing. - if b.syncPeer != nil { - return - } - - best, err := b.server.BestSnapshot() - if err != nil { - log.Errorf("Failed to get hash and height for the "+ - "latest block: %s", err) - return - } - var bestPeer *serverPeer - var enext *list.Element - for e := peers.Front(); e != nil; e = enext { - enext = e.Next() - sp := e.Value.(*serverPeer) - - // Remove sync candidate peers that are no longer candidates due - // to passing their latest known block. NOTE: The < is - // intentional as opposed to <=. While techcnically the peer - // doesn't have a later block when it's equal, it will likely - // have one soon so it is a reasonable choice. It also allows - // the case where both are at 0 such as during regression test. - if sp.LastBlock() < best.Height { - peers.Remove(e) - continue - } - - // TODO: Use a better algorithm to choose the best peer. - // For now, just pick the candidate with the highest last block. - if bestPeer == nil || sp.LastBlock() > bestPeer.LastBlock() { - bestPeer = sp - } - } - - // Start syncing from the best peer if one was selected. - if bestPeer != nil { - // Clear the requestedBlocks if the sync peer changes, otherwise - // we may ignore blocks we need that the last sync peer failed - // to send. - b.requestedBlocks = make(map[chainhash.Hash]struct{}) - - locator, err := b.server.LatestBlockLocator() - if err != nil { - log.Errorf("Failed to get block locator for the "+ - "latest block: %s", err) - return - } - - log.Infof("Syncing to block height %d from peer %s", - bestPeer.LastBlock(), bestPeer.Addr()) - - // When the current height is less than a known checkpoint we - // can use block headers to learn about which blocks comprise - // the chain up to the checkpoint and perform less validation - // for them. This is possible since each header contains the - // hash of the previous header and a merkle root. Therefore if - // we validate all of the received headers link together - // properly and the checkpoint hashes match, we can be sure the - // hashes for the blocks in between are accurate. Further, once - // the full blocks are downloaded, the merkle root is computed - // and compared against the value in the header which proves the - // full block hasn't been tampered with. - // - // Once we have passed the final checkpoint, or checkpoints are - // disabled, use standard inv messages learn about the blocks - // and fully validate them. Finally, regression test mode does - // not support the headers-first approach so do normal block - // downloads when in regression test mode. - b.syncPeerMutex.Lock() - b.syncPeer = bestPeer - b.syncPeerMutex.Unlock() - if b.nextCheckpoint != nil && - best.Height < b.nextCheckpoint.Height { - - b.syncPeer.PushGetHeadersMsg(locator, b.nextCheckpoint.Hash) - log.Infof("Downloading headers for blocks %d to "+ - "%d from peer %s", best.Height+1, - b.nextCheckpoint.Height, bestPeer.Addr()) - // This will get adjusted when we process headers if - // we request more headers than the peer is willing to - // give us in one message. - } else { - b.syncPeer.PushGetBlocksMsg(locator, &zeroHash) - } - } else { - log.Warnf("No sync peer candidates available") - } -} - -// current returns true if we believe we are synced with our peers, false if we -// still have blocks to check -func (b *blockManager) current() bool { - // Figure out the latest block we know. - header, height, err := b.server.LatestBlock() - if err != nil { - return false - } - - // There is no last checkpoint if checkpoints are disabled or there are - // none for this current network. - checkpoints := b.server.chainParams.Checkpoints - if len(checkpoints) != 0 { - // We aren't current if the newest block we know of isn't ahead - // of all checkpoints. - if checkpoints[len(checkpoints)-1].Height >= int32(height) { - return false - } - } - - // If we have a syncPeer and are below the block we are syncing to, we - // are not current. - if b.syncPeer != nil && int32(height) < b.syncPeer.LastBlock() { - return false - } - - // If our time source (median times of all the connected peers) is at - // least 24 hours ahead of our best known block, we aren't current. - minus24Hours := b.server.timeSource.AdjustedTime().Add(-24 * time.Hour) - return !header.Timestamp.Before(minus24Hours) -} - -// IsCurrent returns whether or not the block manager believes it is synced with -// the connected peers. -func (b *blockManager) IsCurrent() bool { - reply := make(chan bool) - b.peerChan <- isCurrentMsg{reply: reply} - return <-reply -} - -// QueueInv adds the passed inv message and peer to the block handling queue. -func (b *blockManager) QueueInv(inv *wire.MsgInv, sp *serverPeer) { - // No channel handling here because peers do not need to block on inv - // messages. - if atomic.LoadInt32(&b.shutdown) != 0 { - return - } - - b.peerChan <- &invMsg{inv: inv, peer: sp} -} - -// handleInvMsg handles inv messages from all peers. -// We examine the inventory advertised by the remote peer and act accordingly. -func (b *blockManager) handleInvMsg(imsg *invMsg) { - // Attempt to find the final block in the inventory list. There may - // not be one. - lastBlock := -1 - invVects := imsg.inv.InvList - for i := len(invVects) - 1; i >= 0; i-- { - if invVects[i].Type == wire.InvTypeBlock { - lastBlock = i - break - } - } - - // If this inv contains a block announcement, and this isn't coming from - // our current sync peer or we're current, then update the last - // announced block for this peer. We'll use this information later to - // update the heights of peers based on blocks we've accepted that they - // previously announced. - if lastBlock != -1 && (imsg.peer != b.syncPeer || b.current()) { - imsg.peer.UpdateLastAnnouncedBlock(&invVects[lastBlock].Hash) - } - - // Ignore invs from peers that aren't the sync if we are not current. - // Helps prevent dealing with orphans. - if imsg.peer != b.syncPeer && !b.current() { - return - } - - // If our chain is current and a peer announces a block we already - // know of, then update their current block height. - if lastBlock != -1 && b.current() { - _, blkHeight, err := b.server.GetBlockByHash(invVects[lastBlock].Hash) - if err == nil { - imsg.peer.UpdateLastBlockHeight(int32(blkHeight)) - } - } - - // Add blocks to the cache of known inventory for the peer. - for _, iv := range invVects { - if iv.Type == wire.InvTypeBlock { - imsg.peer.AddKnownInventory(iv) - } - } - - // If this is the sync peer or we're current, get the headers - // for the announced blocks and update the last announced block. - if lastBlock != -1 && (imsg.peer == b.syncPeer || b.current()) { - lastEl := b.headerList.Back() - var lastHash chainhash.Hash - if lastEl != nil { - lastHash = lastEl.Value.(*headerNode).header.BlockHash() - } - // Only send getheaders if we don't already know about the last - // block hash being announced. - if lastHash != invVects[lastBlock].Hash && lastEl != nil && - b.lastRequested != invVects[lastBlock].Hash { - // Make a locator starting from the latest known header - // we've processed. - locator := make(blockchain.BlockLocator, 0, - wire.MaxBlockLocatorsPerMsg) - locator = append(locator, &lastHash) - // Add locator from the database as backup. - knownLocator, err := b.server.LatestBlockLocator() - if err == nil { - locator = append(locator, knownLocator...) - } - // Get headers based on locator. - err = imsg.peer.PushGetHeadersMsg(locator, - &invVects[lastBlock].Hash) - if err != nil { - log.Warnf("Failed to send getheaders message "+ - "to peer %s: %s", imsg.peer.Addr(), err) - return - } - b.lastRequested = invVects[lastBlock].Hash - } - } -} - -// QueueHeaders adds the passed headers message and peer to the block handling -// queue. -func (b *blockManager) QueueHeaders(headers *wire.MsgHeaders, sp *serverPeer) { - // No channel handling here because peers do not need to block on - // headers messages. - if atomic.LoadInt32(&b.shutdown) != 0 { - return - } - - b.peerChan <- &headersMsg{headers: headers, peer: sp} -} - -// handleHeadersMsg handles headers messages from all peers. -func (b *blockManager) handleHeadersMsg(hmsg *headersMsg) { - msg := hmsg.headers - numHeaders := len(msg.Headers) - - // Nothing to do for an empty headers message. - if numHeaders == 0 { - return - } - - // For checking to make sure blocks aren't too far in the - // future as of the time we receive the headers message. - maxTimestamp := b.server.timeSource.AdjustedTime(). - Add(maxTimeOffset) - - // Process all of the received headers ensuring each one connects to the - // previous and that checkpoints match. - receivedCheckpoint := false - var finalHash *chainhash.Hash - var finalHeight int32 - for i, blockHeader := range msg.Headers { - blockHash := blockHeader.BlockHash() - finalHash = &blockHash - - // Ensure there is a previous header to compare against. - prevNodeEl := b.headerList.Back() - if prevNodeEl == nil { - log.Warnf("Header list does not contain a previous" + - "element as expected -- disconnecting peer") - hmsg.peer.Disconnect() - return - } - - // Ensure the header properly connects to the previous one, - // that the proof of work is good, and that the header's - // timestamp isn't too far in the future, and add it to the - // list of headers. - node := headerNode{header: blockHeader} - prevNode := prevNodeEl.Value.(*headerNode) - prevHash := prevNode.header.BlockHash() - if prevHash.IsEqual(&blockHeader.PrevBlock) { - err := b.checkHeaderSanity(blockHeader, maxTimestamp, - false) - if err != nil { - log.Warnf("Header doesn't pass sanity check: "+ - "%s -- disconnecting peer", err) - hmsg.peer.Disconnect() - return - } - node.height = prevNode.height + 1 - finalHeight = node.height - err = b.server.putBlock(*blockHeader, - uint32(node.height)) - if err != nil { - log.Criticalf("Couldn't write block to "+ - "database: %s", err) - // Should we panic here? - } - err = b.server.putMaxBlockHeight(uint32(node.height)) - if err != nil { - log.Criticalf("Couldn't write max block height"+ - " to database: %s", err) - // Should we panic here? - } - hmsg.peer.UpdateLastBlockHeight(node.height) - e := b.headerList.PushBack(&node) - b.mapMutex.Lock() - b.basicHeaders[node.header.BlockHash()] = make( - map[chainhash.Hash][]*serverPeer, - ) - b.extendedHeaders[node.header.BlockHash()] = make( - map[chainhash.Hash][]*serverPeer, - ) - b.mapMutex.Unlock() - if b.startHeader == nil { - b.startHeader = e - } - } else { - // The block doesn't connect to the last block we know. - // We will need to do some additional checks to process - // possible reorganizations or incorrect chain on either - // our or the peer's side. - // If we got these headers from a peer that's not our - // sync peer, they might not be aligned correctly or - // even on the right chain. Just ignore the rest of the - // message. However, if we're current, this might be a - // reorg, in which case we'll either change our sync - // peer or disconnect the peer that sent us these - // bad headers. - if hmsg.peer != b.syncPeer && !b.current() { - return - } - // Check if this is the last block we know of. This is - // a shortcut for sendheaders so that each redundant - // header doesn't cause a disk read. - if blockHash == prevHash { - continue - } - // Check if this block is known. If so, we continue to - // the next one. - _, _, err := b.server.GetBlockByHash(blockHash) - if err == nil { - continue - } - // Check if the previous block is known. If it is, this - // is probably a reorg based on the estimated latest - // block that matches between us and the peer as - // derived from the block locator we sent to request - // these headers. Otherwise, the headers don't connect - // to anything we know and we should disconnect the - // peer. - backHead, backHeight, err := b.server.GetBlockByHash( - blockHeader.PrevBlock) - if err != nil { - log.Warnf("Received block header that does not"+ - " properly connect to the chain from"+ - " peer %s (%s) -- disconnecting", - hmsg.peer.Addr(), err) - hmsg.peer.Disconnect() - return - } - // We've found a branch we weren't aware of. If the - // branch is earlier than the latest synchronized - // checkpoint, it's invalid and we need to disconnect - // the reporting peer. - prevCheckpoint := b.findPreviousHeaderCheckpoint( - prevNode.height) - if backHeight < uint32(prevCheckpoint.Height) { - log.Errorf("Attempt at a reorg earlier than a "+ - "checkpoint past which we've already "+ - "synchronized -- disconnecting peer "+ - "%s", hmsg.peer.Addr()) - hmsg.peer.Disconnect() - return - } - // Check the sanity of the new branch. If any of the - // blocks don't pass sanity checks, disconnect the peer. - // We also keep track of the work represented by these - // headers so we can compare it to the work in the known - // good chain. - b.reorgList.Init() - b.reorgList.PushBack(&headerNode{ - header: &backHead, - height: int32(backHeight), - }) - totalWork := big.NewInt(0) - for j, reorgHeader := range msg.Headers[i:] { - err = b.checkHeaderSanity(reorgHeader, - maxTimestamp, true) - if err != nil { - log.Warnf("Header doesn't pass sanity"+ - " check: %s -- disconnecting "+ - "peer", err) - hmsg.peer.Disconnect() - return - } - totalWork.Add(totalWork, - blockchain.CalcWork(reorgHeader.Bits)) - b.reorgList.PushBack(&headerNode{ - header: reorgHeader, - height: int32(backHeight+1) + int32(j), - }) - } - log.Tracef("Sane reorg attempted. Total work from "+ - "reorg chain: %v", totalWork) - // All the headers pass sanity checks. Now we calculate - // the total work for the known chain. - knownWork := big.NewInt(0) - // This should NEVER be nil because the most recent - // block is always pushed back by resetHeaderState - knownEl := b.headerList.Back() - var knownHead wire.BlockHeader - for j := uint32(prevNode.height); j > backHeight; j-- { - if knownEl != nil { - knownHead = *knownEl.Value.(*headerNode).header - knownEl = knownEl.Prev() - } else { - knownHead, _, err = b.server.GetBlockByHash( - knownHead.PrevBlock) - if err != nil { - log.Criticalf("Can't get block"+ - "header for hash %s: "+ - "%v", - knownHead.PrevBlock, - err) - // Should we panic here? - } - } - knownWork.Add(knownWork, - blockchain.CalcWork(knownHead.Bits)) - } - log.Tracef("Total work from known chain: %v", knownWork) - // Compare the two work totals and reject the new chain - // if it doesn't have more work than the previously - // known chain. Disconnect if it's actually less than - // the known chain. - switch knownWork.Cmp(totalWork) { - case 1: - log.Warnf("Reorg attempt that has less work "+ - "than known chain from peer %s -- "+ - "disconnecting", hmsg.peer.Addr()) - hmsg.peer.Disconnect() - fallthrough - case 0: - return - default: - } - // At this point, we have a valid reorg, so we roll - // back the existing chain and add the new block header. - // We also change the sync peer. Then we can continue - // with the rest of the headers in the message as if - // nothing has happened. - b.syncPeerMutex.Lock() - b.syncPeer = hmsg.peer - b.syncPeerMutex.Unlock() - _, err = b.server.rollBackToHeight(backHeight) - if err != nil { - log.Criticalf("Rollback failed: %s", - err) - // Should we panic here? - } - err = b.server.putBlock(*blockHeader, backHeight+1) - if err != nil { - log.Criticalf("Couldn't write block to "+ - "database: %s", err) - // Should we panic here? - } - err = b.server.putMaxBlockHeight(backHeight + 1) - if err != nil { - log.Criticalf("Couldn't write max block height"+ - " to database: %s", err) - // Should we panic here? - } - b.resetHeaderState(&backHead, int32(backHeight)) - b.headerList.PushBack(&headerNode{ - header: blockHeader, - height: int32(backHeight + 1), - }) - b.mapMutex.Lock() - b.basicHeaders[blockHeader.BlockHash()] = make( - map[chainhash.Hash][]*serverPeer, - ) - b.extendedHeaders[blockHeader.BlockHash()] = make( - map[chainhash.Hash][]*serverPeer, - ) - b.mapMutex.Unlock() - if b.lastBasicCFHeaderHeight > int32(backHeight) { - b.lastBasicCFHeaderHeight = int32(backHeight) - } - if b.lastExtCFHeaderHeight > int32(backHeight) { - b.lastExtCFHeaderHeight = int32(backHeight) - } - } - - // Verify the header at the next checkpoint height matches. - if b.nextCheckpoint != nil && - node.height == b.nextCheckpoint.Height { - nodeHash := node.header.BlockHash() - if nodeHash.IsEqual(b.nextCheckpoint.Hash) { - receivedCheckpoint = true - log.Infof("Verified downloaded block "+ - "header against checkpoint at height "+ - "%d/hash %s", node.height, nodeHash) - } else { - log.Warnf("Block header at height %d/hash "+ - "%s from peer %s does NOT match "+ - "expected checkpoint hash of %s -- "+ - "disconnecting", node.height, - nodeHash, hmsg.peer.Addr(), - b.nextCheckpoint.Hash) - prevCheckpoint := - b.findPreviousHeaderCheckpoint( - node.height) - log.Infof("Rolling back to previous validated "+ - "checkpoint at height %d/hash %s", - prevCheckpoint.Height, - prevCheckpoint.Hash) - _, err := b.server.rollBackToHeight(uint32( - prevCheckpoint.Height)) - if err != nil { - log.Criticalf("Rollback failed: %s", - err) - // Should we panic here? - } - hmsg.peer.Disconnect() - return - } - break - } - } - - // When this header is a checkpoint, switch to fetching the blocks for - // all of the headers since the last checkpoint. - if receivedCheckpoint { - b.nextCheckpoint = b.findNextHeaderCheckpoint(finalHeight) - } - - // Send getcfheaders to each peer based on these headers. - cfhLocator := blockchain.BlockLocator([]*chainhash.Hash{ - &msg.Headers[0].PrevBlock, - }) - cfhStopHash := msg.Headers[len(msg.Headers)-1].BlockHash() - cfhCount := len(msg.Headers) - cfhReqB := cfhRequest{ - extended: false, - stopHash: cfhStopHash, - } - cfhReqE := cfhRequest{ - extended: true, - stopHash: cfhStopHash, - } - b.server.ForAllPeers(func(sp *serverPeer) { - // Should probably use better isolation for this but we're in - // the same package. One of the things to clean up when we do - // more general cleanup. - sp.mtxReqCFH.Lock() - sp.requestedCFHeaders[cfhReqB] = cfhCount - sp.requestedCFHeaders[cfhReqE] = cfhCount - sp.mtxReqCFH.Unlock() - sp.pushGetCFHeadersMsg(cfhLocator, &cfhStopHash, false) - sp.pushGetCFHeadersMsg(cfhLocator, &cfhStopHash, true) - }) - - // If not current, request the next batch of headers starting from the - // latest known header and ending with the next checkpoint. - if !b.current() || b.server.chainParams.Net == - chaincfg.SimNetParams.Net { - locator := blockchain.BlockLocator([]*chainhash.Hash{finalHash}) - nextHash := zeroHash - if b.nextCheckpoint != nil { - nextHash = *b.nextCheckpoint.Hash - } - err := hmsg.peer.PushGetHeadersMsg(locator, &nextHash) - if err != nil { - log.Warnf("Failed to send getheaders message to "+ - "peer %s: %s", hmsg.peer.Addr(), err) - // Unnecessary but we might put other code after this - // eventually. - return - } - } -} - -// QueueCFHeaders adds the passed headers message and peer to the block handling -// queue. -func (b *blockManager) QueueCFHeaders(cfheaders *wire.MsgCFHeaders, - sp *serverPeer) { - // No channel handling here because peers do not need to block on - // cfheaders messages. - if atomic.LoadInt32(&b.shutdown) != 0 { - return - } - - // Ignore messages with 0 headers. - if len(cfheaders.HeaderHashes) == 0 { - return - } - - // Check that the count is correct. This works even when the map lookup - // fails as it returns 0 in that case. - req := cfhRequest{ - extended: cfheaders.Extended, - stopHash: cfheaders.StopHash, - } - // TODO: Get rid of this by refactoring all of this using the query API - sp.mtxReqCFH.Lock() - expLen := sp.requestedCFHeaders[req] - sp.mtxReqCFH.Unlock() - if expLen != len(cfheaders.HeaderHashes) { - log.Warnf("Received cfheaders message doesn't match any "+ - "getcfheaders request. Peer %s is probably on a "+ - "different chain -- ignoring", sp.Addr()) - return - } - // TODO: Remove this by refactoring this section into a query client. - sp.mtxReqCFH.Lock() - delete(sp.requestedCFHeaders, req) - sp.mtxReqCFH.Unlock() - - // Track number of pending cfheaders messsages for both basic and - // extended filters. - pendingMsgs := &b.numBasicCFHeadersMsgs - if cfheaders.Extended { - pendingMsgs = &b.numExtCFHeadersMsgs - } - atomic.AddInt32(pendingMsgs, 1) - b.peerChan <- &cfheadersMsg{cfheaders: cfheaders, peer: sp} -} - -// handleCFHeadersMsg handles cfheaders messages from all peers. -// TODO: Refactor this using query API. -func (b *blockManager) handleCFHeadersMsg(cfhmsg *cfheadersMsg) { - // Grab the matching request we sent, as this message should correspond - // to that, and delete it from the map on return as we're now handling - // it. - headerMap := b.basicHeaders - pendingMsgs := &b.numBasicCFHeadersMsgs - if cfhmsg.cfheaders.Extended { - headerMap = b.extendedHeaders - pendingMsgs = &b.numExtCFHeadersMsgs - } - atomic.AddInt32(pendingMsgs, -1) - headerList := cfhmsg.cfheaders.HeaderHashes - respLen := len(headerList) - // Find the block header matching the last filter header, if any. - el := b.headerList.Back() - for el != nil { - if el.Value.(*headerNode).header.BlockHash() == - cfhmsg.cfheaders.StopHash { - break - } - el = el.Prev() - } - // If nothing matched, there's nothing more to do. - if el == nil { - return - } - // Cycle through the filter header hashes and process them. - var node *headerNode - var hash chainhash.Hash - for i := respLen - 1; i >= 0 && el != nil; i-- { - // If there's no map for this header, the header is either no - // longer valid or has already been processed and committed to - // the database. Either way, break processing. - node = el.Value.(*headerNode) - hash = node.header.BlockHash() - b.mapMutex.Lock() - if _, ok := headerMap[hash]; !ok { - b.mapMutex.Unlock() - log.Tracef("Breaking at %d (%s)", node.height, hash) - break - } - // Process this header and set up the next iteration. - headerMap[hash][*headerList[i]] = append( - headerMap[hash][*headerList[i]], cfhmsg.peer, - ) - b.mapMutex.Unlock() - el = el.Prev() - } - b.intChan <- &processCFHeadersMsg{ - earliestNode: node, - stopHash: cfhmsg.cfheaders.StopHash, - extended: cfhmsg.cfheaders.Extended, - } - log.Tracef("Processed cfheaders starting at %d(%s), ending at %s, from"+ - " peer %s, extended: %t", node.height, node.header.BlockHash(), - cfhmsg.cfheaders.StopHash, cfhmsg.peer.Addr(), - cfhmsg.cfheaders.Extended) -} - -// handleProcessCFHeadersMsg checks to see if we have enough cfheaders to make -// a decision about what the correct headers are, makes that decision if -// possible, and downloads any cfilters and blocks necessary to make that -// decision. -// TODO: Refactor this using query API. -func (b *blockManager) handleProcessCFHeadersMsg(msg *processCFHeadersMsg) { - // Assume we aren't ready to make a decision about correct headers yet. - ready := false - - headerMap := b.basicHeaders - writeFunc := b.server.putBasicHeader - readFunc := b.server.GetBasicHeader - lastCFHeaderHeight := &b.lastBasicCFHeaderHeight - pendingMsgs := &b.numBasicCFHeadersMsgs - if msg.extended { - headerMap = b.extendedHeaders - writeFunc = b.server.putExtHeader - readFunc = b.server.GetExtHeader - lastCFHeaderHeight = &b.lastExtCFHeaderHeight - pendingMsgs = &b.numExtCFHeadersMsgs - } - - stopHash := msg.earliestNode.header.PrevBlock - - // If we have started receiving cfheaders messages for blocks farther - // than the last set we haven't made a decision on, it's time to make - // a decision. - if msg.earliestNode.height > *lastCFHeaderHeight+1 { - ready = true - } - - // If we have fewer processed cfheaders messages for the earliest node - // than the number of connected peers, give the other peers some time to - // catch up before checking if we've processed all of the queued - // cfheaders messages. - numHeaders := 0 - blockMap := headerMap[msg.earliestNode.header.BlockHash()] - for headerHash := range blockMap { - numHeaders += len(blockMap[headerHash]) - } - // Sleep for a bit if we have more peers than cfheaders messages for the - // earliest node for which we're trying to get cfheaders. This lets us - // wait for other peers to send cfheaders messages before making any - // decisions about whether we should write the headers in this message. - connCount := int(b.server.ConnectedCount()) - log.Tracef("Number of peers for which we've processed a cfheaders for "+ - "block %s: %d of %d", msg.earliestNode.header.BlockHash(), - numHeaders, connCount) - if numHeaders <= connCount { - time.Sleep(WaitForMoreCFHeaders) - } - - // If there are no other cfheaders messages left for this type (basic vs - // extended), we should go ahead and make a decision because we have all - // the info we're going to get. - if atomic.LoadInt32(pendingMsgs) == 0 { - ready = true - stopHash = msg.stopHash - } - - // Do nothing if we're not ready to make a decision yet. - if !ready { - return - } - - // At this point, we've got all the cfheaders messages we're going to - // get for the range of headers described by the passed message. We now - // iterate through all of those headers, looking for conflicts. If we - // find a conflict, we have to do additional checks; otherwise, we write - // the filter header to the database. - el := b.headerList.Front() - for el != nil { - node := el.Value.(*headerNode) - header := node.header - hash := header.BlockHash() - if node.height > *lastCFHeaderHeight { - b.mapMutex.Lock() - blockMap := headerMap[hash] - switch len(blockMap) { - // This should only happen if the filter has already - // been written to the database. - case 0: - if _, err := readFunc(hash); err != nil { - // We don't have the filter stored in - // the DB, there's something wrong. - log.Warnf("Somehow we have 0 cfheaders"+ - " for block %d (%s)", - node.height, hash) - b.mapMutex.Unlock() - return - } - // This is the normal case when nobody's trying to - // bamboozle us (or ALL our peers are). - case 1: - // This will only cycle once - for headerHash := range blockMap { - writeFunc(hash, headerHash) - log.Tracef("Wrote header for block %d "+ - "with %d cfheaders messages, "+ - "extended: %t", node.height, - len(blockMap[headerHash]), - msg.extended) - // Notify subscribers of a connected - // block. - // TODO: Rethink this so we're not - // interrupting block processing for - // notifications if the client messes - // up channel handling. - b.server.mtxSubscribers.RLock() - for sub := range b.server.blockSubscribers { - channel := sub.onConnectBasic - if msg.extended { - channel = - sub.onConnectExt - } - if channel != nil { - select { - case channel <- *header: - case <-sub.quit: - } - } - } - b.server.mtxSubscribers.RUnlock() - } - *lastCFHeaderHeight = node.height - // This is when we have conflicting information from - // multiple peers. - // TODO: Handle this case as an adversarial condition. - default: - log.Warnf("Got more than 1 possible filter "+ - "header for block %d (%s)", node.height, - node.header.BlockHash()) - } - b.mapMutex.Unlock() - } - - //elToRemove := el - el = el.Next() - //b.headerList.Remove(elToRemove) - //b.startHeader = el - - // If we've reached the end, we can return - if hash == stopHash { - log.Tracef("Finished processing cfheaders messages up "+ - "to height %d/hash %s, extended: %t", - node.height, hash, msg.extended) - return - } - } -} - -// checkHeaderSanity checks the PoW, and timestamp of a block header. -func (b *blockManager) checkHeaderSanity(blockHeader *wire.BlockHeader, - maxTimestamp time.Time, reorgAttempt bool) error { - diff, err := b.calcNextRequiredDifficulty( - blockHeader.Timestamp, reorgAttempt) - if err != nil { - return err - } - stubBlock := btcutil.NewBlock(&wire.MsgBlock{ - Header: *blockHeader, - }) - err = blockchain.CheckProofOfWork(stubBlock, - blockchain.CompactToBig(diff)) - if err != nil { - return err - } - // Ensure the block time is not too far in the future. - if blockHeader.Timestamp.After(maxTimestamp) { - return fmt.Errorf("block timestamp of %v is too far in the "+ - "future", blockHeader.Timestamp) - } - return nil -} - -// calcNextRequiredDifficulty calculates the required difficulty for the block -// after the passed previous block node based on the difficulty retarget rules. -func (b *blockManager) calcNextRequiredDifficulty(newBlockTime time.Time, - reorgAttempt bool) (uint32, error) { - - hList := b.headerList - if reorgAttempt { - hList = b.reorgList - } - - lastNodeEl := hList.Back() - - // Genesis block. - if lastNodeEl == nil { - return b.server.chainParams.PowLimitBits, nil - } - - lastNode := lastNodeEl.Value.(*headerNode) - - // Return the previous block's difficulty requirements if this block - // is not at a difficulty retarget interval. - if (lastNode.height+1)%b.blocksPerRetarget != 0 { - // For networks that support it, allow special reduction of the - // required difficulty once too much time has elapsed without - // mining a block. - if b.server.chainParams.ReduceMinDifficulty { - // Return minimum difficulty when more than the desired - // amount of time has elapsed without mining a block. - reductionTime := int64( - b.server.chainParams.MinDiffReductionTime / - time.Second) - allowMinTime := lastNode.header.Timestamp.Unix() + - reductionTime - if newBlockTime.Unix() > allowMinTime { - return b.server.chainParams.PowLimitBits, nil - } - - // The block was mined within the desired timeframe, so - // return the difficulty for the last block which did - // not have the special minimum difficulty rule applied. - prevBits, err := b.findPrevTestNetDifficulty(hList) - if err != nil { - return 0, err - } - return prevBits, nil - } - - // For the main network (or any unrecognized networks), simply - // return the previous block's difficulty requirements. - return lastNode.header.Bits, nil - } - - // Get the block node at the previous retarget (targetTimespan days - // worth of blocks). - firstNode, err := b.server.GetBlockByHeight( - uint32(lastNode.height + 1 - b.blocksPerRetarget)) - if err != nil { - return 0, err - } - - // Limit the amount of adjustment that can occur to the previous - // difficulty. - actualTimespan := lastNode.header.Timestamp.Unix() - - firstNode.Timestamp.Unix() - adjustedTimespan := actualTimespan - if actualTimespan < b.minRetargetTimespan { - adjustedTimespan = b.minRetargetTimespan - } else if actualTimespan > b.maxRetargetTimespan { - adjustedTimespan = b.maxRetargetTimespan - } - - // Calculate new target difficulty as: - // currentDifficulty * (adjustedTimespan / targetTimespan) - // The result uses integer division which means it will be slightly - // rounded down. Bitcoind also uses integer division to calculate this - // result. - oldTarget := blockchain.CompactToBig(lastNode.header.Bits) - newTarget := new(big.Int).Mul(oldTarget, big.NewInt(adjustedTimespan)) - targetTimeSpan := int64(b.server.chainParams.TargetTimespan / - time.Second) - newTarget.Div(newTarget, big.NewInt(targetTimeSpan)) - - // Limit new value to the proof of work limit. - if newTarget.Cmp(b.server.chainParams.PowLimit) > 0 { - newTarget.Set(b.server.chainParams.PowLimit) - } - - // Log new target difficulty and return it. The new target logging is - // intentionally converting the bits back to a number instead of using - // newTarget since conversion to the compact representation loses - // precision. - newTargetBits := blockchain.BigToCompact(newTarget) - log.Debugf("Difficulty retarget at block height %d", lastNode.height+1) - log.Debugf("Old target %08x (%064x)", lastNode.header.Bits, oldTarget) - log.Debugf("New target %08x (%064x)", newTargetBits, - blockchain.CompactToBig(newTargetBits)) - log.Debugf("Actual timespan %v, adjusted timespan %v, target timespan %v", - time.Duration(actualTimespan)*time.Second, - time.Duration(adjustedTimespan)*time.Second, - b.server.chainParams.TargetTimespan) - - return newTargetBits, nil -} - -// findPrevTestNetDifficulty returns the difficulty of the previous block which -// did not have the special testnet minimum difficulty rule applied. -func (b *blockManager) findPrevTestNetDifficulty(hList *list.List) (uint32, error) { - startNodeEl := hList.Back() - - // Genesis block. - if startNodeEl == nil { - return b.server.chainParams.PowLimitBits, nil - } - - startNode := startNodeEl.Value.(*headerNode) - - // Search backwards through the chain for the last block without - // the special rule applied. - iterEl := startNodeEl - iterNode := startNode.header - iterHeight := startNode.height - for iterNode != nil && iterHeight%b.blocksPerRetarget != 0 && - iterNode.Bits == b.server.chainParams.PowLimitBits { - - // Get the previous block node. This function is used over - // simply accessing iterNode.parent directly as it will - // dynamically create previous block nodes as needed. This - // helps allow only the pieces of the chain that are needed - // to remain in memory. - iterHeight-- - el := iterEl.Prev() - if el != nil { - iterNode = el.Value.(*headerNode).header - } else { - node, err := b.server.GetBlockByHeight( - uint32(iterHeight)) - if err != nil { - log.Errorf("GetBlockByHeight: %s", err) - return 0, err - } - iterNode = &node - } - } - - // Return the found difficulty or the minimum difficulty if no - // appropriate block was found. - lastBits := b.server.chainParams.PowLimitBits - if iterNode != nil { - lastBits = iterNode.Bits - } - return lastBits, nil -} diff --git a/spvsvc/spvchain/db.go b/spvsvc/spvchain/db.go deleted file mode 100644 index 114ad31..0000000 --- a/spvsvc/spvchain/db.go +++ /dev/null @@ -1,761 +0,0 @@ -// NOTE: THIS API IS UNSTABLE RIGHT NOW. - -package spvchain - -import ( - "bytes" - "encoding/binary" - "fmt" - "time" - - "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcutil/gcs" - "github.com/btcsuite/btcutil/gcs/builder" - "github.com/btcsuite/btcwallet/waddrmgr" - "github.com/btcsuite/btcwallet/walletdb" -) - -const ( - // LatestDBVersion is the most recent database version. - LatestDBVersion = 1 -) - -var ( - // latestDBVersion is the most recent database version as a variable so - // the tests can change it to force errors. - latestDBVersion uint32 = LatestDBVersion -) - -// Key names for various database fields. -var ( - // Bucket names. - spvBucketName = []byte("spvchain") - blockHeaderBucketName = []byte("bh") - basicHeaderBucketName = []byte("bfh") - basicFilterBucketName = []byte("bf") - extHeaderBucketName = []byte("efh") - extFilterBucketName = []byte("ef") - - // Db related key names (main bucket). - dbVersionName = []byte("dbver") - dbCreateDateName = []byte("dbcreated") - maxBlockHeightName = []byte("maxblockheight") -) - -// uint32ToBytes converts a 32 bit unsigned integer into a 4-byte slice in -// little-endian order: 1 -> [1 0 0 0]. -func uint32ToBytes(number uint32) []byte { - buf := make([]byte, 4) - binary.LittleEndian.PutUint32(buf, number) - return buf -} - -// uint64ToBytes converts a 64 bit unsigned integer into a 8-byte slice in -// little-endian order: 1 -> [1 0 0 0 0 0 0 0]. -func uint64ToBytes(number uint64) []byte { - buf := make([]byte, 8) - binary.LittleEndian.PutUint64(buf, number) - return buf -} - -// dbUpdateOption is a function type for the kind of DB update to be done. -// These can call each other and dbViewOption functions; however, they cannot -// be called by dbViewOption functions. -type dbUpdateOption func(bucket walletdb.ReadWriteBucket) error - -// dbViewOption is a funciton type for the kind of data to be fetched from DB. -// These can call each other and can be called by dbUpdateOption functions; -// however, they cannot call dbUpdateOption functions. -type dbViewOption func(bucket walletdb.ReadBucket) error - -// fetchDBVersion fetches the current manager version from the database. -func (s *ChainService) fetchDBVersion() (uint32, error) { - var version uint32 - err := s.dbView(fetchDBVersion(&version)) - return version, err -} - -func fetchDBVersion(version *uint32) dbViewOption { - return func(bucket walletdb.ReadBucket) error { - verBytes := bucket.Get(dbVersionName) - if verBytes == nil { - return fmt.Errorf("required version number not " + - "stored in database") - } - *version = binary.LittleEndian.Uint32(verBytes) - return nil - } -} - -// putDBVersion stores the provided version to the database. -func (s *ChainService) putDBVersion(version uint32) error { - return s.dbUpdate(putDBVersion(version)) -} - -func putDBVersion(version uint32) dbUpdateOption { - return func(bucket walletdb.ReadWriteBucket) error { - verBytes := uint32ToBytes(version) - return bucket.Put(dbVersionName, verBytes) - } -} - -// putMaxBlockHeight stores the max block height to the database. -func (s *ChainService) putMaxBlockHeight(maxBlockHeight uint32) error { - return s.dbUpdate(putMaxBlockHeight(maxBlockHeight)) -} - -func putMaxBlockHeight(maxBlockHeight uint32) dbUpdateOption { - return func(bucket walletdb.ReadWriteBucket) error { - maxBlockHeightBytes := uint32ToBytes(maxBlockHeight) - err := bucket.Put(maxBlockHeightName, maxBlockHeightBytes) - if err != nil { - return fmt.Errorf("failed to store max block height: %s", err) - } - return nil - } -} - -// putBlock stores the provided block header and height, keyed to the block -// hash, in the database. -func (s *ChainService) putBlock(header wire.BlockHeader, height uint32) error { - return s.dbUpdate(putBlock(header, height)) -} - -func putBlock(header wire.BlockHeader, height uint32) dbUpdateOption { - return func(bucket walletdb.ReadWriteBucket) error { - var buf bytes.Buffer - err := header.Serialize(&buf) - if err != nil { - return err - } - _, err = buf.Write(uint32ToBytes(height)) - if err != nil { - return err - } - blockHash := header.BlockHash() - bhBucket := bucket.NestedReadWriteBucket(blockHeaderBucketName) - err = bhBucket.Put(blockHash[:], buf.Bytes()) - if err != nil { - return fmt.Errorf("failed to store SPV block info: %s", - err) - } - err = bhBucket.Put(uint32ToBytes(height), blockHash[:]) - if err != nil { - return fmt.Errorf("failed to store block height info:"+ - " %s", err) - } - return nil - } -} - -// putFilter stores the provided filter, keyed to the block hash, in the -// appropriate filter bucket in the database. -func (s *ChainService) putFilter(blockHash chainhash.Hash, bucketName []byte, - filter *gcs.Filter) error { - return s.dbUpdate(putFilter(blockHash, bucketName, filter)) -} - -func putFilter(blockHash chainhash.Hash, bucketName []byte, - filter *gcs.Filter) dbUpdateOption { - return func(bucket walletdb.ReadWriteBucket) error { - var buf bytes.Buffer - _, err := buf.Write(filter.NBytes()) - if err != nil { - return err - } - filterBucket := bucket.NestedReadWriteBucket(bucketName) - err = filterBucket.Put(blockHash[:], buf.Bytes()) - if err != nil { - return fmt.Errorf("failed to store filter: %s", err) - } - return nil - } -} - -// putBasicFilter stores the provided filter, keyed to the block hash, in the -// basic filter bucket in the database. -func (s *ChainService) putBasicFilter(blockHash chainhash.Hash, - filter *gcs.Filter) error { - return s.dbUpdate(putBasicFilter(blockHash, filter)) -} - -func putBasicFilter(blockHash chainhash.Hash, - filter *gcs.Filter) dbUpdateOption { - return putFilter(blockHash, basicFilterBucketName, filter) -} - -// putExtFilter stores the provided filter, keyed to the block hash, in the -// extended filter bucket in the database. -func (s *ChainService) putExtFilter(blockHash chainhash.Hash, - filter *gcs.Filter) error { - return s.dbUpdate(putExtFilter(blockHash, filter)) -} - -func putExtFilter(blockHash chainhash.Hash, - filter *gcs.Filter) dbUpdateOption { - return putFilter(blockHash, extFilterBucketName, filter) -} - -// putHeader stores the provided header, keyed to the block hash, in the -// appropriate filter header bucket in the database. -func (s *ChainService) putHeader(blockHash chainhash.Hash, bucketName []byte, - filterTip chainhash.Hash) error { - return s.dbUpdate(putHeader(blockHash, bucketName, filterTip)) -} - -func putHeader(blockHash chainhash.Hash, bucketName []byte, - filterTip chainhash.Hash) dbUpdateOption { - return func(bucket walletdb.ReadWriteBucket) error { - headerBucket := bucket.NestedReadWriteBucket(bucketName) - err := headerBucket.Put(blockHash[:], filterTip[:]) - if err != nil { - return fmt.Errorf("failed to store filter header: %s", err) - } - return nil - } -} - -// putBasicHeader stores the provided header, keyed to the block hash, in the -// basic filter header bucket in the database. -func (s *ChainService) putBasicHeader(blockHash chainhash.Hash, - filterTip chainhash.Hash) error { - return s.dbUpdate(putBasicHeader(blockHash, filterTip)) -} - -func putBasicHeader(blockHash chainhash.Hash, - filterTip chainhash.Hash) dbUpdateOption { - return putHeader(blockHash, basicHeaderBucketName, filterTip) -} - -// putExtHeader stores the provided header, keyed to the block hash, in the -// extended filter header bucket in the database. -func (s *ChainService) putExtHeader(blockHash chainhash.Hash, - filterTip chainhash.Hash) error { - return s.dbUpdate(putExtHeader(blockHash, filterTip)) -} - -func putExtHeader(blockHash chainhash.Hash, - filterTip chainhash.Hash) dbUpdateOption { - return putHeader(blockHash, extHeaderBucketName, filterTip) -} - -// getFilter retreives the filter, keyed to the provided block hash, from the -// appropriate filter bucket in the database. -func (s *ChainService) getFilter(blockHash chainhash.Hash, - bucketName []byte) (*gcs.Filter, error) { - var filter gcs.Filter - err := s.dbView(getFilter(blockHash, bucketName, &filter)) - return &filter, err -} - -func getFilter(blockHash chainhash.Hash, bucketName []byte, - filter *gcs.Filter) dbViewOption { - return func(bucket walletdb.ReadBucket) error { - filterBucket := bucket.NestedReadBucket(bucketName) - filterBytes := filterBucket.Get(blockHash[:]) - if len(filterBytes) == 0 { - return fmt.Errorf("failed to get filter") - } - calcFilter, err := gcs.FromNBytes(builder.DefaultP, filterBytes) - if calcFilter != nil { - *filter = *calcFilter - } - return err - } -} - -// GetBasicFilter retrieves the filter, keyed to the provided block hash, from -// the basic filter bucket in the database. -func (s *ChainService) GetBasicFilter(blockHash chainhash.Hash) (*gcs.Filter, - error) { - var filter gcs.Filter - err := s.dbView(getBasicFilter(blockHash, &filter)) - return &filter, err -} - -func getBasicFilter(blockHash chainhash.Hash, filter *gcs.Filter) dbViewOption { - return getFilter(blockHash, basicFilterBucketName, filter) -} - -// GetExtFilter retrieves the filter, keyed to the provided block hash, from -// the extended filter bucket in the database. -func (s *ChainService) GetExtFilter(blockHash chainhash.Hash) (*gcs.Filter, - error) { - var filter gcs.Filter - err := s.dbView(getExtFilter(blockHash, &filter)) - return &filter, err -} - -func getExtFilter(blockHash chainhash.Hash, filter *gcs.Filter) dbViewOption { - return getFilter(blockHash, extFilterBucketName, filter) -} - -// getHeader retrieves the header, keyed to the provided block hash, from the -// appropriate filter header bucket in the database. -func (s *ChainService) getHeader(blockHash chainhash.Hash, - bucketName []byte) (*chainhash.Hash, error) { - var filterTip chainhash.Hash - err := s.dbView(getHeader(blockHash, bucketName, &filterTip)) - return &filterTip, err -} - -func getHeader(blockHash chainhash.Hash, bucketName []byte, - filterTip *chainhash.Hash) dbViewOption { - return func(bucket walletdb.ReadBucket) error { - headerBucket := bucket.NestedReadBucket(bucketName) - headerBytes := headerBucket.Get(blockHash[:]) - if len(filterTip) == 0 { - return fmt.Errorf("failed to get filter header") - } - calcFilterTip, err := chainhash.NewHash(headerBytes) - if calcFilterTip != nil { - *filterTip = *calcFilterTip - } - return err - } -} - -// GetBasicHeader retrieves the header, keyed to the provided block hash, from -// the basic filter header bucket in the database. -func (s *ChainService) GetBasicHeader(blockHash chainhash.Hash) ( - *chainhash.Hash, error) { - var filterTip chainhash.Hash - err := s.dbView(getBasicHeader(blockHash, &filterTip)) - return &filterTip, err -} - -func getBasicHeader(blockHash chainhash.Hash, - filterTip *chainhash.Hash) dbViewOption { - return getHeader(blockHash, basicHeaderBucketName, filterTip) -} - -// GetExtHeader retrieves the header, keyed to the provided block hash, from the -// extended filter header bucket in the database. -func (s *ChainService) GetExtHeader(blockHash chainhash.Hash) (*chainhash.Hash, - error) { - var filterTip chainhash.Hash - err := s.dbView(getExtHeader(blockHash, &filterTip)) - return &filterTip, err -} - -func getExtHeader(blockHash chainhash.Hash, - filterTip *chainhash.Hash) dbViewOption { - return getHeader(blockHash, extHeaderBucketName, filterTip) -} - -// rollBackLastBlock rolls back the last known block and returns the BlockStamp -// representing the new last known block. -func (s *ChainService) rollBackLastBlock() (*waddrmgr.BlockStamp, error) { - var bs waddrmgr.BlockStamp - err := s.dbUpdate(rollBackLastBlock(&bs)) - return &bs, err -} - -func rollBackLastBlock(bs *waddrmgr.BlockStamp) dbUpdateOption { - return func(bucket walletdb.ReadWriteBucket) error { - headerBucket := bucket.NestedReadWriteBucket( - blockHeaderBucketName) - var sync waddrmgr.BlockStamp - err := syncedTo(&sync)(bucket) - if err != nil { - return err - } - err = headerBucket.Delete(sync.Hash[:]) - if err != nil { - return err - } - err = headerBucket.Delete(uint32ToBytes(uint32(sync.Height))) - if err != nil { - return err - } - err = putMaxBlockHeight(uint32(sync.Height - 1))(bucket) - if err != nil { - return err - } - sync = waddrmgr.BlockStamp{} - err = syncedTo(&sync)(bucket) - if sync != (waddrmgr.BlockStamp{}) { - *bs = sync - } - return err - } -} - -// GetBlockByHash retrieves the block header, filter, and filter tip, based on -// the provided block hash, from the database. -func (s *ChainService) GetBlockByHash(blockHash chainhash.Hash) ( - wire.BlockHeader, uint32, error) { - var header wire.BlockHeader - var height uint32 - err := s.dbView(getBlockByHash(blockHash, &header, &height)) - return header, height, err -} - -func getBlockByHash(blockHash chainhash.Hash, header *wire.BlockHeader, - height *uint32) dbViewOption { - return func(bucket walletdb.ReadBucket) error { - headerBucket := bucket.NestedReadBucket(blockHeaderBucketName) - blockBytes := headerBucket.Get(blockHash[:]) - if len(blockBytes) < wire.MaxBlockHeaderPayload+4 { - return fmt.Errorf("failed to retrieve block info for"+ - " hash %s: want %d bytes, got %d.", blockHash, - wire.MaxBlockHeaderPayload+4, len(blockBytes)) - } - buf := bytes.NewReader(blockBytes[:wire.MaxBlockHeaderPayload]) - err := header.Deserialize(buf) - if err != nil { - return fmt.Errorf("failed to deserialize block header "+ - "for hash: %s", blockHash) - } - *height = binary.LittleEndian.Uint32( - blockBytes[wire.MaxBlockHeaderPayload : wire.MaxBlockHeaderPayload+4]) - return nil - } -} - -// GetBlockHashByHeight retrieves the hash of a block by its height. -func (s *ChainService) GetBlockHashByHeight(height uint32) (chainhash.Hash, - error) { - var blockHash chainhash.Hash - err := s.dbView(getBlockHashByHeight(height, &blockHash)) - return blockHash, err -} - -func getBlockHashByHeight(height uint32, - blockHash *chainhash.Hash) dbViewOption { - return func(bucket walletdb.ReadBucket) error { - headerBucket := bucket.NestedReadBucket(blockHeaderBucketName) - hashBytes := headerBucket.Get(uint32ToBytes(height)) - if hashBytes == nil { - return fmt.Errorf("no block hash for height %d", height) - } - blockHash.SetBytes(hashBytes) - return nil - } -} - -// GetBlockByHeight retrieves a block's information by its height. -func (s *ChainService) GetBlockByHeight(height uint32) (wire.BlockHeader, - error) { - var header wire.BlockHeader - err := s.dbView(getBlockByHeight(height, &header)) - return header, err -} - -func getBlockByHeight(height uint32, header *wire.BlockHeader) dbViewOption { - return func(bucket walletdb.ReadBucket) error { - var blockHash chainhash.Hash - err := getBlockHashByHeight(height, &blockHash)(bucket) - if err != nil { - return err - } - var gotHeight uint32 - err = getBlockByHash(blockHash, header, &gotHeight)(bucket) - if err != nil { - return err - } - if gotHeight != height { - return fmt.Errorf("Got height %d for block at "+ - "requested height %d", gotHeight, height) - } - return nil - } -} - -// BestSnapshot is a synonym for SyncedTo -func (s *ChainService) BestSnapshot() (*waddrmgr.BlockStamp, error) { - return s.SyncedTo() -} - -// SyncedTo retrieves the most recent block's height and hash. -func (s *ChainService) SyncedTo() (*waddrmgr.BlockStamp, error) { - var bs waddrmgr.BlockStamp - err := s.dbView(syncedTo(&bs)) - return &bs, err -} - -func syncedTo(bs *waddrmgr.BlockStamp) dbViewOption { - return func(bucket walletdb.ReadBucket) error { - var header wire.BlockHeader - var height uint32 - err := latestBlock(&header, &height)(bucket) - if err != nil { - return err - } - bs.Hash = header.BlockHash() - bs.Height = int32(height) - return nil - } -} - -// LatestBlock retrieves latest stored block's header and height. -func (s *ChainService) LatestBlock() (wire.BlockHeader, uint32, error) { - var bh wire.BlockHeader - var h uint32 - err := s.dbView(latestBlock(&bh, &h)) - return bh, h, err -} - -func latestBlock(header *wire.BlockHeader, height *uint32) dbViewOption { - return func(bucket walletdb.ReadBucket) error { - maxBlockHeightBytes := bucket.Get(maxBlockHeightName) - if maxBlockHeightBytes == nil { - return fmt.Errorf("no max block height stored") - } - *height = binary.LittleEndian.Uint32(maxBlockHeightBytes) - return getBlockByHeight(*height, header)(bucket) - } -} - -// BlockLocatorFromHash returns a block locator based on the provided hash. -func (s *ChainService) BlockLocatorFromHash(hash chainhash.Hash) ( - blockchain.BlockLocator, error) { - var locator blockchain.BlockLocator - err := s.dbView(blockLocatorFromHash(hash, &locator)) - return locator, err -} - -func blockLocatorFromHash(hash chainhash.Hash, - locator *blockchain.BlockLocator) dbViewOption { - return func(bucket walletdb.ReadBucket) error { - // Append the initial hash - *locator = append(*locator, &hash) - // If hash isn't found in DB or this is the genesis block, return - // the locator as is - var header wire.BlockHeader - var height uint32 - err := getBlockByHash(hash, &header, &height)(bucket) - if (err != nil) || (height == 0) { - return nil - } - - decrement := uint32(1) - for (height > 0) && (len(*locator) < wire.MaxBlockLocatorsPerMsg) { - // Decrement by 1 for the first 10 blocks, then double the - // jump until we get to the genesis hash - if len(*locator) > 10 { - decrement *= 2 - } - if decrement > height { - height = 0 - } else { - height -= decrement - } - var blockHash chainhash.Hash - err := getBlockHashByHeight(height, &blockHash)(bucket) - if err != nil { - return nil - } - *locator = append(*locator, &blockHash) - } - return nil - } -} - -// LatestBlockLocator returns the block locator for the latest known block -// stored in the database. -func (s *ChainService) LatestBlockLocator() (blockchain.BlockLocator, error) { - var locator blockchain.BlockLocator - err := s.dbView(latestBlockLocator(&locator)) - return locator, err -} - -func latestBlockLocator(locator *blockchain.BlockLocator) dbViewOption { - return func(bucket walletdb.ReadBucket) error { - var best waddrmgr.BlockStamp - err := syncedTo(&best)(bucket) - if err != nil { - return err - } - return blockLocatorFromHash(best.Hash, locator)(bucket) - } -} - -// CheckConnectivity cycles through all of the block headers, from last to -// first, and makes sure they all connect to each other. -func (s *ChainService) CheckConnectivity() error { - return s.dbView(checkConnectivity()) -} - -func checkConnectivity() dbViewOption { - return func(bucket walletdb.ReadBucket) error { - var header wire.BlockHeader - var height uint32 - err := latestBlock(&header, &height)(bucket) - if err != nil { - return fmt.Errorf("Couldn't retrieve latest block: %s", - err) - } - for height > 0 { - var newHeader wire.BlockHeader - var newHeight uint32 - err := getBlockByHash(header.PrevBlock, &newHeader, - &newHeight)(bucket) - if err != nil { - return fmt.Errorf("Couldn't retrieve block %s:"+ - " %s", header.PrevBlock, err) - } - if newHeader.BlockHash() != header.PrevBlock { - return fmt.Errorf("Block %s doesn't match "+ - "block %s's PrevBlock (%s)", - newHeader.BlockHash(), - header.BlockHash(), header.PrevBlock) - } - if newHeight != height-1 { - return fmt.Errorf("Block %s doesn't have "+ - "correct height: want %d, got %d", - newHeader.BlockHash(), height-1, - newHeight) - } - header = newHeader - height = newHeight - } - return nil - } -} - -// createSPVNS creates the initial namespace structure needed for all of the -// SPV-related data. This includes things such as all of the buckets as well as -// the version and creation date. -func (s *ChainService) createSPVNS() error { - tx, err := s.db.BeginReadWriteTx() - if err != nil { - return err - } - - spvBucket, err := tx.CreateTopLevelBucket(spvBucketName) - if err != nil { - return fmt.Errorf("failed to create main bucket: %s", err) - } - - _, err = spvBucket.CreateBucketIfNotExists(blockHeaderBucketName) - if err != nil { - return fmt.Errorf("failed to create block header bucket: %s", - err) - } - - _, err = spvBucket.CreateBucketIfNotExists(basicFilterBucketName) - if err != nil { - return fmt.Errorf("failed to create basic filter "+ - "bucket: %s", err) - } - - _, err = spvBucket.CreateBucketIfNotExists(basicHeaderBucketName) - if err != nil { - return fmt.Errorf("failed to create basic header "+ - "bucket: %s", err) - } - - _, err = spvBucket.CreateBucketIfNotExists(extFilterBucketName) - if err != nil { - return fmt.Errorf("failed to create extended filter "+ - "bucket: %s", err) - } - - _, err = spvBucket.CreateBucketIfNotExists(extHeaderBucketName) - if err != nil { - return fmt.Errorf("failed to create extended header "+ - "bucket: %s", err) - } - - createDate := spvBucket.Get(dbCreateDateName) - if createDate != nil { - log.Info("Wallet SPV namespace already created.") - return nil - } - - log.Info("Creating wallet SPV namespace.") - - basicFilter, err := builder.BuildBasicFilter( - s.chainParams.GenesisBlock) - if err != nil { - return err - } - - basicFilterTip := builder.MakeHeaderForFilter(basicFilter, - s.chainParams.GenesisBlock.Header.PrevBlock) - - extFilter, err := builder.BuildExtFilter( - s.chainParams.GenesisBlock) - if err != nil { - return err - } - - extFilterTip := builder.MakeHeaderForFilter(extFilter, - s.chainParams.GenesisBlock.Header.PrevBlock) - - err = putBlock(s.chainParams.GenesisBlock.Header, 0)(spvBucket) - if err != nil { - return err - } - - err = putBasicFilter(*s.chainParams.GenesisHash, basicFilter)(spvBucket) - if err != nil { - return err - } - - err = putBasicHeader(*s.chainParams.GenesisHash, basicFilterTip)( - spvBucket) - if err != nil { - return err - } - - err = putExtFilter(*s.chainParams.GenesisHash, extFilter)(spvBucket) - if err != nil { - return err - } - - err = putExtHeader(*s.chainParams.GenesisHash, extFilterTip)(spvBucket) - if err != nil { - return err - } - - err = putDBVersion(latestDBVersion)(spvBucket) - if err != nil { - return err - } - - err = putMaxBlockHeight(0)(spvBucket) - if err != nil { - return err - } - - err = spvBucket.Put(dbCreateDateName, - uint64ToBytes(uint64(time.Now().Unix()))) - if err != nil { - return fmt.Errorf("failed to store database creation "+ - "time: %s", err) - } - - return tx.Commit() -} - -// dbUpdate allows the passed function to update the ChainService DB bucket. -func (s *ChainService) dbUpdate(updateFunc dbUpdateOption) error { - tx, err := s.db.BeginReadWriteTx() - if err != nil { - tx.Rollback() - return err - } - bucket := tx.ReadWriteBucket(spvBucketName) - err = updateFunc(bucket) - if err != nil { - tx.Rollback() - return err - } - return tx.Commit() -} - -// dbView allows the passed function to read the ChainService DB bucket. -func (s *ChainService) dbView(viewFunc dbViewOption) error { - tx, err := s.db.BeginReadTx() - defer tx.Rollback() - if err != nil { - return err - } - bucket := tx.ReadBucket(spvBucketName) - return viewFunc(bucket) - -} diff --git a/spvsvc/spvchain/driver.go b/spvsvc/spvchain/driver.go deleted file mode 100644 index b5f9d67..0000000 --- a/spvsvc/spvchain/driver.go +++ /dev/null @@ -1,70 +0,0 @@ -package spvchain - -import ( - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcwallet/waddrmgr" -) - -// SPVChain is an implementation of the btcwalet chain.Interface interface. -type SPVChain struct { - cs *ChainService -} - -// NewSPVChain creates a new SPVChain struct with a backing ChainService -func NewSPVChain(chainService *ChainService) *SPVChain { - return &SPVChain{ - cs: chainService, - } -} - -// Start replicates the RPC client's Start method. -func (s *SPVChain) Start() error { - s.cs.Start() - return nil -} - -// Stop replicates the RPC client's Stop method. -func (s *SPVChain) Stop() { - s.cs.Stop() -} - -// WaitForShutdown replicates the RPC client's WaitForShutdown method. -func (s *SPVChain) WaitForShutdown() { - s.cs.Stop() -} - -// SendRawTransaction replicates the RPC client's SendRawTransaction command. -func (s *SPVChain) SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) ( - *chainhash.Hash, error) { - err := s.cs.SendTransaction(tx) - if err != nil { - return nil, err - } - hash := tx.TxHash() - return &hash, nil -} - -// GetBlock replicates the RPC client's GetBlock command. -func (s *SPVChain) GetBlock(hash *chainhash.Hash) (*wire.MsgBlock, error) { - block, err := s.cs.GetBlockFromNetwork(*hash) - if err != nil { - return nil, err - } - return block.MsgBlock(), nil -} - -// GetBestBlock replicates the RPC client's GetBestBlock command. -func (s *SPVChain) GetBestBlock() (*chainhash.Hash, int32, error) { - header, height, err := s.cs.LatestBlock() - if err != nil { - return nil, 0, err - } - hash := header.BlockHash() - return &hash, int32(height), nil -} - -// BlockStamp replicates the RPC client's BlockStamp command. -func (s *SPVChain) BlockStamp() (*waddrmgr.BlockStamp, error) { - return s.cs.SyncedTo() -} diff --git a/spvsvc/spvchain/log.go b/spvsvc/spvchain/log.go deleted file mode 100644 index 48f1e9a..0000000 --- a/spvsvc/spvchain/log.go +++ /dev/null @@ -1,26 +0,0 @@ -package spvchain - -import "github.com/btcsuite/btclog" - -// log is a logger that is initialized with no output filters. This -// means the package will not perform any logging by default until the caller -// requests it. -var log btclog.Logger - -// The default amount of logging is none. -func init() { - DisableLog() -} - -// DisableLog disables all library log output. Logging output is disabled -// by default until either UseLogger or SetLogWriter are called. -func DisableLog() { - log = btclog.Disabled -} - -// UseLogger uses a specified Logger to output package logging info. -// This should be used in preference to SetLogWriter if the caller is also -// using btclog. -func UseLogger(logger btclog.Logger) { - log = logger -} diff --git a/spvsvc/spvchain/notifications.go b/spvsvc/spvchain/notifications.go deleted file mode 100644 index 7c6af69..0000000 --- a/spvsvc/spvchain/notifications.go +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright (c) 2013-2016 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -// NOTE: THIS API IS UNSTABLE RIGHT NOW. - -package spvchain - -import ( - "errors" - - "github.com/btcsuite/btcd/addrmgr" - "github.com/btcsuite/btcd/connmgr" -) - -type getConnCountMsg struct { - reply chan int32 -} - -type getPeersMsg struct { - reply chan []*serverPeer -} - -type getOutboundGroup struct { - key string - reply chan int -} - -type getAddedNodesMsg struct { - reply chan []*serverPeer -} - -type disconnectNodeMsg struct { - cmp func(*serverPeer) bool - reply chan error -} - -type connectNodeMsg struct { - addr string - permanent bool - reply chan error -} - -type removeNodeMsg struct { - cmp func(*serverPeer) bool - reply chan error -} - -type forAllPeersMsg struct { - closure func(*serverPeer) -} - -// TODO: General - abstract out more of blockmanager into queries. It'll make -// this way more maintainable and usable. - -// handleQuery is the central handler for all queries and commands from other -// goroutines related to peer state. -func (s *ChainService) handleQuery(state *peerState, querymsg interface{}) { - switch msg := querymsg.(type) { - case getConnCountMsg: - nconnected := int32(0) - state.forAllPeers(func(sp *serverPeer) { - if sp.Connected() { - nconnected++ - } - }) - msg.reply <- nconnected - - case getPeersMsg: - peers := make([]*serverPeer, 0, state.Count()) - state.forAllPeers(func(sp *serverPeer) { - if !sp.Connected() { - return - } - peers = append(peers, sp) - }) - msg.reply <- peers - - case connectNodeMsg: - // TODO: duplicate oneshots? - // Limit max number of total peers. - if state.Count() >= MaxPeers { - msg.reply <- errors.New("max peers reached") - return - } - for _, peer := range state.persistentPeers { - if peer.Addr() == msg.addr { - if msg.permanent { - msg.reply <- errors.New("peer already connected") - } else { - msg.reply <- errors.New("peer exists as a permanent peer") - } - return - } - } - - netAddr, err := addrStringToNetAddr(msg.addr) - if err != nil { - msg.reply <- err - return - } - - // TODO: if too many, nuke a non-perm peer. - go s.connManager.Connect(&connmgr.ConnReq{ - Addr: netAddr, - Permanent: msg.permanent, - }) - msg.reply <- nil - case removeNodeMsg: - found := disconnectPeer(state.persistentPeers, msg.cmp, func(sp *serverPeer) { - // Keep group counts ok since we remove from - // the list now. - state.outboundGroups[addrmgr.GroupKey(sp.NA())]-- - }) - - if found { - msg.reply <- nil - } else { - msg.reply <- errors.New("peer not found") - } - case getOutboundGroup: - count, ok := state.outboundGroups[msg.key] - if ok { - msg.reply <- count - } else { - msg.reply <- 0 - } - // Request a list of the persistent (added) peers. - case getAddedNodesMsg: - // Respond with a slice of the relavent peers. - peers := make([]*serverPeer, 0, len(state.persistentPeers)) - for _, sp := range state.persistentPeers { - peers = append(peers, sp) - } - msg.reply <- peers - case disconnectNodeMsg: - // Check outbound peers. - found := disconnectPeer(state.outboundPeers, msg.cmp, func(sp *serverPeer) { - // Keep group counts ok since we remove from - // the list now. - state.outboundGroups[addrmgr.GroupKey(sp.NA())]-- - }) - if found { - // If there are multiple outbound connections to the same - // ip:port, continue disconnecting them all until no such - // peers are found. - for found { - found = disconnectPeer(state.outboundPeers, msg.cmp, func(sp *serverPeer) { - state.outboundGroups[addrmgr.GroupKey(sp.NA())]-- - }) - } - msg.reply <- nil - return - } - - msg.reply <- errors.New("peer not found") - case forAllPeersMsg: - // TODO: Remove this when it's unnecessary due to wider use of - // queryPeers. - // Run the closure on all peers in the passed state. - state.forAllPeers(msg.closure) - // Even though this is a query, there's no reply channel as the - // forAllPeers method doesn't return anything. An error might be - // useful in the future. - } -} - -// ConnectedCount returns the number of currently connected peers. -func (s *ChainService) ConnectedCount() int32 { - replyChan := make(chan int32) - - s.query <- getConnCountMsg{reply: replyChan} - - return <-replyChan -} - -// OutboundGroupCount returns the number of peers connected to the given -// outbound group key. -func (s *ChainService) OutboundGroupCount(key string) int { - replyChan := make(chan int) - s.query <- getOutboundGroup{key: key, reply: replyChan} - return <-replyChan -} - -// AddedNodeInfo returns an array of btcjson.GetAddedNodeInfoResult structures -// describing the persistent (added) nodes. -func (s *ChainService) AddedNodeInfo() []*serverPeer { - replyChan := make(chan []*serverPeer) - s.query <- getAddedNodesMsg{reply: replyChan} - return <-replyChan -} - -// Peers returns an array of all connected peers. -func (s *ChainService) Peers() []*serverPeer { - replyChan := make(chan []*serverPeer) - - s.query <- getPeersMsg{reply: replyChan} - - return <-replyChan -} - -// DisconnectNodeByAddr disconnects a peer by target address. Both outbound and -// inbound nodes will be searched for the target node. An error message will -// be returned if the peer was not found. -func (s *ChainService) DisconnectNodeByAddr(addr string) error { - replyChan := make(chan error) - - s.query <- disconnectNodeMsg{ - cmp: func(sp *serverPeer) bool { return sp.Addr() == addr }, - reply: replyChan, - } - - return <-replyChan -} - -// DisconnectNodeByID disconnects a peer by target node id. Both outbound and -// inbound nodes will be searched for the target node. An error message will be -// returned if the peer was not found. -func (s *ChainService) DisconnectNodeByID(id int32) error { - replyChan := make(chan error) - - s.query <- disconnectNodeMsg{ - cmp: func(sp *serverPeer) bool { return sp.ID() == id }, - reply: replyChan, - } - - return <-replyChan -} - -// RemoveNodeByAddr removes a peer from the list of persistent peers if -// present. An error will be returned if the peer was not found. -func (s *ChainService) RemoveNodeByAddr(addr string) error { - replyChan := make(chan error) - - s.query <- removeNodeMsg{ - cmp: func(sp *serverPeer) bool { return sp.Addr() == addr }, - reply: replyChan, - } - - return <-replyChan -} - -// RemoveNodeByID removes a peer by node ID from the list of persistent peers -// if present. An error will be returned if the peer was not found. -func (s *ChainService) RemoveNodeByID(id int32) error { - replyChan := make(chan error) - - s.query <- removeNodeMsg{ - cmp: func(sp *serverPeer) bool { return sp.ID() == id }, - reply: replyChan, - } - - return <-replyChan -} - -// ConnectNode adds `addr' as a new outbound peer. If permanent is true then the -// peer will be persistent and reconnect if the connection is lost. -// It is an error to call this with an already existing peer. -func (s *ChainService) ConnectNode(addr string, permanent bool) error { - replyChan := make(chan error) - - s.query <- connectNodeMsg{addr: addr, permanent: permanent, reply: replyChan} - - return <-replyChan -} - -// ForAllPeers runs a closure over all peers (outbound and persistent) to which -// the ChainService is connected. Nothing is returned because the peerState's -// ForAllPeers method doesn't return anything as the closure passed to it -// doesn't return anything. -func (s *ChainService) ForAllPeers(closure func(sp *serverPeer)) { - s.query <- forAllPeersMsg{ - closure: closure, - } -} diff --git a/spvsvc/spvchain/query.go b/spvsvc/spvchain/query.go deleted file mode 100644 index cb32fba..0000000 --- a/spvsvc/spvchain/query.go +++ /dev/null @@ -1,494 +0,0 @@ -// NOTE: THIS API IS UNSTABLE RIGHT NOW. - -package spvchain - -import ( - "fmt" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcutil/gcs" - "github.com/btcsuite/btcutil/gcs/builder" -) - -var ( - // QueryTimeout specifies how long to wait for a peer to answer a query. - QueryTimeout = time.Second * 3 - - // QueryNumRetries specifies how many times to retry sending a query to - // each peer before we've concluded we aren't going to get a valid - // response. This allows to make up for missed messages in some - // instances. - QueryNumRetries = 2 -) - -// Query options can be modified per-query, unlike global options. -// TODO: Make more query options that override global options. -type queryOptions struct { - // timeout lets the query know how long to wait for a peer to - // answer the query before moving onto the next peer. - timeout time.Duration - - // numRetries tells the query how many times to retry asking each peer - // the query. - numRetries uint8 - - // doneChan lets the query signal the caller when it's done, in case - // it's run in a goroutine. - doneChan chan<- struct{} -} - -// QueryOption is a functional option argument to any of the network query -// methods, such as GetBlockFromNetwork and GetCFilter (when that resorts to a -// network query). These are always processed in order, with later options -// overriding earlier ones. -type QueryOption func(*queryOptions) - -// defaultQueryOptions returns a queryOptions set to package-level defaults. -func defaultQueryOptions() *queryOptions { - return &queryOptions{ - timeout: QueryTimeout, - numRetries: uint8(QueryNumRetries), - } -} - -// Timeout is a query option that lets the query know how long to wait for -// each peer we ask the query to answer it before moving on. -func Timeout(timeout time.Duration) QueryOption { - return func(qo *queryOptions) { - qo.timeout = timeout - } -} - -// NumRetries is a query option that lets the query know the maximum number of -// times each peer should be queried. The default is one. -func NumRetries(numRetries uint8) QueryOption { - return func(qo *queryOptions) { - qo.numRetries = numRetries - } -} - -// DoneChan allows the caller to pass a channel that will get closed when the -// query is finished. -func DoneChan(doneChan chan<- struct{}) QueryOption { - return func(qo *queryOptions) { - qo.doneChan = doneChan - } -} - -type spMsg struct { - sp *serverPeer - msg wire.Message -} - -type spMsgSubscription struct { - msgChan chan<- spMsg - quitChan <-chan struct{} - wg *sync.WaitGroup -} - -// queryPeers is a helper function that sends a query to one or more peers and -// waits for an answer. The timeout for queries is set by the QueryTimeout -// package-level variable. -func (s *ChainService) queryPeers( - // queryMsg is the message to send to each peer selected by selectPeer. - queryMsg wire.Message, - // checkResponse is caled for every message within the timeout period. - // The quit channel lets the query know to terminate because the - // required response has been found. This is done by closing the - // channel. - checkResponse func(sp *serverPeer, resp wire.Message, - quit chan<- struct{}), - // options takes functional options for executing the query. - options ...QueryOption, -) { - qo := defaultQueryOptions() - for _, option := range options { - option(qo) - } - - // This is done in a single-threaded query because the peerState is held - // in a single thread. This is the only part of the query framework that - // requires access to peerState, so it's done once per query. - peers := s.Peers() - syncPeer := s.blockManager.SyncPeer() - - // This will be shared state between the per-peer goroutines. - quit := make(chan struct{}) - allQuit := make(chan struct{}) - startQuery := make(chan struct{}) - var wg sync.WaitGroup - var syncPeerTries uint32 - // Increase this number to be able to handle more queries at once as - // each channel gets results for all queries, otherwise messages can - // get mixed and there's a vicious cycle of retries causing a bigger - // message flood, more of which get missed. - msgChan := make(chan spMsg) - var subwg sync.WaitGroup - subscription := spMsgSubscription{ - msgChan: msgChan, - quitChan: allQuit, - wg: &subwg, - } - - // Start a goroutine for each peer that potentially queries that peer. - for _, sp := range peers { - wg.Add(1) - go func(sp *serverPeer) { - numRetries := qo.numRetries - defer wg.Done() - defer sp.unsubscribeRecvMsgs(subscription) - // Should we do this when the goroutine gets a message - // via startQuery rather than at the launch of the - // goroutine? - if !sp.Connected() { - return - } - timeout := make(<-chan time.Time) - queryLoop: - for { - select { - case <-timeout: - // After timeout, we try to notify - // another of our peer goroutines to - // do a query until we get a signal to - // quit. - select { - case startQuery <- struct{}{}: - case <-quit: - return - case <-allQuit: - return - } - // At this point, we've sent startQuery. - // We return if we've run through this - // section of code numRetries times. - if numRetries--; numRetries == 0 { - return - } - case <-quit: - // After we're told to quit, we return. - return - case <-allQuit: - // After we're told to quit, we return. - return - case <-startQuery: - // We're the lucky peer whose turn it is - // to try to answer the current query. - // TODO: Add support for querying *all* - // peers simultaneously to avoid timeout - // delays. - // If the sync peer hasn't tried yet and - // we aren't the sync peer, don't do - // anything but forward the message down - // the startQuery channel until the - // sync peer gets a shot. - if sp == syncPeer { - atomic.StoreUint32( - &syncPeerTries, 1) - } - if atomic.LoadUint32(&syncPeerTries) == - 0 { - select { - case startQuery <- struct{}{}: - case <-quit: - return - case <-allQuit: - return - } - continue queryLoop - } - sp.subscribeRecvMsg(subscription) - // Don't want the peer hanging on send - // to the channel if we quit before - // reading the channel. - sentChan := make(chan struct{}, 1) - sp.QueueMessageWithEncoding(queryMsg, - sentChan, wire.WitnessEncoding) - select { - case <-sentChan: - case <-quit: - return - case <-allQuit: - return - } - timeout = time.After(qo.timeout) - default: - } - } - }(sp) - } - startQuery <- struct{}{} - - // This goroutine will wait until all of the peer-query goroutines have - // terminated, and then initiate a query shutdown. - go func() { - wg.Wait() - // If we timed out on each goroutine and didn't quit or time out - // on the main goroutine, make sure our main goroutine knows to - // quit. - select { - case <-allQuit: - default: - close(allQuit) - } - // Close the done channel, if any - if qo.doneChan != nil { - close(qo.doneChan) - } - // Wait until all goroutines started by subscriptions have - // exited after we closed allQuit before letting the message - // channel get garbage collected. - subwg.Wait() - }() - - // Loop for any messages sent to us via our subscription channel and - // check them for whether they satisfy the query. Break the loop if it's - // time to quit. - timeout := time.After(time.Duration(len(peers)+1) * - qo.timeout * time.Duration(qo.numRetries)) -checkResponses: - for { - select { - case <-timeout: - // When we time out, close the allQuit channel - // if it hasn't already been closed. - select { - case <-allQuit: - default: - close(allQuit) - } - break checkResponses - case <-quit: - break checkResponses - case <-allQuit: - break checkResponses - case sm := <-msgChan: - // TODO: This will get stuck if checkResponse - // gets stuck. This is a caveat for callers that - // should be fixed before exposing this function - // for public use. - checkResponse(sm.sp, sm.msg, quit) - } - } -} - -// GetCFilter gets a cfilter from the database. Failing that, it requests the -// cfilter from the network and writes it to the database. -func (s *ChainService) GetCFilter(blockHash chainhash.Hash, - extended bool, options ...QueryOption) (*gcs.Filter, error) { - getFilter := s.GetBasicFilter - getHeader := s.GetBasicHeader - putFilter := s.putBasicFilter - if extended { - getFilter = s.GetExtFilter - getHeader = s.GetExtHeader - putFilter = s.putExtFilter - } - filter, err := getFilter(blockHash) - if err == nil && filter != nil { - return filter, nil - } - // We didn't get the filter from the DB, so we'll set it to nil and try - // to get it from the network. - filter = nil - block, _, err := s.GetBlockByHash(blockHash) - if err != nil { - return nil, err - } - if block.BlockHash() != blockHash { - return nil, fmt.Errorf("Couldn't get header for block %s "+ - "from database", blockHash) - } - curHeader, err := getHeader(blockHash) - if err != nil { - return nil, fmt.Errorf("Couldn't get cfheader for block %s "+ - "from database", blockHash) - } - prevHeader, err := getHeader(block.PrevBlock) - if err != nil { - return nil, fmt.Errorf("Couldn't get cfheader for block %s "+ - "from database", blockHash) - } - // If we're expecting a zero filter, just return a nil filter and don't - // bother trying to get it from the network. The caller will know - // there's no error because we're also returning a nil error. - if builder.MakeHeaderForFilter(nil, *prevHeader) == *curHeader { - return nil, nil - } - s.queryPeers( - // Send a wire.GetCFilterMsg - wire.NewMsgGetCFilter(&blockHash, extended), - // Check responses and if we get one that matches, - // end the query early. - func(sp *serverPeer, resp wire.Message, - quit chan<- struct{}) { - switch response := resp.(type) { - // We're only interested in "cfilter" messages. - case *wire.MsgCFilter: - // Only keep this going if we haven't already - // found a filter, or we risk closing an already - // closed channel. - if filter != nil { - return - } - if len(response.Data) < 4 { - // Filter data is too short. - // Ignore this message. - return - } - if blockHash != response.BlockHash { - // The response doesn't match our - // request. Ignore this message. - return - } - gotFilter, err := gcs.FromNBytes( - builder.DefaultP, response.Data) - if err != nil { - // Malformed filter data. We - // can ignore this message. - return - } - if builder.MakeHeaderForFilter(gotFilter, - *prevHeader) != *curHeader { - // Filter data doesn't match - // the headers we know about. - // Ignore this response. - return - } - // At this point, the filter matches - // what we know about it and we declare - // it sane. We can kill the query and - // pass the response back to the caller. - close(quit) - filter = gotFilter - default: - } - }, - options..., - ) - // If we've found a filter, write it to the database for next time. - if filter != nil { - putFilter(blockHash, filter) - log.Tracef("Wrote filter for block %s, extended: %t", - blockHash, extended) - } - return filter, nil -} - -// GetBlockFromNetwork gets a block by requesting it from the network, one peer -// at a time, until one answers. -func (s *ChainService) GetBlockFromNetwork( - blockHash chainhash.Hash, options ...QueryOption) (*btcutil.Block, - error) { - blockHeader, height, err := s.GetBlockByHash(blockHash) - if err != nil || blockHeader.BlockHash() != blockHash { - return nil, fmt.Errorf("Couldn't get header for block %s "+ - "from database", blockHash) - } - getData := wire.NewMsgGetData() - getData.AddInvVect(wire.NewInvVect(wire.InvTypeWitnessBlock, - &blockHash)) - // The block is only updated from the checkResponse function argument, - // which is always called single-threadedly. We don't check the block - // until after the query is finished, so we can just write to it - // naively. - var foundBlock *btcutil.Block - s.queryPeers( - // Send a wire.GetCFilterMsg - getData, - // Check responses and if we get one that matches, - // end the query early. - func(sp *serverPeer, resp wire.Message, - quit chan<- struct{}) { - switch response := resp.(type) { - // We're only interested in "block" messages. - case *wire.MsgBlock: - // Only keep this going if we haven't already - // found a block, or we risk closing an already - // closed channel. - if foundBlock != nil { - return - } - // If this isn't our block, ignore it. - if response.BlockHash() != blockHash { - return - } - block := btcutil.NewBlock(response) - // Only set height if btcutil hasn't - // automagically put one in. - if block.Height() == - btcutil.BlockHeightUnknown { - block.SetHeight(int32(height)) - } - // If this claims our block but doesn't - // pass the sanity check, the peer is - // trying to bamboozle us. Disconnect - // it. - if err := blockchain.CheckBlockSanity( - block, - // We don't need to check PoW - // because by the time we get - // here, it's been checked - // during header synchronization - s.chainParams.PowLimit, - s.timeSource, - ); err != nil { - log.Warnf("Invalid block for %s "+ - "received from %s -- "+ - "disconnecting peer", blockHash, - sp.Addr()) - sp.Disconnect() - return - } - // At this point, the block matches what we know - // about it and we declare it sane. We can kill - // the query and pass the response back to the - // caller. - close(quit) - foundBlock = block - default: - } - }, - options..., - ) - if foundBlock == nil { - return nil, fmt.Errorf("Couldn't retrieve block %s from "+ - "network", blockHash) - } - return foundBlock, nil -} - -// SendTransaction sends a transaction to each peer. It returns an error if any -// peer rejects the transaction for any reason than that it's already known. -// TODO: Better privacy by sending to only one random peer and watching -// propagation, requires better peer selection support in query API. -func (s *ChainService) SendTransaction(tx *wire.MsgTx, - options ...QueryOption) error { - var err error - s.queryPeers( - tx, - func(sp *serverPeer, resp wire.Message, quit chan<- struct{}) { - switch response := resp.(type) { - case *wire.MsgReject: - if response.Hash == tx.TxHash() && - !strings.Contains(response.Reason, - "already have transaction") { - err = log.Errorf("Transaction %s "+ - "rejected by %s: %s", - tx.TxHash(), sp.Addr(), - response.Reason) - close(quit) - } - } - }, - options..., - ) - return err -} diff --git a/spvsvc/spvchain/rescan.go b/spvsvc/spvchain/rescan.go deleted file mode 100644 index 5b6cb03..0000000 --- a/spvsvc/spvchain/rescan.go +++ /dev/null @@ -1,777 +0,0 @@ -// NOTE: THIS API IS UNSTABLE RIGHT NOW. - -package spvchain - -import ( - "bytes" - "fmt" - "sync/atomic" - - "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcrpcclient" - "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcutil/gcs" - "github.com/btcsuite/btcutil/gcs/builder" - "github.com/btcsuite/btcwallet/waddrmgr" -) - -// Relevant package-level variables live here -var () - -// Functional parameters for Rescan -type rescanOptions struct { - chain *ChainService - queryOptions []QueryOption - ntfn btcrpcclient.NotificationHandlers - startBlock *waddrmgr.BlockStamp - endBlock *waddrmgr.BlockStamp - watchAddrs []btcutil.Address - watchOutPoints []wire.OutPoint - watchTxIDs []chainhash.Hash - watchList [][]byte - txIdx uint32 - update <-chan *updateOptions - quit <-chan struct{} -} - -// RescanOption is a functional option argument to any of the rescan and -// notification subscription methods. These are always processed in order, with -// later options overriding earlier ones. -type RescanOption func(ro *rescanOptions) - -func defaultRescanOptions() *rescanOptions { - return &rescanOptions{} -} - -// QueryOptions pass onto the underlying queries. -func QueryOptions(options ...QueryOption) RescanOption { - return func(ro *rescanOptions) { - ro.queryOptions = options - } -} - -// NotificationHandlers specifies notification handlers for the rescan. These -// will always run in the same goroutine as the caller. -func NotificationHandlers(ntfn btcrpcclient.NotificationHandlers) RescanOption { - return func(ro *rescanOptions) { - ro.ntfn = ntfn - } -} - -// StartBlock specifies the start block. The hash is checked first; if there's -// no such hash (zero hash avoids lookup), the height is checked next. If -// the height is 0 or the start block isn't specified, starts from the genesis -// block. This block is assumed to already be known, and no notifications will -// be sent for this block. -func StartBlock(startBlock *waddrmgr.BlockStamp) RescanOption { - return func(ro *rescanOptions) { - ro.startBlock = startBlock - } -} - -// EndBlock specifies the end block. The hash is checked first; if there's no -// such hash (zero hash avoids lookup), the height is checked next. If the -// height is 0 or in the future or the end block isn't specified, the quit -// channel MUST be specified as Rescan will sync to the tip of the blockchain -// and continue to stay in sync and pass notifications. This is enforced at -// runtime. -func EndBlock(endBlock *waddrmgr.BlockStamp) RescanOption { - return func(ro *rescanOptions) { - ro.endBlock = endBlock - } -} - -// WatchAddrs specifies the addresses to watch/filter for. Each call to this -// function adds to the list of addresses being watched rather than replacing -// the list. Each time a transaction spends to the specified address, the -// outpoint is added to the WatchOutPoints list. -func WatchAddrs(watchAddrs ...btcutil.Address) RescanOption { - return func(ro *rescanOptions) { - ro.watchAddrs = append(ro.watchAddrs, watchAddrs...) - } -} - -// WatchOutPoints specifies the outpoints to watch for on-chain spends. Each -// call to this function adds to the list of outpoints being watched rather -// than replacing the list. -func WatchOutPoints(watchOutPoints ...wire.OutPoint) RescanOption { - return func(ro *rescanOptions) { - ro.watchOutPoints = append(ro.watchOutPoints, watchOutPoints...) - } -} - -// WatchTxIDs specifies the outpoints to watch for on-chain spends. Each -// call to this function adds to the list of outpoints being watched rather -// than replacing the list. -func WatchTxIDs(watchTxIDs ...chainhash.Hash) RescanOption { - return func(ro *rescanOptions) { - ro.watchTxIDs = append(ro.watchTxIDs, watchTxIDs...) - } -} - -// TxIdx specifies a hint transaction index into the block in which the UTXO -// is created (eg, coinbase is 0, next transaction is 1, etc.) -func TxIdx(txIdx uint32) RescanOption { - return func(ro *rescanOptions) { - ro.txIdx = txIdx - } -} - -// QuitChan specifies the quit channel. This can be used by the caller to let -// an indefinite rescan (one with no EndBlock set) know it should gracefully -// shut down. If this isn't specified, an end block MUST be specified as Rescan -// must know when to stop. This is enforced at runtime. -func QuitChan(quit <-chan struct{}) RescanOption { - return func(ro *rescanOptions) { - ro.quit = quit - } -} - -// updateChan specifies an update channel. This is for internal use by the -// Rescan.Update functionality. -func updateChan(update <-chan *updateOptions) RescanOption { - return func(ro *rescanOptions) { - ro.update = update - } -} - -// Rescan is a single-threaded function that uses headers from the database and -// functional options as arguments. -func (s *ChainService) Rescan(options ...RescanOption) error { - ro := defaultRescanOptions() - ro.endBlock = &waddrmgr.BlockStamp{ - Hash: *s.chainParams.GenesisHash, - Height: 0, - } - for _, option := range options { - option(ro) - } - ro.chain = s - - // If we have something to watch, create a watch list. - for _, addr := range ro.watchAddrs { - ro.watchList = append(ro.watchList, addr.ScriptAddress()) - } - for _, op := range ro.watchOutPoints { - ro.watchList = append(ro.watchList, - builder.OutPointToFilterEntry(op)) - } - for _, txid := range ro.watchTxIDs { - ro.watchList = append(ro.watchList, txid[:]) - } - if len(ro.watchList) == 0 { - return fmt.Errorf("Rescan must specify addresses and/or " + - "outpoints and/or TXIDs to watch") - } - - // Check that we have either an end block or a quit channel. - if ro.endBlock != nil { - if (ro.endBlock.Hash != chainhash.Hash{}) { - _, height, err := s.GetBlockByHash(ro.endBlock.Hash) - if err != nil { - ro.endBlock.Hash = chainhash.Hash{} - } else { - ro.endBlock.Height = int32(height) - } - } - if (ro.endBlock.Hash == chainhash.Hash{}) { - if ro.endBlock.Height != 0 { - header, err := s.GetBlockByHeight( - uint32(ro.endBlock.Height)) - if err == nil { - ro.endBlock.Hash = header.BlockHash() - } else { - ro.endBlock = &waddrmgr.BlockStamp{} - } - } - } - } else { - ro.endBlock = &waddrmgr.BlockStamp{} - } - if ro.quit == nil && ro.endBlock.Height == 0 { - return fmt.Errorf("Rescan request must specify a quit channel" + - " or valid end block") - } - - // Track our position in the chain. - var curHeader wire.BlockHeader - curStamp := *ro.startBlock - - // Find our starting block. - if (curStamp.Hash != chainhash.Hash{}) { - header, height, err := s.GetBlockByHash(curStamp.Hash) - if err == nil { - curHeader = header - curStamp.Height = int32(height) - } else { - curStamp.Hash = chainhash.Hash{} - } - } - if (curStamp.Hash == chainhash.Hash{}) { - if curStamp.Height == 0 { - curStamp.Hash = *s.chainParams.GenesisHash - } else { - header, err := s.GetBlockByHeight( - uint32(curStamp.Height)) - if err == nil { - curHeader = header - curStamp.Hash = curHeader.BlockHash() - } else { - curHeader = - s.chainParams.GenesisBlock.Header - curStamp.Hash = - *s.chainParams.GenesisHash - curStamp.Height = 0 - } - } - } - log.Tracef("Starting rescan from known block %d (%s)", curStamp.Height, - curStamp.Hash) - - // Listen for notifications. - blockConnected := make(chan wire.BlockHeader) - blockDisconnected := make(chan wire.BlockHeader) - subscription := blockSubscription{ - onConnectBasic: blockConnected, - onDisconnect: blockDisconnected, - quit: ro.quit, - } - - // Loop through blocks, one at a time. This relies on the underlying - // ChainService API to send blockConnected and blockDisconnected - // notifications in the correct order. - current := false -rescanLoop: - for { - // If we're current, we wait for notifications. - if current { - // Wait for a signal that we have a newly connected - // header and cfheader, or a newly disconnected header; - // alternatively, forward ourselves to the next block - // if possible. - select { - case <-ro.quit: - s.unsubscribeBlockMsgs(subscription) - return nil - case update := <-ro.update: - rewound, err := ro.updateFilter(update, - &curStamp, &curHeader) - if err != nil { - return err - } - if rewound { - current = false - } - case header := <-blockConnected: - // Only deal with the next block from what we - // know about. Otherwise, it's in the future. - if header.PrevBlock != curStamp.Hash { - continue rescanLoop - } - curHeader = header - curStamp.Hash = header.BlockHash() - curStamp.Height++ - case header := <-blockDisconnected: - // Only deal with it if it's the current block - // we know about. Otherwise, it's in the future. - if header.BlockHash() == curStamp.Hash { - // Run through notifications. This is - // all single-threaded. We include - // deprecated calls as they're still - // used, for now. - if ro.ntfn. - OnFilteredBlockDisconnected != - nil { - ro.ntfn.OnFilteredBlockDisconnected( - curStamp.Height, - &curHeader) - } - if ro.ntfn.OnBlockDisconnected != nil { - ro.ntfn.OnBlockDisconnected( - &curStamp.Hash, - curStamp.Height, - curHeader.Timestamp) - } - header, _, err := s.GetBlockByHash( - header.PrevBlock) - if err != nil { - return err - } - curHeader = header - curStamp.Hash = header.BlockHash() - curStamp.Height-- - } - continue rescanLoop - } - } else { - // Since we're not current, we try to manually advance - // the block. If we fail, we mark outselves as current - // and follow notifications. - header, err := s.GetBlockByHeight(uint32( - curStamp.Height + 1)) - if err != nil { - log.Tracef("Rescan became current at %d (%s), "+ - "subscribing to block notifications", - curStamp.Height, curStamp.Hash) - current = true - // Subscribe to block notifications. - s.subscribeBlockMsg(subscription) - continue rescanLoop - } - curHeader = header - curStamp.Height++ - curStamp.Hash = header.BlockHash() - } - - // At this point, we've found the block header that's next in - // our rescan. First, if we're sending out BlockConnected - // notifications, do that. - if ro.ntfn.OnBlockConnected != nil { - ro.ntfn.OnBlockConnected(&curStamp.Hash, - curStamp.Height, curHeader.Timestamp) - } - // Now we need to see if it matches the rescan's filters, so we - // get the basic filter from the DB or network. - var block *btcutil.Block - var relevantTxs []*btcutil.Tx - var bFilter, eFilter *gcs.Filter - var err error - key := builder.DeriveKey(&curStamp.Hash) - matched := false - bFilter, err = s.GetCFilter(curStamp.Hash, false) - if err != nil { - return err - } - if bFilter != nil && bFilter.N() != 0 { - // We see if any relevant transactions match. - matched, err = bFilter.MatchAny(key, ro.watchList) - if err != nil { - return err - } - } - if len(ro.watchTxIDs) > 0 { - eFilter, err = s.GetCFilter(curStamp.Hash, true) - if err != nil { - return err - } - } - if eFilter != nil && eFilter.N() != 0 { - // We see if any relevant transactions match. - matched, err = eFilter.MatchAny(key, ro.watchList) - if err != nil { - return err - } - } - // If we have no transactions, we just send an - // OnFilteredBlockConnected notification with no relevant - // transactions. - if matched { - // We've matched. Now we actually get the block - // and cycle through the transactions to see - // which ones are relevant. - block, err = s.GetBlockFromNetwork( - curStamp.Hash, ro.queryOptions...) - if err != nil { - return err - } - if block == nil { - return fmt.Errorf("Couldn't get block %d "+ - "(%s) from network", curStamp.Height, - curStamp.Hash) - } - relevantTxs, err = ro.notifyBlock(block) - if err != nil { - return err - } - } - if ro.ntfn.OnFilteredBlockConnected != nil { - ro.ntfn.OnFilteredBlockConnected(curStamp.Height, - &curHeader, relevantTxs) - } - if curStamp.Hash == ro.endBlock.Hash || curStamp.Height == - ro.endBlock.Height { - return nil - } - select { - case update := <-ro.update: - rewound, err := ro.updateFilter(update, &curStamp, - &curHeader) - if err != nil { - return err - } - if rewound { - current = false - } - default: - } - } -} - -// updateFilter atomically updates the filter and rewinds to the specified -// height if not 0. -func (ro *rescanOptions) updateFilter(update *updateOptions, - curStamp *waddrmgr.BlockStamp, curHeader *wire.BlockHeader) (bool, - error) { - ro.watchAddrs = append(ro.watchAddrs, - update.addrs...) - ro.watchOutPoints = append(ro.watchOutPoints, - update.outPoints...) - ro.watchTxIDs = append(ro.watchTxIDs, - update.txIDs...) - for _, addr := range update.addrs { - ro.watchList = append(ro.watchList, addr.ScriptAddress()) - } - for _, op := range update.outPoints { - ro.watchList = append(ro.watchList, - builder.OutPointToFilterEntry(op)) - } - for _, txid := range update.txIDs { - ro.watchList = append(ro.watchList, txid[:]) - } - // Rewind if requested - if update.rewind == 0 { - return false, nil - } - var header wire.BlockHeader - var height uint32 - var rewound bool - var err error - for curStamp.Height > int32(update.rewind) { - if ro.ntfn.OnBlockDisconnected != nil { - ro.ntfn.OnBlockDisconnected(&curStamp.Hash, - curStamp.Height, curHeader.Timestamp) - } - if ro.ntfn.OnFilteredBlockDisconnected != nil { - ro.ntfn.OnFilteredBlockDisconnected(curStamp.Height, - curHeader) - } - // Don't rewind past the last block we need to disconnect, - // because otherwise we connect the last known good block - // without ever disconnecting it. - if curStamp.Height == int32(update.rewind+1) { - break - } - // Rewind and continue. - header, height, err = - ro.chain.GetBlockByHash(curHeader.PrevBlock) - if err != nil { - return rewound, err - } - *curHeader = header - curStamp.Height = int32(height) - curStamp.Hash = curHeader.BlockHash() - rewound = true - } - return rewound, nil -} - -// notifyBlock notifies listeners based on the block filter. It writes back to -// the outPoints argument the updated list of outpoints to monitor based on -// matched addresses. -func (ro *rescanOptions) notifyBlock(block *btcutil.Block) ([]*btcutil.Tx, - error) { - var relevantTxs []*btcutil.Tx - blockHeader := block.MsgBlock().Header - details := btcjson.BlockDetails{ - Height: block.Height(), - Hash: block.Hash().String(), - Time: blockHeader.Timestamp.Unix(), - } - for txIdx, tx := range block.Transactions() { - relevant := false - txDetails := details - txDetails.Index = txIdx - for _, hash := range ro.watchTxIDs { - if hash == *(tx.Hash()) { - relevant = true - break - } - } - for _, in := range tx.MsgTx().TxIn { - if relevant { - break - } - for _, op := range ro.watchOutPoints { - if in.PreviousOutPoint == op { - relevant = true - if ro.ntfn.OnRedeemingTx != nil { - ro.ntfn.OnRedeemingTx(tx, - &txDetails) - } - break - } - } - } - for outIdx, out := range tx.MsgTx().TxOut { - pushedData, err := txscript.PushedData(out.PkScript) - if err != nil { - continue - } - for _, addr := range ro.watchAddrs { - if relevant { - break - } - for _, data := range pushedData { - if bytes.Equal(data, - addr.ScriptAddress()) { - relevant = true - hash := tx.Hash() - outPoint := wire.OutPoint{ - Hash: *hash, - Index: uint32(outIdx), - } - ro.watchOutPoints = append( - ro.watchOutPoints, - outPoint) - ro.watchList = append( - ro.watchList, - builder.OutPointToFilterEntry( - outPoint)) - if ro.ntfn.OnRecvTx != nil { - ro.ntfn.OnRecvTx(tx, - &txDetails) - } - } - } - } - } - if relevant { - relevantTxs = append(relevantTxs, tx) - } - } - return relevantTxs, nil -} - -// Rescan is an object that represents a long-running rescan/notification -// client with updateable filters. It's meant to be close to a drop-in -// replacement for the btcd rescan and notification functionality used in -// wallets. It only contains information about whether a goroutine is running. -type Rescan struct { - running uint32 - updateChan chan<- *updateOptions -} - -// NewRescan returns a rescan object that runs in another goroutine and has an -// updateable filter. It returns the long-running rescan object, and a channel -// which returns any error on termination of the rescan process. -func (s *ChainService) NewRescan(options ...RescanOption) (Rescan, - <-chan error) { - updChan := make(chan *updateOptions) - errChan := make(chan error) - rescan := Rescan{ - running: 1, - updateChan: updChan, - } - go func() { - err := s.Rescan(append(options, updateChan(updChan))...) - atomic.StoreUint32(&rescan.running, 0) - errChan <- err - }() - return rescan, errChan -} - -// Functional parameters for Update. -type updateOptions struct { - addrs []btcutil.Address - outPoints []wire.OutPoint - txIDs []chainhash.Hash - rewind uint32 -} - -// UpdateOption is a functional option argument for the Rescan.Update method. -type UpdateOption func(uo *updateOptions) - -func defaultUpdateOptions() *updateOptions { - return &updateOptions{} -} - -// AddAddrs adds addresses to the filter. -func AddAddrs(addrs ...btcutil.Address) UpdateOption { - return func(uo *updateOptions) { - uo.addrs = append(uo.addrs, addrs...) - } -} - -// AddOutPoints adds outpoints to the filter. -func AddOutPoints(outPoints ...wire.OutPoint) UpdateOption { - return func(uo *updateOptions) { - uo.outPoints = append(uo.outPoints, outPoints...) - } -} - -// AddTxIDs adds TxIDs to the filter. -func AddTxIDs(txIDs ...chainhash.Hash) UpdateOption { - return func(uo *updateOptions) { - uo.txIDs = append(uo.txIDs, txIDs...) - } -} - -// Rewind rewinds the rescan to the specified height (meaning, disconnects down -// to the block immediately after the specified height) and restarts it from -// that point with the (possibly) newly expanded filter. Especially useful when -// called in the same Update() as one of the previous three options. -func Rewind(height uint32) UpdateOption { - return func(uo *updateOptions) { - uo.rewind = height - } -} - -// Update sends an update to a long-running rescan/notification goroutine. -func (r *Rescan) Update(options ...UpdateOption) error { - running := atomic.LoadUint32(&r.running) - if running != 1 { - return fmt.Errorf("Rescan is already done and cannot be " + - "updated.") - } - uo := defaultUpdateOptions() - for _, option := range options { - option(uo) - } - r.updateChan <- uo - return nil -} - -// GetUtxo gets the appropriate TxOut or errors if it's spent. The option -// WatchOutPoints (with a single outpoint) is required. StartBlock can be used -// to give a hint about which block the transaction is in, and TxIdx can be used -// to give a hint of which transaction in the block matches it (coinbase is 0, -// first normal transaction is 1, etc.). -func (s *ChainService) GetUtxo(options ...RescanOption) (*wire.TxOut, - *wire.MsgTx, error) { - ro := defaultRescanOptions() - ro.startBlock = &waddrmgr.BlockStamp{ - Hash: *s.chainParams.GenesisHash, - Height: 0, - } - for _, option := range options { - option(ro) - } - if len(ro.watchOutPoints) != 1 { - return nil, nil, fmt.Errorf("Must pass exactly one OutPoint.") - } - watchList := [][]byte{ - builder.OutPointToFilterEntry(ro.watchOutPoints[0]), - ro.watchOutPoints[0].Hash[:], - } - // Track our position in the chain. - curHeader, curHeight, err := s.LatestBlock() - curStamp := &waddrmgr.BlockStamp{ - Hash: curHeader.BlockHash(), - Height: int32(curHeight), - } - if err != nil { - return nil, nil, err - } - - // Find our earliest possible block. - if (ro.startBlock.Hash != chainhash.Hash{}) { - _, height, err := s.GetBlockByHash(ro.startBlock.Hash) - if err == nil { - ro.startBlock.Height = int32(height) - } else { - ro.startBlock.Hash = chainhash.Hash{} - } - } - if (ro.startBlock.Hash == chainhash.Hash{}) { - if ro.startBlock.Height == 0 { - ro.startBlock.Hash = *s.chainParams.GenesisHash - } else { - header, err := s.GetBlockByHeight( - uint32(ro.startBlock.Height)) - if err == nil { - ro.startBlock.Hash = header.BlockHash() - } else { - ro.startBlock.Hash = *s.chainParams.GenesisHash - ro.startBlock.Height = 0 - } - } - } - log.Tracef("Starting scan for output spend from known block %d (%s) "+ - "back to block %d (%s)", curStamp.Height, curStamp.Hash, - ro.startBlock.Height, ro.startBlock.Hash) - - for { - // Check the basic filter for the spend and the extended filter - // for the transaction in which the outpout is funded. - filter, err := s.GetCFilter(curStamp.Hash, false, - ro.queryOptions...) - if err != nil { - return nil, nil, fmt.Errorf("Couldn't get basic "+ - "filter for block %d (%s)", curStamp.Height, - curStamp.Hash) - } - matched := false - if filter != nil { - matched, err = filter.MatchAny(builder.DeriveKey( - &curStamp.Hash), watchList) - } - if err != nil { - return nil, nil, err - } - if !matched { - filter, err = s.GetCFilter(curStamp.Hash, true, - ro.queryOptions...) - if err != nil { - return nil, nil, fmt.Errorf("Couldn't get "+ - "extended filter for block %d (%s)", - curStamp.Height, curStamp.Hash) - } - if filter != nil { - matched, err = filter.MatchAny( - builder.DeriveKey(&curStamp.Hash), - watchList) - } - } - // If either is matched, download the block and check to see - // what we have. - if matched { - block, err := s.GetBlockFromNetwork(curStamp.Hash, - ro.queryOptions...) - if err != nil { - return nil, nil, err - } - if block == nil { - return nil, nil, fmt.Errorf("Couldn't get "+ - "block %d (%s)", curStamp.Height, - curStamp.Hash) - } - // If we've spent the output in this block, return an - // error stating that the output is spent. - for _, tx := range block.Transactions() { - for _, ti := range tx.MsgTx().TxIn { - if ti.PreviousOutPoint == - ro.watchOutPoints[0] { - return nil, tx.MsgTx(), nil - } - } - } - // If we found the transaction that created the output, - // then it's not spent and we can return the TxOut. - for _, tx := range block.Transactions() { - if *(tx.Hash()) == - ro.watchOutPoints[0].Hash { - return tx.MsgTx(). - TxOut[ro.watchOutPoints[0]. - Index], nil, nil - } - } - } - // Otherwise, iterate backwards until we've gone too - // far. - curStamp.Height-- - if curStamp.Height < ro.startBlock.Height { - return nil, nil, fmt.Errorf("Couldn't find "+ - "transaction %s", - ro.watchOutPoints[0].Hash) - } - header, err := s.GetBlockByHeight( - uint32(curStamp.Height)) - if err != nil { - return nil, nil, err - } - curStamp.Hash = header.BlockHash() - } -} diff --git a/spvsvc/spvchain/spvchain.go b/spvsvc/spvchain/spvchain.go deleted file mode 100644 index dc63fe1..0000000 --- a/spvsvc/spvchain/spvchain.go +++ /dev/null @@ -1,1191 +0,0 @@ -// NOTE: THIS API IS UNSTABLE RIGHT NOW. -// TODO: Add functional options to ChainService instantiation. - -package spvchain - -import ( - "errors" - "fmt" - "net" - "strconv" - "sync" - "sync/atomic" - "time" - - "github.com/btcsuite/btcd/addrmgr" - "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/connmgr" - "github.com/btcsuite/btcd/peer" - "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/waddrmgr" - "github.com/btcsuite/btcwallet/walletdb" -) - -// These are exported variables so they can be changed by users. -// TODO: Export functional options for these as much as possible so they can be -// changed call-to-call. -var ( - // ConnectionRetryInterval is the base amount of time to wait in between - // retries when connecting to persistent peers. It is adjusted by the - // number of retries such that there is a retry backoff. - ConnectionRetryInterval = time.Second * 5 - - // UserAgentName is the user agent name and is used to help identify - // ourselves to other bitcoin peers. - UserAgentName = "spvchain" - - // UserAgentVersion is the user agent version and is used to help - // identify ourselves to other bitcoin peers. - UserAgentVersion = "0.0.1-alpha" - - // Services describes the services that are supported by the server. - Services = wire.SFNodeWitness | wire.SFNodeCF - - // RequiredServices describes the services that are required to be - // supported by outbound peers. - RequiredServices = wire.SFNodeNetwork | wire.SFNodeWitness | wire.SFNodeCF - - // BanThreshold is the maximum ban score before a peer is banned. - BanThreshold = uint32(100) - - // BanDuration is the duration of a ban. - BanDuration = time.Hour * 24 - - // TargetOutbound is the number of outbound peers to target. - TargetOutbound = 8 - - // MaxPeers is the maximum number of connections the client maintains. - MaxPeers = 125 - - // DisableDNSSeed disables getting initial addresses for Bitcoin nodes - // from DNS. - DisableDNSSeed = false -) - -// updatePeerHeightsMsg is a message sent from the blockmanager to the server -// after a new block has been accepted. The purpose of the message is to update -// the heights of peers that were known to announce the block before we -// connected it to the main chain or recognized it as an orphan. With these -// updates, peer heights will be kept up to date, allowing for fresh data when -// selecting sync peer candidacy. -type updatePeerHeightsMsg struct { - newHash *chainhash.Hash - newHeight int32 - originPeer *serverPeer -} - -// peerState maintains state of inbound, persistent, outbound peers as well -// as banned peers and outbound groups. -type peerState struct { - outboundPeers map[int32]*serverPeer - persistentPeers map[int32]*serverPeer - banned map[string]time.Time - outboundGroups map[string]int -} - -// Count returns the count of all known peers. -func (ps *peerState) Count() int { - return len(ps.outboundPeers) + len(ps.persistentPeers) -} - -// forAllOutboundPeers is a helper function that runs closure on all outbound -// peers known to peerState. -func (ps *peerState) forAllOutboundPeers(closure func(sp *serverPeer)) { - for _, e := range ps.outboundPeers { - closure(e) - } - for _, e := range ps.persistentPeers { - closure(e) - } -} - -// forAllPeers is a helper function that runs closure on all peers known to -// peerState. -func (ps *peerState) forAllPeers(closure func(sp *serverPeer)) { - ps.forAllOutboundPeers(closure) -} - -// cfhRequest records which cfheaders we've requested, and the order in which -// we've requested them. Since there's no way to associate the cfheaders to the -// actual block hashes based on the cfheaders message to keep it compact, we -// track it this way. -type cfhRequest struct { - extended bool - stopHash chainhash.Hash -} - -// serverPeer extends the peer to maintain state shared by the server and -// the blockmanager. -type serverPeer struct { - // The following variables must only be used atomically - feeFilter int64 - - *peer.Peer - - connReq *connmgr.ConnReq - server *ChainService - persistent bool - continueHash *chainhash.Hash - relayMtx sync.Mutex - requestQueue []*wire.InvVect - knownAddresses map[string]struct{} - banScore connmgr.DynamicBanScore - quit chan struct{} - // The following slice of channels is used to subscribe to messages from - // the peer. This allows broadcast to multiple subscribers at once, - // allowing for multiple queries to be going to multiple peers at any - // one time. The mutex is for subscribe/unsubscribe functionality. - // The sends on these channels WILL NOT block; any messages the channel - // can't accept will be dropped silently. - recvSubscribers map[spMsgSubscription]struct{} - mtxSubscribers sync.RWMutex - // These are only necessary until the cfheaders logic is refactored as - // a query client. - requestedCFHeaders map[cfhRequest]int - mtxReqCFH sync.Mutex -} - -// newServerPeer returns a new serverPeer instance. The peer needs to be set by -// the caller. -func newServerPeer(s *ChainService, isPersistent bool) *serverPeer { - return &serverPeer{ - server: s, - persistent: isPersistent, - requestedCFHeaders: make(map[cfhRequest]int), - knownAddresses: make(map[string]struct{}), - quit: make(chan struct{}), - recvSubscribers: make(map[spMsgSubscription]struct{}), - } -} - -// newestBlock returns the current best block hash and height using the format -// required by the configuration for the peer package. -func (sp *serverPeer) newestBlock() (*chainhash.Hash, int32, error) { - best, err := sp.server.BestSnapshot() - if err != nil { - return nil, 0, err - } - return &best.Hash, best.Height, nil -} - -// addKnownAddresses adds the given addresses to the set of known addresses to -// the peer to prevent sending duplicate addresses. -func (sp *serverPeer) addKnownAddresses(addresses []*wire.NetAddress) { - for _, na := range addresses { - sp.knownAddresses[addrmgr.NetAddressKey(na)] = struct{}{} - } -} - -// addressKnown true if the given address is already known to the peer. -func (sp *serverPeer) addressKnown(na *wire.NetAddress) bool { - _, exists := sp.knownAddresses[addrmgr.NetAddressKey(na)] - return exists -} - -// addBanScore increases the persistent and decaying ban score fields by the -// values passed as parameters. If the resulting score exceeds half of the ban -// threshold, a warning is logged including the reason provided. Further, if -// the score is above the ban threshold, the peer will be banned and -// disconnected. -func (sp *serverPeer) addBanScore(persistent, transient uint32, reason string) { - // No warning is logged and no score is calculated if banning is disabled. - warnThreshold := BanThreshold >> 1 - if transient == 0 && persistent == 0 { - // The score is not being increased, but a warning message is still - // logged if the score is above the warn threshold. - score := sp.banScore.Int() - if score > warnThreshold { - log.Warnf("Misbehaving peer %s: %s -- ban score is %d, "+ - "it was not increased this time", sp, reason, score) - } - return - } - score := sp.banScore.Increase(persistent, transient) - if score > warnThreshold { - log.Warnf("Misbehaving peer %s: %s -- ban score increased to %d", - sp, reason, score) - if score > BanThreshold { - log.Warnf("Misbehaving peer %s -- banning and disconnecting", - sp) - sp.server.BanPeer(sp) - sp.Disconnect() - } - } -} - -// pushGetCFHeadersMsg sends a getcfheaders message for the provided block -// locator and stop hash to the connected peer. -func (sp *serverPeer) pushGetCFHeadersMsg(locator blockchain.BlockLocator, - stopHash *chainhash.Hash, ext bool) error { - msg := wire.NewMsgGetCFHeaders() - msg.HashStop = *stopHash - for _, hash := range locator { - err := msg.AddBlockLocatorHash(hash) - if err != nil { - return err - } - } - msg.Extended = ext - sp.QueueMessage(msg, nil) - return nil -} - -// pushSendHeadersMsg sends a sendheaders message to the connected peer. -func (sp *serverPeer) pushSendHeadersMsg() error { - if sp.VersionKnown() { - if sp.ProtocolVersion() > wire.SendHeadersVersion { - sp.QueueMessage(wire.NewMsgSendHeaders(), nil) - } - } - return nil -} - -// OnVerAck is invoked when a peer receives a verack bitcoin message and is used -// to send the "sendheaders" command to peers that are of a sufficienty new -// protocol version. -func (sp *serverPeer) OnVerAck(_ *peer.Peer, msg *wire.MsgVerAck) { - sp.pushSendHeadersMsg() -} - -// OnVersion is invoked when a peer receives a version bitcoin message -// and is used to negotiate the protocol version details as well as kick start -// the communications. -func (sp *serverPeer) OnVersion(_ *peer.Peer, msg *wire.MsgVersion) { - // Add the remote peer time as a sample for creating an offset against - // the local clock to keep the network time in sync. - sp.server.timeSource.AddTimeSample(sp.Addr(), msg.Timestamp) - - // Signal the block manager this peer is a new sync candidate. - sp.server.blockManager.NewPeer(sp) - - // Update the address manager and request known addresses from the - // remote peer for outbound connections. This is skipped when running - // on the simulation test network since it is only intended to connect - // to specified peers and actively avoids advertising and connecting to - // discovered peers. - if sp.server.chainParams.Net != chaincfg.SimNetParams.Net { - addrManager := sp.server.addrManager - // Request known addresses if the server address manager needs - // more and the peer has a protocol version new enough to - // include a timestamp with addresses. - hasTimestamp := sp.ProtocolVersion() >= - wire.NetAddressTimeVersion - if addrManager.NeedMoreAddresses() && hasTimestamp { - sp.QueueMessage(wire.NewMsgGetAddr(), nil) - } - - // Mark the address as a known good address. - addrManager.Good(sp.NA()) - } - - // Add valid peer to the server. - sp.server.AddPeer(sp) -} - -// OnInv is invoked when a peer receives an inv bitcoin message and is -// used to examine the inventory being advertised by the remote peer and react -// accordingly. We pass the message down to blockmanager which will call -// QueueMessage with any appropriate responses. -func (sp *serverPeer) OnInv(p *peer.Peer, msg *wire.MsgInv) { - log.Tracef("Got inv with %d items from %s", len(msg.InvList), p.Addr()) - newInv := wire.NewMsgInvSizeHint(uint(len(msg.InvList))) - for _, invVect := range msg.InvList { - if invVect.Type == wire.InvTypeTx { - log.Tracef("Ignoring tx %s in inv from %v -- "+ - "SPV mode", invVect.Hash, sp) - if sp.ProtocolVersion() >= wire.BIP0037Version { - log.Infof("Peer %v is announcing "+ - "transactions -- disconnecting", sp) - sp.Disconnect() - return - } - continue - } - err := newInv.AddInvVect(invVect) - if err != nil { - log.Errorf("Failed to add inventory vector: %s", err) - break - } - } - - if len(newInv.InvList) > 0 { - sp.server.blockManager.QueueInv(newInv, sp) - } -} - -// OnHeaders is invoked when a peer receives a headers bitcoin -// message. The message is passed down to the block manager. -func (sp *serverPeer) OnHeaders(p *peer.Peer, msg *wire.MsgHeaders) { - log.Tracef("Got headers with %d items from %s", len(msg.Headers), - p.Addr()) - sp.server.blockManager.QueueHeaders(msg, sp) -} - -// handleGetData is invoked when a peer receives a getdata bitcoin message and -// is used to deliver block and transaction information. -func (sp *serverPeer) OnGetData(_ *peer.Peer, msg *wire.MsgGetData) { - numAdded := 0 - notFound := wire.NewMsgNotFound() - - length := len(msg.InvList) - // A decaying ban score increase is applied to prevent exhausting resources - // with unusually large inventory queries. - // Requesting more than the maximum inventory vector length within a short - // period of time yields a score above the default ban threshold. Sustained - // bursts of small requests are not penalized as that would potentially ban - // peers performing IBD. - // This incremental score decays each minute to half of its value. - sp.addBanScore(0, uint32(length)*99/wire.MaxInvPerMsg, "getdata") - - // We wait on this wait channel periodically to prevent queuing - // far more data than we can send in a reasonable time, wasting memory. - // The waiting occurs after the database fetch for the next one to - // provide a little pipelining. - var waitChan chan struct{} - doneChan := make(chan struct{}, 1) - - for i, iv := range msg.InvList { - var c chan struct{} - // If this will be the last message we send. - if i == length-1 && len(notFound.InvList) == 0 { - c = doneChan - } else if (i+1)%3 == 0 { - // Buffered so as to not make the send goroutine block. - c = make(chan struct{}, 1) - } - var err error - switch iv.Type { - case wire.InvTypeTx: - err = sp.server.pushTxMsg(sp, &iv.Hash, c, waitChan) - default: - log.Warnf("Unsupported type in inventory request %d", - iv.Type) - continue - } - if err != nil { - notFound.AddInvVect(iv) - - // When there is a failure fetching the final entry - // and the done channel was sent in due to there - // being no outstanding not found inventory, consume - // it here because there is now not found inventory - // that will use the channel momentarily. - if i == len(msg.InvList)-1 && c != nil { - <-c - } - } - numAdded++ - waitChan = c - } - if len(notFound.InvList) != 0 { - sp.QueueMessage(notFound, doneChan) - } - - // Wait for messages to be sent. We can send quite a lot of data at this - // point and this will keep the peer busy for a decent amount of time. - // We don't process anything else by them in this time so that we - // have an idea of when we should hear back from them - else the idle - // timeout could fire when we were only half done sending the blocks. - if numAdded > 0 { - <-doneChan - } -} - -// OnFeeFilter is invoked when a peer receives a feefilter bitcoin message and -// is used by remote peers to request that no transactions which have a fee rate -// lower than provided value are inventoried to them. The peer will be -// disconnected if an invalid fee filter value is provided. -func (sp *serverPeer) OnFeeFilter(_ *peer.Peer, msg *wire.MsgFeeFilter) { - // Check that the passed minimum fee is a valid amount. - if msg.MinFee < 0 || msg.MinFee > btcutil.MaxSatoshi { - log.Debugf("Peer %v sent an invalid feefilter '%v' -- "+ - "disconnecting", sp, btcutil.Amount(msg.MinFee)) - sp.Disconnect() - return - } - - atomic.StoreInt64(&sp.feeFilter, msg.MinFee) -} - -// OnReject is invoked when a peer receives a reject bitcoin message and is -// used to notify the server about a rejected transaction. -func (sp *serverPeer) OnReject(_ *peer.Peer, msg *wire.MsgReject) { - -} - -// OnCFHeaders is invoked when a peer receives a cfheaders bitcoin message and -// is used to notify the server about a list of committed filter headers. -func (sp *serverPeer) OnCFHeaders(p *peer.Peer, msg *wire.MsgCFHeaders) { - log.Tracef("Got cfheaders message with %d items from %s", - len(msg.HeaderHashes), p.Addr()) - sp.server.blockManager.QueueCFHeaders(msg, sp) -} - -// OnAddr is invoked when a peer receives an addr bitcoin message and is -// used to notify the server about advertised addresses. -func (sp *serverPeer) OnAddr(_ *peer.Peer, msg *wire.MsgAddr) { - // Ignore addresses when running on the simulation test network. This - // helps prevent the network from becoming another public test network - // since it will not be able to learn about other peers that have not - // specifically been provided. - if sp.server.chainParams.Net == chaincfg.SimNetParams.Net { - return - } - - // Ignore old style addresses which don't include a timestamp. - if sp.ProtocolVersion() < wire.NetAddressTimeVersion { - return - } - - // A message that has no addresses is invalid. - if len(msg.AddrList) == 0 { - log.Errorf("Command [%s] from %s does not contain any addresses", - msg.Command(), sp) - sp.Disconnect() - return - } - - for _, na := range msg.AddrList { - // Don't add more address if we're disconnecting. - if !sp.Connected() { - return - } - - // Set the timestamp to 5 days ago if it's more than 24 hours - // in the future so this address is one of the first to be - // removed when space is needed. - now := time.Now() - if na.Timestamp.After(now.Add(time.Minute * 10)) { - na.Timestamp = now.Add(-1 * time.Hour * 24 * 5) - } - - // Add address to known addresses for this peer. - sp.addKnownAddresses([]*wire.NetAddress{na}) - } - - // Add addresses to server address manager. The address manager handles - // the details of things such as preventing duplicate addresses, max - // addresses, and last seen updates. - // XXX bitcoind gives a 2 hour time penalty here, do we want to do the - // same? - sp.server.addrManager.AddAddresses(msg.AddrList, sp.NA()) -} - -// OnRead is invoked when a peer receives a message and it is used to update -// the bytes received by the server. -func (sp *serverPeer) OnRead(_ *peer.Peer, bytesRead int, msg wire.Message, - err error) { - sp.server.AddBytesReceived(uint64(bytesRead)) - // Send a message to each subscriber. Each message gets its own - // goroutine to prevent blocking on the mutex lock. - // TODO: Flood control. - sp.mtxSubscribers.RLock() - defer sp.mtxSubscribers.RUnlock() - for subscription := range sp.recvSubscribers { - subscription.wg.Add(1) - go func(subscription spMsgSubscription) { - defer subscription.wg.Done() - select { - case <-subscription.quitChan: - case subscription.msgChan <- spMsg{ - msg: msg, - sp: sp, - }: - } - }(subscription) - } -} - -// subscribeRecvMsg handles adding OnRead subscriptions to the server peer. -func (sp *serverPeer) subscribeRecvMsg(subscription spMsgSubscription) { - sp.mtxSubscribers.Lock() - defer sp.mtxSubscribers.Unlock() - sp.recvSubscribers[subscription] = struct{}{} -} - -// unsubscribeRecvMsgs handles removing OnRead subscriptions from the server -// peer. -func (sp *serverPeer) unsubscribeRecvMsgs(subscription spMsgSubscription) { - sp.mtxSubscribers.Lock() - defer sp.mtxSubscribers.Unlock() - delete(sp.recvSubscribers, subscription) -} - -// OnWrite is invoked when a peer sends a message and it is used to update -// the bytes sent by the server. -func (sp *serverPeer) OnWrite(_ *peer.Peer, bytesWritten int, msg wire.Message, err error) { - sp.server.AddBytesSent(uint64(bytesWritten)) -} - -// blockSubscription allows a client to subscribe to and unsubscribe from block -// connect and disconnect notifications. -type blockSubscription struct { - onConnectBasic chan<- wire.BlockHeader - onConnectExt chan<- wire.BlockHeader - onDisconnect chan<- wire.BlockHeader - quit <-chan struct{} -} - -// ChainService is instantiated with functional options -type ChainService struct { - // The following variables must only be used atomically. - // Putting the uint64s first makes them 64-bit aligned for 32-bit systems. - bytesReceived uint64 // Total bytes received from all peers since start. - bytesSent uint64 // Total bytes sent by all peers since start. - started int32 - shutdown int32 - - db walletdb.DB - chainParams chaincfg.Params - addrManager *addrmgr.AddrManager - connManager *connmgr.ConnManager - blockManager *blockManager - newPeers chan *serverPeer - donePeers chan *serverPeer - banPeers chan *serverPeer - query chan interface{} - peerHeightsUpdate chan updatePeerHeightsMsg - wg sync.WaitGroup - quit chan struct{} - timeSource blockchain.MedianTimeSource - services wire.ServiceFlag - blockSubscribers map[blockSubscription]struct{} - mtxSubscribers sync.RWMutex - - userAgentName string - userAgentVersion string -} - -// BanPeer bans a peer that has already been connected to the server by ip. -func (s *ChainService) BanPeer(sp *serverPeer) { - s.banPeers <- sp -} - -// AddPeer adds a new peer that has already been connected to the server. -func (s *ChainService) AddPeer(sp *serverPeer) { - s.newPeers <- sp -} - -// AddBytesSent adds the passed number of bytes to the total bytes sent counter -// for the server. It is safe for concurrent access. -func (s *ChainService) AddBytesSent(bytesSent uint64) { - atomic.AddUint64(&s.bytesSent, bytesSent) -} - -// AddBytesReceived adds the passed number of bytes to the total bytes received -// counter for the server. It is safe for concurrent access. -func (s *ChainService) AddBytesReceived(bytesReceived uint64) { - atomic.AddUint64(&s.bytesReceived, bytesReceived) -} - -// NetTotals returns the sum of all bytes received and sent across the network -// for all peers. It is safe for concurrent access. -func (s *ChainService) NetTotals() (uint64, uint64) { - return atomic.LoadUint64(&s.bytesReceived), - atomic.LoadUint64(&s.bytesSent) -} - -// pushTxMsg sends a tx message for the provided transaction hash to the -// connected peer. An error is returned if the transaction hash is not known. -func (s *ChainService) pushTxMsg(sp *serverPeer, hash *chainhash.Hash, doneChan chan<- struct{}, waitChan <-chan struct{}) error { - // Attempt to fetch the requested transaction from the pool. A - // call could be made to check for existence first, but simply trying - // to fetch a missing transaction results in the same behavior. - /* tx, err := s.txMemPool.FetchTransaction(hash) - if err != nil { - log.Tracef("Unable to fetch tx %v from transaction "+ - "pool: %v", hash, err) - - if doneChan != nil { - doneChan <- struct{}{} - } - return err - } - - // Once we have fetched data wait for any previous operation to finish. - if waitChan != nil { - <-waitChan - } - - sp.QueueMessage(tx.MsgTx(), doneChan) */ - - return nil -} - -// rollBackToHeight rolls back all blocks until it hits the specified height. -// It sends notifications along the way. -func (s *ChainService) rollBackToHeight(height uint32) (*waddrmgr.BlockStamp, - error) { - bs, err := s.SyncedTo() - if err != nil { - return nil, err - } - for uint32(bs.Height) > height { - header, _, err := s.GetBlockByHash(bs.Hash) - if err != nil { - return nil, err - } - bs, err = s.rollBackLastBlock() - if err != nil { - return nil, err - } - // Now we send the block disconnected notifications. - // TODO: Rethink this so we don't send notifications - // outside the package directly from here, and so we - // don't end up halting in the middle of processing - // blocks if a client mishandles a channel while still - // guaranteeing in-order delivery. - s.mtxSubscribers.RLock() - for sub := range s.blockSubscribers { - if sub.onDisconnect != nil { - select { - case sub.onDisconnect <- header: - case <-sub.quit: - } - } - } - s.mtxSubscribers.RUnlock() - } - return bs, nil -} - -// peerHandler is used to handle peer operations such as adding and removing -// peers to and from the server, banning peers, and broadcasting messages to -// peers. It must be run in a goroutine. -func (s *ChainService) peerHandler() { - // Start the address manager and block manager, both of which are needed - // by peers. This is done here since their lifecycle is closely tied - // to this handler and rather than adding more channels to sychronize - // things, it's easier and slightly faster to simply start and stop them - // in this handler. - s.addrManager.Start() - s.blockManager.Start() - - state := &peerState{ - persistentPeers: make(map[int32]*serverPeer), - outboundPeers: make(map[int32]*serverPeer), - banned: make(map[string]time.Time), - outboundGroups: make(map[string]int), - } - - if !DisableDNSSeed { - // Add peers discovered through DNS to the address manager. - connmgr.SeedFromDNS(&s.chainParams, RequiredServices, - net.LookupIP, func(addrs []*wire.NetAddress) { - // Bitcoind uses a lookup of the dns seeder here. This - // is rather strange since the values looked up by the - // DNS seed lookups will vary quite a lot. - // to replicate this behaviour we put all addresses as - // having come from the first one. - s.addrManager.AddAddresses(addrs, addrs[0]) - }) - } - go s.connManager.Start() - -out: - for { - select { - // New peers connected to the server. - case p := <-s.newPeers: - s.handleAddPeerMsg(state, p) - - // Disconnected peers. - case p := <-s.donePeers: - s.handleDonePeerMsg(state, p) - - // Block accepted in mainchain or orphan, update peer height. - case umsg := <-s.peerHeightsUpdate: - s.handleUpdatePeerHeights(state, umsg) - - // Peer to ban. - case p := <-s.banPeers: - s.handleBanPeerMsg(state, p) - - case qmsg := <-s.query: - s.handleQuery(state, qmsg) - - case <-s.quit: - // Disconnect all peers on server shutdown. - state.forAllPeers(func(sp *serverPeer) { - log.Tracef("Shutdown peer %s", sp) - sp.Disconnect() - }) - break out - } - } - - s.connManager.Stop() - s.blockManager.Stop() - s.addrManager.Stop() - - // Drain channels before exiting so nothing is left waiting around - // to send. -cleanup: - for { - select { - case <-s.newPeers: - case <-s.donePeers: - case <-s.peerHeightsUpdate: - case <-s.query: - default: - break cleanup - } - } - s.wg.Done() - log.Tracef("Peer handler done") -} - -// Config is a struct detailing the configuration of the chain service. -type Config struct { - DataDir string - Database walletdb.DB - Namespace []byte - ChainParams chaincfg.Params - ConnectPeers []string - AddPeers []string -} - -// NewChainService returns a new chain service configured to connect to the -// bitcoin network type specified by chainParams. Use start to begin syncing -// with peers. -func NewChainService(cfg Config) (*ChainService, error) { - amgr := addrmgr.New(cfg.DataDir, net.LookupIP) - - s := ChainService{ - chainParams: cfg.ChainParams, - addrManager: amgr, - newPeers: make(chan *serverPeer, MaxPeers), - donePeers: make(chan *serverPeer, MaxPeers), - banPeers: make(chan *serverPeer, MaxPeers), - query: make(chan interface{}), - quit: make(chan struct{}), - peerHeightsUpdate: make(chan updatePeerHeightsMsg), - db: cfg.Database, - timeSource: blockchain.NewMedianTime(), - services: Services, - userAgentName: UserAgentName, - userAgentVersion: UserAgentVersion, - blockSubscribers: make(map[blockSubscription]struct{}), - } - - err := s.createSPVNS() - if err != nil { - return nil, err - } - - bm, err := newBlockManager(&s) - if err != nil { - return nil, err - } - s.blockManager = bm - - // Only setup a function to return new addresses to connect to when - // not running in connect-only mode. The simulation network is always - // in connect-only mode since it is only intended to connect to - // specified peers and actively avoid advertising and connecting to - // discovered peers in order to prevent it from becoming a public test - // network. - var newAddressFunc func() (net.Addr, error) - if s.chainParams.Net != chaincfg.SimNetParams.Net { - newAddressFunc = func() (net.Addr, error) { - for tries := 0; tries < 100; tries++ { - addr := s.addrManager.GetAddress() - if addr == nil { - break - } - - // Address will not be invalid, local or unroutable - // because addrmanager rejects those on addition. - // Just check that we don't already have an address - // in the same group so that we are not connecting - // to the same network segment at the expense of - // others. - key := addrmgr.GroupKey(addr.NetAddress()) - if s.OutboundGroupCount(key) != 0 { - continue - } - - // only allow recent nodes (10mins) after we failed 30 - // times - if tries < 30 && time.Since(addr.LastAttempt()) < 10*time.Minute { - continue - } - - // allow nondefault ports after 50 failed tries. - if tries < 50 && fmt.Sprintf("%d", addr.NetAddress().Port) != - s.chainParams.DefaultPort { - continue - } - - addrString := addrmgr.NetAddressKey(addr.NetAddress()) - return addrStringToNetAddr(addrString) - } - - return nil, errors.New("no valid connect address") - } - } - - // Create a connection manager. - if MaxPeers < TargetOutbound { - TargetOutbound = MaxPeers - } - cmgr, err := connmgr.New(&connmgr.Config{ - RetryDuration: ConnectionRetryInterval, - TargetOutbound: uint32(TargetOutbound), - Dial: func(addr net.Addr) (net.Conn, error) { - return net.Dial(addr.Network(), addr.String()) - }, - OnConnection: s.outboundPeerConnected, - GetNewAddress: newAddressFunc, - }) - if err != nil { - return nil, err - } - s.connManager = cmgr - - // Start up persistent peers. - permanentPeers := cfg.ConnectPeers - if len(permanentPeers) == 0 { - permanentPeers = cfg.AddPeers - } - for _, addr := range permanentPeers { - tcpAddr, err := addrStringToNetAddr(addr) - if err != nil { - return nil, err - } - - go s.connManager.Connect(&connmgr.ConnReq{ - Addr: tcpAddr, - Permanent: true, - }) - } - - return &s, nil -} - -// addrStringToNetAddr takes an address in the form of 'host:port' and returns -// a net.Addr which maps to the original address with any host names resolved -// to IP addresses. -func addrStringToNetAddr(addr string) (net.Addr, error) { - host, strPort, err := net.SplitHostPort(addr) - if err != nil { - return nil, err - } - - // Attempt to look up an IP address associated with the parsed host. - ips, err := net.LookupIP(host) - if err != nil { - return nil, err - } - if len(ips) == 0 { - return nil, fmt.Errorf("no addresses found for %s", host) - } - - port, err := strconv.Atoi(strPort) - if err != nil { - return nil, err - } - - return &net.TCPAddr{ - IP: ips[0], - Port: port, - }, nil -} - -// handleUpdatePeerHeight updates the heights of all peers who were known to -// announce a block we recently accepted. -func (s *ChainService) handleUpdatePeerHeights(state *peerState, umsg updatePeerHeightsMsg) { - state.forAllPeers(func(sp *serverPeer) { - // The origin peer should already have the updated height. - if sp == umsg.originPeer { - return - } - - // This is a pointer to the underlying memory which doesn't - // change. - latestBlkHash := sp.LastAnnouncedBlock() - - // Skip this peer if it hasn't recently announced any new blocks. - if latestBlkHash == nil { - return - } - - // If the peer has recently announced a block, and this block - // matches our newly accepted block, then update their block - // height. - if *latestBlkHash == *umsg.newHash { - sp.UpdateLastBlockHeight(umsg.newHeight) - sp.UpdateLastAnnouncedBlock(nil) - } - }) -} - -// handleAddPeerMsg deals with adding new peers. It is invoked from the -// peerHandler goroutine. -func (s *ChainService) handleAddPeerMsg(state *peerState, sp *serverPeer) bool { - if sp == nil { - return false - } - - // Ignore new peers if we're shutting down. - if atomic.LoadInt32(&s.shutdown) != 0 { - log.Infof("New peer %s ignored - server is shutting down", sp) - sp.Disconnect() - return false - } - - // Disconnect banned peers. - host, _, err := net.SplitHostPort(sp.Addr()) - if err != nil { - log.Debugf("can't split host/port: %s", err) - sp.Disconnect() - return false - } - if banEnd, ok := state.banned[host]; ok { - if time.Now().Before(banEnd) { - log.Debugf("Peer %s is banned for another %v - disconnecting", - host, banEnd.Sub(time.Now())) - sp.Disconnect() - return false - } - - log.Infof("Peer %s is no longer banned", host) - delete(state.banned, host) - } - - // TODO: Check for max peers from a single IP. - - // Limit max number of total peers. - if state.Count() >= MaxPeers { - log.Infof("Max peers reached [%d] - disconnecting peer %s", - MaxPeers, sp) - sp.Disconnect() - // TODO: how to handle permanent peers here? - // they should be rescheduled. - return false - } - - // Add the new peer and start it. - log.Debugf("New peer %s", sp) - state.outboundGroups[addrmgr.GroupKey(sp.NA())]++ - if sp.persistent { - state.persistentPeers[sp.ID()] = sp - } else { - state.outboundPeers[sp.ID()] = sp - } - - return true -} - -// handleDonePeerMsg deals with peers that have signalled they are done. It is -// invoked from the peerHandler goroutine. -func (s *ChainService) handleDonePeerMsg(state *peerState, sp *serverPeer) { - var list map[int32]*serverPeer - if sp.persistent { - list = state.persistentPeers - } else { - list = state.outboundPeers - } - if _, ok := list[sp.ID()]; ok { - if !sp.Inbound() && sp.VersionKnown() { - state.outboundGroups[addrmgr.GroupKey(sp.NA())]-- - } - if !sp.Inbound() && sp.connReq != nil { - s.connManager.Disconnect(sp.connReq.ID()) - } - delete(list, sp.ID()) - log.Debugf("Removed peer %s", sp) - return - } - - if sp.connReq != nil { - s.connManager.Disconnect(sp.connReq.ID()) - } - - // Update the address' last seen time if the peer has acknowledged - // our version and has sent us its version as well. - if sp.VerAckReceived() && sp.VersionKnown() && sp.NA() != nil { - s.addrManager.Connected(sp.NA()) - } - - // If we get here it means that either we didn't know about the peer - // or we purposefully deleted it. -} - -// handleBanPeerMsg deals with banning peers. It is invoked from the -// peerHandler goroutine. -func (s *ChainService) handleBanPeerMsg(state *peerState, sp *serverPeer) { - host, _, err := net.SplitHostPort(sp.Addr()) - if err != nil { - log.Debugf("can't split ban peer %s: %s", sp.Addr(), err) - return - } - log.Infof("Banned peer %s for %v", host, BanDuration) - state.banned[host] = time.Now().Add(BanDuration) -} - -// disconnectPeer attempts to drop the connection of a tageted peer in the -// passed peer list. Targets are identified via usage of the passed -// `compareFunc`, which should return `true` if the passed peer is the target -// peer. This function returns true on success and false if the peer is unable -// to be located. If the peer is found, and the passed callback: `whenFound' -// isn't nil, we call it with the peer as the argument before it is removed -// from the peerList, and is disconnected from the server. -func disconnectPeer(peerList map[int32]*serverPeer, compareFunc func(*serverPeer) bool, whenFound func(*serverPeer)) bool { - for addr, peer := range peerList { - if compareFunc(peer) { - if whenFound != nil { - whenFound(peer) - } - - // This is ok because we are not continuing - // to iterate so won't corrupt the loop. - delete(peerList, addr) - peer.Disconnect() - return true - } - } - return false -} - -// PublishTransaction sends the transaction to the consensus RPC server so it -// can be propigated to other nodes and eventually mined. -func (s *ChainService) PublishTransaction(tx *wire.MsgTx) error { - /*_, err := s.rpcClient.SendRawTransaction(tx, false) - return err*/ - return nil -} - -// newPeerConfig returns the configuration for the given serverPeer. -func newPeerConfig(sp *serverPeer) *peer.Config { - return &peer.Config{ - Listeners: peer.MessageListeners{ - OnVersion: sp.OnVersion, - //OnVerAck: sp.OnVerAck, // Don't use sendheaders yet - OnInv: sp.OnInv, - OnHeaders: sp.OnHeaders, - OnCFHeaders: sp.OnCFHeaders, - OnGetData: sp.OnGetData, - OnReject: sp.OnReject, - OnFeeFilter: sp.OnFeeFilter, - OnAddr: sp.OnAddr, - OnRead: sp.OnRead, - OnWrite: sp.OnWrite, - - // Note: The reference client currently bans peers that send alerts - // not signed with its key. We could verify against their key, but - // since the reference client is currently unwilling to support - // other implementations' alert messages, we will not relay theirs. - OnAlert: nil, - }, - NewestBlock: sp.newestBlock, - HostToNetAddress: sp.server.addrManager.HostToNetAddress, - UserAgentName: sp.server.userAgentName, - UserAgentVersion: sp.server.userAgentVersion, - ChainParams: &sp.server.chainParams, - Services: sp.server.services, - ProtocolVersion: wire.FeeFilterVersion, - DisableRelayTx: true, - } -} - -// outboundPeerConnected is invoked by the connection manager when a new -// outbound connection is established. It initializes a new outbound server -// peer instance, associates it with the relevant state such as the connection -// request instance and the connection itself, and finally notifies the address -// manager of the attempt. -func (s *ChainService) outboundPeerConnected(c *connmgr.ConnReq, conn net.Conn) { - sp := newServerPeer(s, c.Permanent) - p, err := peer.NewOutboundPeer(newPeerConfig(sp), c.Addr.String()) - if err != nil { - log.Debugf("Cannot create outbound peer %s: %s", c.Addr, err) - s.connManager.Disconnect(c.ID()) - } - sp.Peer = p - sp.connReq = c - sp.AssociateConnection(conn) - go s.peerDoneHandler(sp) - s.addrManager.Attempt(sp.NA()) -} - -// peerDoneHandler handles peer disconnects by notifiying the server that it's -// done along with other performing other desirable cleanup. -func (s *ChainService) peerDoneHandler(sp *serverPeer) { - sp.WaitForDisconnect() - s.donePeers <- sp - - // Only tell block manager we are gone if we ever told it we existed. - if sp.VersionKnown() { - s.blockManager.DonePeer(sp) - } - close(sp.quit) -} - -// UpdatePeerHeights updates the heights of all peers who have have announced -// the latest connected main chain block, or a recognized orphan. These height -// updates allow us to dynamically refresh peer heights, ensuring sync peer -// selection has access to the latest block heights for each peer. -func (s *ChainService) UpdatePeerHeights(latestBlkHash *chainhash.Hash, latestHeight int32, updateSource *serverPeer) { - s.peerHeightsUpdate <- updatePeerHeightsMsg{ - newHash: latestBlkHash, - newHeight: latestHeight, - originPeer: updateSource, - } -} - -// ChainParams returns a copy of the ChainService's chaincfg.Params. -func (s *ChainService) ChainParams() chaincfg.Params { - return s.chainParams -} - -// Start begins connecting to peers and syncing the blockchain. -func (s *ChainService) Start() { - // Already started? - if atomic.AddInt32(&s.started, 1) != 1 { - return - } - - // Start the peer handler which in turn starts the address and block - // managers. - s.wg.Add(1) - go s.peerHandler() -} - -// Stop gracefully shuts down the server by stopping and disconnecting all -// peers and the main listener. -func (s *ChainService) Stop() error { - // Make sure this only happens once. - if atomic.AddInt32(&s.shutdown, 1) != 1 { - return nil - } - - // Signal the remaining goroutines to quit. - close(s.quit) - s.wg.Wait() - return nil -} - -// IsCurrent lets the caller know whether the chain service's block manager -// thinks its view of the network is current. -func (s *ChainService) IsCurrent() bool { - return s.blockManager.IsCurrent() -} - -// subscribeBlockMsg handles adding block subscriptions to the ChainService. -// TODO: Rethink this. -func (s *ChainService) subscribeBlockMsg(subscription blockSubscription) { - s.mtxSubscribers.Lock() - defer s.mtxSubscribers.Unlock() - s.blockSubscribers[subscription] = struct{}{} -} - -// unsubscribeBlockMsgs handles removing block subscriptions from the -// ChainService. -func (s *ChainService) unsubscribeBlockMsgs(subscription blockSubscription) { - s.mtxSubscribers.Lock() - defer s.mtxSubscribers.Unlock() - delete(s.blockSubscribers, subscription) -} diff --git a/spvsvc/spvchain/sync_test.go b/spvsvc/spvchain/sync_test.go deleted file mode 100644 index 6c2578d..0000000 --- a/spvsvc/spvchain/sync_test.go +++ /dev/null @@ -1,1336 +0,0 @@ -// TODO: Break up tests into bite-sized pieces. - -package spvchain_test - -import ( - "bytes" - "fmt" - "io/ioutil" - "os" - "reflect" - "sync" - "testing" - "time" - - "github.com/aakselrod/btctestlog" - "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/rpctest" - "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btclog" - "github.com/btcsuite/btcrpcclient" - "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcutil/gcs" - "github.com/btcsuite/btcutil/gcs/builder" - "github.com/btcsuite/btcwallet/spvsvc/spvchain" - "github.com/btcsuite/btcwallet/waddrmgr" - "github.com/btcsuite/btcwallet/wallet/txauthor" - "github.com/btcsuite/btcwallet/walletdb" - _ "github.com/btcsuite/btcwallet/walletdb/bdb" -) - -var ( - // Try btclog.InfoLvl for output like you'd see in normal operation, or - // btclog.TraceLvl to help debug code. Anything but btclog.Off turns on - // log messages from the tests themselves as well. Keep in mind some - // log messages may not appear in order due to use of multiple query - // goroutines in the tests. - logLevel = btclog.TraceLvl - syncTimeout = 30 * time.Second - syncUpdate = time.Second - // Don't set this too high for your platform, or the tests will miss - // messages. - // TODO: Make this a benchmark instead. - // TODO: Implement load limiting for both outgoing and incoming - // messages. - numQueryThreads = 20 - queryOptions = []spvchain.QueryOption{ - //spvchain.NumRetries(5), - } - // The logged sequence of events we want to see. The value of i - // represents the block for which a loop is generating a log entry, - // given for readability only. - // "bc": OnBlockConnected - // "fc" xx: OnFilteredBlockConnected with xx (uint8) relevant TXs - // "rv": OnRecvTx - // "rd": OnRedeemingTx - // "bd": OnBlockDisconnected - // "fd": OnFilteredBlockDisconnected - wantLog = func() (log []byte) { - for i := 796; i <= 800; i++ { - // BlockConnected and FilteredBlockConnected - log = append(log, []byte("bcfc")...) - // 0 relevant TXs - log = append(log, 0x00) - } - // Block with one relevant (receive) transaction - log = append(log, []byte("bcrvfc")...) - log = append(log, 0x01) - // 124 blocks with nothing - for i := 802; i <= 925; i++ { - log = append(log, []byte("bcfc")...) - log = append(log, 0x00) - } - // Block with 1 redeeming transaction - log = append(log, []byte("bcrdfc")...) - log = append(log, 0x01) - // Block with nothing - log = append(log, []byte("bcfc")...) - log = append(log, 0x00) - // Update with rewind - rewind back to 795, add another address, - // and see more interesting transactions. - for i := 927; i >= 796; i-- { - // BlockDisconnected and FilteredBlockDisconnected - log = append(log, []byte("bdfd")...) - } - // Forward to 800 - for i := 796; i <= 800; i++ { - // BlockConnected and FilteredBlockConnected - log = append(log, []byte("bcfc")...) - // 0 relevant TXs - log = append(log, 0x00) - } - // Block with two relevant (receive) transactions - log = append(log, []byte("bcrvrvfc")...) - log = append(log, 0x02) - // 124 blocks with nothing - for i := 802; i <= 925; i++ { - log = append(log, []byte("bcfc")...) - log = append(log, 0x00) - } - // 2 blocks with 1 redeeming transaction each - for i := 926; i <= 927; i++ { - log = append(log, []byte("bcrdfc")...) - log = append(log, 0x01) - } - // Block with nothing - log = append(log, []byte("bcfc")...) - log = append(log, 0x00) - // 3 block rollback - for i := 928; i >= 926; i-- { - log = append(log, []byte("fdbd")...) - } - // 5 block empty reorg - for i := 926; i <= 930; i++ { - log = append(log, []byte("bcfc")...) - log = append(log, 0x00) - } - // 5 block rollback - for i := 930; i >= 926; i-- { - log = append(log, []byte("fdbd")...) - } - // 2 blocks with 1 redeeming transaction each - for i := 926; i <= 927; i++ { - log = append(log, []byte("bcrdfc")...) - log = append(log, 0x01) - } - // 8 block rest of reorg - for i := 928; i <= 935; i++ { - log = append(log, []byte("bcfc")...) - log = append(log, 0x00) - } - return log - }() - - // rescanMtx locks all the variables to which the rescan goroutine's - // notifications write. - rescanMtx sync.RWMutex - - // gotLog is where we accumulate the event log from the rescan. Then we - // compare it to wantLog to see if the series of events the rescan saw - // happened as expected. - gotLog []byte - - // curBlockHeight lets the rescan goroutine track where it thinks the - // chain is based on OnBlockConnected and OnBlockDisconnected. - curBlockHeight int32 - - // curFilteredBlockHeight lets the rescan goroutine track where it - // thinks the chain is based on OnFilteredBlockConnected and - // OnFilteredBlockDisconnected. - curFilteredBlockHeight int32 - - // ourKnownTxsByBlock lets the rescan goroutine keep track of - // transactions we're interested in that are in the blockchain we're - // following as signalled by OnBlockConnected, OnBlockDisconnected, - // OnRecvTx, and OnRedeemingTx. - ourKnownTxsByBlock = make(map[chainhash.Hash][]*btcutil.Tx) - - // ourKnownTxsByFilteredBlock lets the rescan goroutine keep track of - // transactions we're interested in that are in the blockchain we're - // following as signalled by OnFilteredBlockConnected and - // OnFilteredBlockDisconnected. - ourKnownTxsByFilteredBlock = make(map[chainhash.Hash][]*btcutil.Tx) -) - -// secSource is an implementation of btcwallet/txauthor/SecretsSource that -// stores WitnessPubKeyHash addresses. -type secSource struct { - keys map[string]*btcec.PrivateKey - scripts map[string]*[]byte - params *chaincfg.Params -} - -func (s *secSource) add(privKey *btcec.PrivateKey) (btcutil.Address, error) { - pubKeyHash := btcutil.Hash160(privKey.PubKey().SerializeCompressed()) - addr, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, s.params) - if err != nil { - return nil, err - } - script, err := txscript.PayToAddrScript(addr) - if err != nil { - return nil, err - } - s.keys[addr.String()] = privKey - s.scripts[addr.String()] = &script - _, addrs, _, err := txscript.ExtractPkScriptAddrs(script, s.params) - if err != nil { - return nil, err - } - if addrs[0].String() != addr.String() { - return nil, fmt.Errorf("Encoded and decoded addresses don't "+ - "match. Encoded: %s, decoded: %s", addr, addrs[0]) - } - return addr, nil -} - -// GetKey is required by the txscript.KeyDB interface -func (s *secSource) GetKey(addr btcutil.Address) (*btcec.PrivateKey, bool, - error) { - privKey, ok := s.keys[addr.String()] - if !ok { - return nil, true, fmt.Errorf("No key for address %s", addr) - } - return privKey, true, nil -} - -// GetScript is required by the txscript.ScriptDB interface -func (s *secSource) GetScript(addr btcutil.Address) ([]byte, error) { - script, ok := s.scripts[addr.String()] - if !ok { - return nil, fmt.Errorf("No script for address %s", addr) - } - return *script, nil -} - -// ChainParams is required by the SecretsSource interface -func (s *secSource) ChainParams() *chaincfg.Params { - return s.params -} - -func newSecSource(params *chaincfg.Params) *secSource { - return &secSource{ - keys: make(map[string]*btcec.PrivateKey), - scripts: make(map[string]*[]byte), - params: params, - } -} - -func TestSetup(t *testing.T) { - // Set up logging. - logger, err := btctestlog.NewTestLogger(t) - if err != nil { - t.Fatalf("Could not set up logger: %s", err) - } - chainLogger := btclog.NewSubsystemLogger(logger, "CHAIN: ") - chainLogger.SetLevel(logLevel) - spvchain.UseLogger(chainLogger) - rpcLogger := btclog.NewSubsystemLogger(logger, "RPCC: ") - rpcLogger.SetLevel(logLevel) - btcrpcclient.UseLogger(rpcLogger) - - // Create a btcd SimNet node and generate 500 blocks - h1, err := rpctest.New(&chaincfg.SimNetParams, nil, nil) - if err != nil { - t.Fatalf("Couldn't create harness: %s", err) - } - defer h1.TearDown() - err = h1.SetUp(false, 0) - if err != nil { - t.Fatalf("Couldn't set up harness: %s", err) - } - _, err = h1.Node.Generate(500) - if err != nil { - t.Fatalf("Couldn't generate blocks: %s", err) - } - - // Create a second btcd SimNet node - h2, err := rpctest.New(&chaincfg.SimNetParams, nil, nil) - if err != nil { - t.Fatalf("Couldn't create harness: %s", err) - } - defer h2.TearDown() - err = h2.SetUp(false, 0) - if err != nil { - t.Fatalf("Couldn't set up harness: %s", err) - } - - // Create a third btcd SimNet node and generate 900 blocks - h3, err := rpctest.New(&chaincfg.SimNetParams, nil, nil) - if err != nil { - t.Fatalf("Couldn't create harness: %s", err) - } - defer h3.TearDown() - err = h3.SetUp(false, 0) - if err != nil { - t.Fatalf("Couldn't set up harness: %s", err) - } - _, err = h3.Node.Generate(900) - if err != nil { - t.Fatalf("Couldn't generate blocks: %s", err) - } - - // Connect, sync, and disconnect h1 and h2 - err = csd([]*rpctest.Harness{h1, h2}) - if err != nil { - t.Fatalf("Couldn't connect/sync/disconnect h1 and h2: %s", err) - } - - // Generate 300 blocks on the first node and 350 on the second - _, err = h1.Node.Generate(300) - if err != nil { - t.Fatalf("Couldn't generate blocks: %s", err) - } - _, err = h2.Node.Generate(350) - if err != nil { - t.Fatalf("Couldn't generate blocks: %s", err) - } - - // Now we have a node with 800 blocks (h1), 850 blocks (h2), and - // 900 blocks (h3). The chains of nodes h1 and h2 match up to block - // 500. By default, a synchronizing wallet connected to all three - // should synchronize to h3. However, we're going to take checkpoints - // from h1 at 111, 333, 555, and 777, and add those to the - // synchronizing wallet's chain parameters so that it should - // disconnect from h3 at block 111, and from h2 at block 555, and - // then synchronize to block 800 from h1. Order of connection is - // unfortunately not guaranteed, so the reorg may not happen with every - // test. - - // Copy parameters and insert checkpoints - modParams := chaincfg.SimNetParams - for _, height := range []int64{111, 333, 555, 777} { - hash, err := h1.Node.GetBlockHash(height) - if err != nil { - t.Fatalf("Couldn't get block hash for height %d: %s", - height, err) - } - modParams.Checkpoints = append(modParams.Checkpoints, - chaincfg.Checkpoint{ - Hash: hash, - Height: int32(height), - }) - } - - // Create a temporary directory, initialize an empty walletdb with an - // SPV chain namespace, and create a configuration for the ChainService. - tempDir, err := ioutil.TempDir("", "spvchain") - if err != nil { - t.Fatalf("Failed to create temporary directory: %s", err) - } - defer os.RemoveAll(tempDir) - db, err := walletdb.Create("bdb", tempDir+"/weks.db") - defer db.Close() - if err != nil { - t.Fatalf("Error opening DB: %s\n", err) - } - if err != nil { - t.Fatalf("Error geting namespace: %s\n", err) - } - config := spvchain.Config{ - DataDir: tempDir, - Database: db, - ChainParams: modParams, - AddPeers: []string{ - h3.P2PAddress(), - h2.P2PAddress(), - h1.P2PAddress(), - }, - } - - spvchain.MaxPeers = 3 - spvchain.BanDuration = 5 * time.Second - spvchain.WaitForMoreCFHeaders = time.Second - svc, err := spvchain.NewChainService(config) - if err != nil { - t.Fatalf("Error creating ChainService: %s", err) - } - svc.Start() - defer svc.Stop() - - // Make sure the client synchronizes with the correct node - err = waitForSync(t, svc, h1) - if err != nil { - t.Fatalf("Couldn't sync ChainService: %s", err) - } - - // Generate an address and send it some coins on the h1 chain. We use - // this to test rescans and notifications. - secSrc := newSecSource(&modParams) - privKey1, err := btcec.NewPrivateKey(btcec.S256()) - if err != nil { - t.Fatalf("Couldn't generate private key: %s", err) - } - addr1, err := secSrc.add(privKey1) - if err != nil { - t.Fatalf("Couldn't create address from key: %s", err) - } - script1, err := secSrc.GetScript(addr1) - if err != nil { - t.Fatalf("Couldn't create script from address: %s", err) - } - out1 := wire.TxOut{ - PkScript: script1, - Value: 1000000000, - } - // Fee rate is satoshis per byte - tx1, err := h1.CreateTransaction([]*wire.TxOut{&out1}, 1000) - if err != nil { - t.Fatalf("Couldn't create transaction from script: %s", err) - } - _, err = h1.Node.SendRawTransaction(tx1, true) - if err != nil { - t.Fatalf("Unable to send raw transaction to node: %s", err) - } - privKey2, err := btcec.NewPrivateKey(btcec.S256()) - if err != nil { - t.Fatalf("Couldn't generate private key: %s", err) - } - addr2, err := secSrc.add(privKey2) - if err != nil { - t.Fatalf("Couldn't create address from key: %s", err) - } - script2, err := secSrc.GetScript(addr2) - if err != nil { - t.Fatalf("Couldn't create script from address: %s", err) - } - out2 := wire.TxOut{ - PkScript: script2, - Value: 1000000000, - } - // Fee rate is satoshis per byte - tx2, err := h1.CreateTransaction([]*wire.TxOut{&out2}, 1000) - if err != nil { - t.Fatalf("Couldn't create transaction from script: %s", err) - } - _, err = h1.Node.SendRawTransaction(tx2, true) - if err != nil { - t.Fatalf("Unable to send raw transaction to node: %s", err) - } - _, err = h1.Node.Generate(1) - if err != nil { - t.Fatalf("Couldn't generate/submit block: %s", err) - } - err = waitForSync(t, svc, h1) - if err != nil { - t.Fatalf("Couldn't sync ChainService: %s", err) - } - - // Do a rescan that searches only for a specific TXID - startBlock := waddrmgr.BlockStamp{Height: 795} - endBlock := waddrmgr.BlockStamp{Height: 801} - var foundTx *btcutil.Tx - err = svc.Rescan( - spvchain.StartBlock(&startBlock), - spvchain.EndBlock(&endBlock), - spvchain.WatchTxIDs(tx1.TxHash()), - spvchain.NotificationHandlers(btcrpcclient.NotificationHandlers{ - OnFilteredBlockConnected: func(height int32, - header *wire.BlockHeader, - relevantTxs []*btcutil.Tx) { - if height == 801 { - if len(relevantTxs) != 1 { - t.Fatalf("Didn't get expected "+ - "number of relevant "+ - "transactions from "+ - "rescan: want 1, got "+ - "%d", len(relevantTxs)) - } - if *(relevantTxs[0].Hash()) != - tx1.TxHash() { - t.Fatalf("Didn't get expected "+ - "relevant transaction:"+ - " want %s, got %s", - tx1.TxHash(), - relevantTxs[0].Hash()) - } - foundTx = relevantTxs[0] - } - }, - }), - ) - if err != nil || foundTx == nil || *(foundTx.Hash()) != tx1.TxHash() { - t.Fatalf("Couldn't rescan chain for transaction %s: %s", - tx1.TxHash(), err) - } - - // Call GetUtxo for our output in tx1 to see if it's spent. - ourIndex := 1 << 30 // Should work on 32-bit systems - for i, txo := range tx1.TxOut { - if bytes.Equal(txo.PkScript, script1) { - ourIndex = i - } - } - var ourOutPoint wire.OutPoint - if ourIndex != 1<<30 { - ourOutPoint = wire.OutPoint{ - Hash: tx1.TxHash(), - Index: uint32(ourIndex), - } - } else { - t.Fatalf("Couldn't find the index of our output in transaction"+ - " %s", tx1.TxHash()) - } - txo, redeemingTx, err := svc.GetUtxo( - spvchain.WatchOutPoints(ourOutPoint), - spvchain.StartBlock(&waddrmgr.BlockStamp{Height: 801}), - ) - if err != nil { - t.Fatalf("Couldn't get UTXO %s: %s", ourOutPoint, err) - } - if !bytes.Equal(txo.PkScript, script1) { - t.Fatalf("UTXO's script doesn't match expected script for %s", - ourOutPoint) - } - - // Start a rescan with notifications in another goroutine. We'll kill - // it with a quit channel at the end and make sure we got the expected - // results. - quitRescan := make(chan struct{}) - startBlock = waddrmgr.BlockStamp{Height: 795} - rescan, errChan := startRescan(t, svc, addr1, &startBlock, quitRescan) - if err != nil { - t.Fatalf("Couldn't start a rescan for %s: %s", addr1, err) - } - err = waitForSync(t, svc, h1) - if err != nil { - t.Fatalf("Couldn't sync ChainService: %s", err) - } - numTXs, _, err := checkRescanStatus() - if numTXs != 1 { - t.Fatalf("Wrong number of relevant transactions. Want: 1, got:"+ - " %d", numTXs) - } - - // Generate 124 blocks on h1 to make sure it reorgs the other nodes. - // Ensure the ChainService instance stays caught up. - h1.Node.Generate(124) - err = waitForSync(t, svc, h1) - if err != nil { - t.Fatalf("Couldn't sync ChainService: %s", err) - } - - // Connect/sync/disconnect h2 to make it reorg to the h1 chain. - err = csd([]*rpctest.Harness{h1, h2}) - if err != nil { - t.Fatalf("Couldn't sync h2 to h1: %s", err) - } - - // Spend the outputs we sent ourselves over two blocks. - inSrc := func(tx wire.MsgTx) func(target btcutil.Amount) ( - total btcutil.Amount, inputs []*wire.TxIn, - inputValues []btcutil.Amount, scripts [][]byte, err error) { - ourIndex := 1 << 30 // Should work on 32-bit systems - for i, txo := range tx.TxOut { - if bytes.Equal(txo.PkScript, script1) || - bytes.Equal(txo.PkScript, script2) { - ourIndex = i - } - } - return func(target btcutil.Amount) (total btcutil.Amount, - inputs []*wire.TxIn, inputValues []btcutil.Amount, - scripts [][]byte, err error) { - if ourIndex == 1<<30 { - err = fmt.Errorf("Couldn't find our address " + - "in the passed transaction's outputs.") - return - } - total = target - inputs = []*wire.TxIn{ - { - PreviousOutPoint: wire.OutPoint{ - Hash: tx.TxHash(), - Index: uint32(ourIndex), - }, - }, - } - inputValues = []btcutil.Amount{ - btcutil.Amount(tx.TxOut[ourIndex].Value)} - scripts = [][]byte{tx.TxOut[ourIndex].PkScript} - err = nil - return - } - } - // Create another address to send to so we don't trip the rescan with - // the old address and we can test monitoring both OutPoint usage and - // receipt by addresses. - privKey3, err := btcec.NewPrivateKey(btcec.S256()) - if err != nil { - t.Fatalf("Couldn't generate private key: %s", err) - } - addr3, err := secSrc.add(privKey3) - if err != nil { - t.Fatalf("Couldn't create address from key: %s", err) - } - script3, err := secSrc.GetScript(addr3) - if err != nil { - t.Fatalf("Couldn't create script from address: %s", err) - } - out3 := wire.TxOut{ - PkScript: script3, - Value: 500000000, - } - // Spend the first transaction and mine a block. - authTx1, err := txauthor.NewUnsignedTransaction( - []*wire.TxOut{ - &out3, - }, - // Fee rate is satoshis per kilobyte - 1024000, - inSrc(*tx1), - func() ([]byte, error) { - return script3, nil - }, - ) - if err != nil { - t.Fatalf("Couldn't create unsigned transaction: %s", err) - } - err = authTx1.AddAllInputScripts(secSrc) - if err != nil { - t.Fatalf("Couldn't sign transaction: %s", err) - } - banPeer(svc, h2) - err = svc.SendTransaction(authTx1.Tx, queryOptions...) - if err != nil { - t.Fatalf("Unable to send transaction to network: %s", err) - } - _, err = h1.Node.Generate(1) - if err != nil { - t.Fatalf("Couldn't generate/submit block: %s", err) - } - err = waitForSync(t, svc, h1) - if err != nil { - t.Fatalf("Couldn't sync ChainService: %s", err) - } - numTXs, _, err = checkRescanStatus() - if numTXs != 2 { - t.Fatalf("Wrong number of relevant transactions. Want: 2, got:"+ - " %d", numTXs) - } - // Spend the second transaction and mine a block. - authTx2, err := txauthor.NewUnsignedTransaction( - []*wire.TxOut{ - &out3, - }, - // Fee rate is satoshis per kilobyte - 1024000, - inSrc(*tx2), - func() ([]byte, error) { - return script3, nil - }, - ) - if err != nil { - t.Fatalf("Couldn't create unsigned transaction: %s", err) - } - err = authTx2.AddAllInputScripts(secSrc) - if err != nil { - t.Fatalf("Couldn't sign transaction: %s", err) - } - banPeer(svc, h2) - err = svc.SendTransaction(authTx2.Tx, queryOptions...) - if err != nil { - t.Fatalf("Unable to send transaction to network: %s", err) - } - _, err = h1.Node.Generate(1) - if err != nil { - t.Fatalf("Couldn't generate/submit block: %s", err) - } - err = waitForSync(t, svc, h1) - if err != nil { - t.Fatalf("Couldn't sync ChainService: %s", err) - } - numTXs, _, err = checkRescanStatus() - if numTXs != 2 { - t.Fatalf("Wrong number of relevant transactions. Want: 2, got:"+ - " %d", numTXs) - } - - // Update the filter with the second address, and we should have 2 more - // relevant transactions. - err = rescan.Update(spvchain.AddAddrs(addr2), spvchain.Rewind(795)) - if err != nil { - t.Fatalf("Couldn't update the rescan filter: %s", err) - } - err = waitForSync(t, svc, h1) - if err != nil { - t.Fatalf("Couldn't sync ChainService: %s", err) - } - numTXs, _, err = checkRescanStatus() - if numTXs != 4 { - t.Fatalf("Wrong number of relevant transactions. Want: 4, got:"+ - " %d", numTXs) - } - - // Generate a block with a nonstandard coinbase to generate a basic - // filter with 0 entries. - _, err = h1.GenerateAndSubmitBlockWithCustomCoinbaseOutputs( - []*btcutil.Tx{}, rpctest.BlockVersion, time.Time{}, - []wire.TxOut{{ - Value: 0, - PkScript: []byte{}, - }}) - if err != nil { - t.Fatalf("Couldn't generate/submit block: %s", err) - } - err = waitForSync(t, svc, h1) - if err != nil { - t.Fatalf("Couldn't sync ChainService: %s", err) - } - - // Check and make sure the previous UTXO is now spent. - txo, redeemingTx, err = svc.GetUtxo( - spvchain.WatchOutPoints(ourOutPoint), - spvchain.StartBlock(&waddrmgr.BlockStamp{Height: 801}), - ) - if err != nil { - t.Fatalf("Couldn't get UTXO %s: %s", ourOutPoint, err) - } - if redeemingTx.TxHash() != authTx1.Tx.TxHash() { - t.Fatalf("Redeeming transaction doesn't match expected "+ - "transaction: want %s, got %s", authTx1.Tx.TxHash(), - redeemingTx.TxHash()) - } - - // Test that we can get blocks and cfilters via P2P and decide which are - // valid and which aren't. - // TODO: Split this out into a benchmark. - err = testRandomBlocks(t, svc, h1) - if err != nil { - t.Fatalf("Testing blocks and cfilters failed: %s", err) - } - - // Generate 5 blocks on h2 and wait for ChainService to sync to the - // newly-best chain on h2. This includes the transactions sent via - // svc.SendTransaction earlier, so we'll have to - _, err = h2.Node.Generate(5) - if err != nil { - t.Fatalf("Couldn't generate/submit blocks: %s", err) - } - err = waitForSync(t, svc, h2) - if err != nil { - t.Fatalf("Couldn't sync ChainService: %s", err) - } - numTXs, _, err = checkRescanStatus() - if numTXs != 2 { - t.Fatalf("Wrong number of relevant transactions. Want: 2, got:"+ - " %d", numTXs) - } - - // Generate 7 blocks on h1 and wait for ChainService to sync to the - // newly-best chain on h1. - _, err = h1.Node.Generate(7) - if err != nil { - t.Fatalf("Couldn't generate/submit block: %s", err) - } - err = waitForSync(t, svc, h1) - if err != nil { - t.Fatalf("Couldn't sync ChainService: %s", err) - } - numTXs, _, err = checkRescanStatus() - if numTXs != 4 { - t.Fatalf("Wrong number of relevant transactions. Want: 4, got:"+ - " %d", numTXs) - } - - close(quitRescan) - err = <-errChan - if err != nil { - t.Fatalf("Rescan ended with error: %s", err) - } - if !bytes.Equal(wantLog, gotLog) { - leastBytes := len(wantLog) - if len(gotLog) < leastBytes { - leastBytes = len(gotLog) - } - diffIndex := 0 - for i := 0; i < leastBytes; i++ { - if wantLog[i] != gotLog[i] { - diffIndex = i - break - } - } - t.Fatalf("Rescan event logs differ starting at %d.\nWant: %s\n"+ - "Got: %s\nDifference - want: %s\nDifference -- got: "+ - "%s", diffIndex, wantLog, gotLog, wantLog[diffIndex:], - gotLog[diffIndex:]) - } -} - -// csd does a connect-sync-disconnect between nodes in order to support -// reorg testing. It brings up and tears down a temporary node, otherwise the -// nodes try to reconnect to each other which results in unintended reorgs. -func csd(harnesses []*rpctest.Harness) error { - hTemp, err := rpctest.New(&chaincfg.SimNetParams, nil, nil) - if err != nil { - return err - } - // Tear down node at the end of the function. - defer hTemp.TearDown() - err = hTemp.SetUp(false, 0) - if err != nil { - return err - } - for _, harness := range harnesses { - err = rpctest.ConnectNode(hTemp, harness) - if err != nil { - return err - } - } - return rpctest.JoinNodes(harnesses, rpctest.Blocks) -} - -// waitForSync waits for the ChainService to sync to the current chain state. -func waitForSync(t *testing.T, svc *spvchain.ChainService, - correctSyncNode *rpctest.Harness) error { - knownBestHash, knownBestHeight, err := - correctSyncNode.Node.GetBestBlock() - if err != nil { - return err - } - if logLevel != btclog.Off { - t.Logf("Syncing to %d (%s)", knownBestHeight, knownBestHash) - } - var haveBest *waddrmgr.BlockStamp - haveBest, err = svc.BestSnapshot() - if err != nil { - return fmt.Errorf("Couldn't get best snapshot from "+ - "ChainService: %s", err) - } - var total time.Duration - for haveBest.Hash != *knownBestHash { - if total > syncTimeout { - return fmt.Errorf("Timed out after %v waiting for "+ - "header synchronization.", syncTimeout) - } - if haveBest.Height > knownBestHeight { - return fmt.Errorf("Synchronized to the wrong chain.") - } - time.Sleep(syncUpdate) - total += syncUpdate - haveBest, err = svc.BestSnapshot() - if err != nil { - return fmt.Errorf("Couldn't get best snapshot from "+ - "ChainService: %s", err) - } - } - // Check if we're current. - if !svc.IsCurrent() { - return fmt.Errorf("ChainService doesn't see itself as current!") - } - // Check if we have all of the cfheaders. - knownBasicHeader, err := correctSyncNode.Node.GetCFilterHeader( - knownBestHash, false) - if err != nil { - return fmt.Errorf("Couldn't get latest basic header from "+ - "%s: %s", correctSyncNode.P2PAddress(), err) - } - knownExtHeader, err := correctSyncNode.Node.GetCFilterHeader( - knownBestHash, true) - if err != nil { - return fmt.Errorf("Couldn't get latest extended header from "+ - "%s: %s", correctSyncNode.P2PAddress(), err) - } - haveBasicHeader := &chainhash.Hash{} - haveExtHeader := &chainhash.Hash{} - for (*knownBasicHeader.HeaderHashes[0] != *haveBasicHeader) && - (*knownExtHeader.HeaderHashes[0] != *haveExtHeader) { - if total > syncTimeout { - return fmt.Errorf("Timed out after %v waiting for "+ - "cfheaders synchronization.", syncTimeout) - } - haveBasicHeader, _ = svc.GetBasicHeader(*knownBestHash) - haveExtHeader, _ = svc.GetExtHeader(*knownBestHash) - time.Sleep(syncUpdate) - total += syncUpdate - } - if logLevel != btclog.Off { - t.Logf("Synced cfheaders to %d (%s)", haveBest.Height, - haveBest.Hash) - } - // At this point, we know the latest cfheader is stored in the - // ChainService database. We now compare each cfheader the - // harness knows about to what's stored in the ChainService - // database to see if we've missed anything or messed anything - // up. - for i := int32(0); i <= haveBest.Height; i++ { - head, err := svc.GetBlockByHeight(uint32(i)) - if err != nil { - return fmt.Errorf("Couldn't read block by "+ - "height: %s", err) - } - hash := head.BlockHash() - haveBasicHeader, err = svc.GetBasicHeader(hash) - if err != nil { - return fmt.Errorf("Couldn't get basic header "+ - "for %d (%s) from DB", i, hash) - } - haveExtHeader, err = svc.GetExtHeader(hash) - if err != nil { - return fmt.Errorf("Couldn't get extended "+ - "header for %d (%s) from DB", i, hash) - } - knownBasicHeader, err = - correctSyncNode.Node.GetCFilterHeader(&hash, - false) - if err != nil { - return fmt.Errorf("Couldn't get basic header "+ - "for %d (%s) from node %s", i, hash, - correctSyncNode.P2PAddress()) - } - knownExtHeader, err = - correctSyncNode.Node.GetCFilterHeader(&hash, - true) - if err != nil { - return fmt.Errorf("Couldn't get extended "+ - "header for %d (%s) from node %s", i, - hash, correctSyncNode.P2PAddress()) - } - if *haveBasicHeader != - *knownBasicHeader.HeaderHashes[0] { - return fmt.Errorf("Basic header for %d (%s) "+ - "doesn't match node %s. DB: %s, node: "+ - "%s", i, hash, - correctSyncNode.P2PAddress(), - haveBasicHeader, - knownBasicHeader.HeaderHashes[0]) - } - if *haveExtHeader != - *knownExtHeader.HeaderHashes[0] { - return fmt.Errorf("Extended header for %d (%s)"+ - " doesn't match node %s. DB: %s, node:"+ - " %s", i, hash, - correctSyncNode.P2PAddress(), - haveExtHeader, - knownExtHeader.HeaderHashes[0]) - } - } - // At this point, we know we have good cfheaders. Now we wait for the - // rescan, if one is going, to catch up. - for { - time.Sleep(syncUpdate) - total += syncUpdate - rescanMtx.RLock() - // We don't want to do this if we haven't started a rescan - // yet. - if len(gotLog) == 0 { - rescanMtx.RUnlock() - break - } - _, rescanHeight, err := checkRescanStatus() - if err != nil { - rescanMtx.RUnlock() - return err - } - if logLevel != btclog.Off { - t.Logf("Rescan caught up to block %d", rescanHeight) - } - if rescanHeight == haveBest.Height { - rescanMtx.RUnlock() - break - } - if total > syncTimeout { - rescanMtx.RUnlock() - return fmt.Errorf("Timed out after %v waiting for "+ - "rescan to catch up.", syncTimeout) - } - rescanMtx.RUnlock() - } - return nil -} - -// testRandomBlocks goes through all blocks in random order and ensures we can -// correctly get cfilters from them. It uses numQueryThreads goroutines running -// at the same time to go through this. 50 is comfortable on my somewhat dated -// laptop with default query optimization settings. -// TODO: Make this a benchmark instead. -func testRandomBlocks(t *testing.T, svc *spvchain.ChainService, - correctSyncNode *rpctest.Harness) error { - var haveBest *waddrmgr.BlockStamp - haveBest, err := svc.BestSnapshot() - if err != nil { - return fmt.Errorf("Couldn't get best snapshot from "+ - "ChainService: %s", err) - } - // Keep track of an error channel with enough buffer space to track one - // error per block. - errChan := make(chan error, haveBest.Height) - // Test getting all of the blocks and filters. - var wg sync.WaitGroup - workerQueue := make(chan struct{}, numQueryThreads) - for i := int32(1); i <= haveBest.Height; i++ { - wg.Add(1) - height := uint32(i) - // Wait until there's room in the worker queue. - workerQueue <- struct{}{} - go func() { - // On exit, open a spot in workerQueue and tell the - // wait group we're done. - defer func() { - <-workerQueue - }() - defer wg.Done() - // Get block header from database. - blockHeader, err := svc.GetBlockByHeight(height) - if err != nil { - errChan <- fmt.Errorf("Couldn't get block "+ - "header by height %d: %s", height, err) - return - } - blockHash := blockHeader.BlockHash() - // Get block via RPC. - wantBlock, err := correctSyncNode.Node.GetBlock( - &blockHash) - if err != nil { - errChan <- fmt.Errorf("Couldn't get block %d "+ - "(%s) by RPC", height, blockHash) - return - } - // Get block from network. - haveBlock, err := svc.GetBlockFromNetwork(blockHash, - queryOptions...) - if err != nil { - errChan <- err - return - } - if haveBlock == nil { - errChan <- fmt.Errorf("Couldn't get block %d "+ - "(%s) from network", height, blockHash) - return - } - // Check that network and RPC blocks match. - if !reflect.DeepEqual(*haveBlock.MsgBlock(), - *wantBlock) { - errChan <- fmt.Errorf("Block from network "+ - "doesn't match block from RPC. Want: "+ - "%s, RPC: %s, network: %s", blockHash, - wantBlock.BlockHash(), - haveBlock.MsgBlock().BlockHash()) - return - } - // Check that block height matches what we have. - if int32(height) != haveBlock.Height() { - errChan <- fmt.Errorf("Block height from "+ - "network doesn't match expected "+ - "height. Want: %s, network: %s", - height, haveBlock.Height()) - return - } - // Get basic cfilter from network. - haveFilter, err := svc.GetCFilter(blockHash, false, - queryOptions...) - if err != nil { - errChan <- err - return - } - // Get basic cfilter from RPC. - wantFilter, err := correctSyncNode.Node.GetCFilter( - &blockHash, false) - if err != nil { - errChan <- fmt.Errorf("Couldn't get basic "+ - "filter for block %d via RPC: %s", - height, err) - return - } - // Check that network and RPC cfilters match. - var haveBytes []byte - if haveFilter != nil { - haveBytes = haveFilter.NBytes() - } - if !bytes.Equal(haveBytes, wantFilter.Data) { - errChan <- fmt.Errorf("Basic filter from P2P "+ - "network/DB doesn't match RPC value "+ - "for block %d", height) - return - } - // Calculate basic filter from block. - calcFilter, err := builder.BuildBasicFilter( - haveBlock.MsgBlock()) - if err != nil && err != gcs.ErrNoData { - errChan <- fmt.Errorf("Couldn't build basic "+ - "filter for block %d (%s): %s", height, - blockHash, err) - return - } - // Check that the network value matches the calculated - // value from the block. - if !reflect.DeepEqual(haveFilter, calcFilter) { - errChan <- fmt.Errorf("Basic filter from P2P "+ - "network/DB doesn't match calculated "+ - "value for block %d", height) - return - } - // Get previous basic filter header from the database. - prevHeader, err := svc.GetBasicHeader( - blockHeader.PrevBlock) - if err != nil { - errChan <- fmt.Errorf("Couldn't get basic "+ - "filter header for block %d (%s) from "+ - "DB: %s", height-1, - blockHeader.PrevBlock, err) - return - } - // Get current basic filter header from the database. - curHeader, err := svc.GetBasicHeader(blockHash) - if err != nil { - errChan <- fmt.Errorf("Couldn't get basic "+ - "filter header for block %d (%s) from "+ - "DB: %s", height-1, blockHash, err) - return - } - // Check that the filter and header line up. - calcHeader := builder.MakeHeaderForFilter(calcFilter, - *prevHeader) - if !bytes.Equal(curHeader[:], calcHeader[:]) { - errChan <- fmt.Errorf("Filter header doesn't "+ - "match. Want: %s, got: %s", curHeader, - calcHeader) - return - } - // Get extended cfilter from network - haveFilter, err = svc.GetCFilter(blockHash, true, - queryOptions...) - if err != nil { - errChan <- err - return - } - // Get extended cfilter from RPC - wantFilter, err = correctSyncNode.Node.GetCFilter( - &blockHash, true) - if err != nil { - errChan <- fmt.Errorf("Couldn't get extended "+ - "filter for block %d via RPC: %s", - height, err) - return - } - // Check that network and RPC cfilters match - if haveFilter != nil { - haveBytes = haveFilter.NBytes() - } - if !bytes.Equal(haveBytes, wantFilter.Data) { - errChan <- fmt.Errorf("Extended filter from "+ - "P2P network/DB doesn't match RPC "+ - "value for block %d", height) - return - } - // Calculate extended filter from block - calcFilter, err = builder.BuildExtFilter( - haveBlock.MsgBlock()) - if err != nil && err != gcs.ErrNoData { - errChan <- fmt.Errorf("Couldn't build extended"+ - " filter for block %d (%s): %s", height, - blockHash, err) - return - } - // Check that the network value matches the calculated - // value from the block. - if !reflect.DeepEqual(haveFilter, calcFilter) { - errChan <- fmt.Errorf("Extended filter from "+ - "P2P network/DB doesn't match "+ - "calculated value for block %d", height) - return - } - // Get previous extended filter header from the - // database. - prevHeader, err = svc.GetExtHeader( - blockHeader.PrevBlock) - if err != nil { - errChan <- fmt.Errorf("Couldn't get extended "+ - "filter header for block %d (%s) from "+ - "DB: %s", height-1, - blockHeader.PrevBlock, err) - return - } - // Get current basic filter header from the database. - curHeader, err = svc.GetExtHeader(blockHash) - if err != nil { - errChan <- fmt.Errorf("Couldn't get extended "+ - "filter header for block %d (%s) from "+ - "DB: %s", height-1, - blockHash, err) - return - } - // Check that the filter and header line up. - calcHeader = builder.MakeHeaderForFilter(calcFilter, - *prevHeader) - if !bytes.Equal(curHeader[:], calcHeader[:]) { - errChan <- fmt.Errorf("Filter header doesn't "+ - "match. Want: %s, got: %s", curHeader, - calcHeader) - return - } - }() - } - // Wait for all queries to finish. - wg.Wait() - // Close the error channel to make the error monitoring goroutine - // finish. - close(errChan) - var lastErr error - for err := range errChan { - if err != nil { - t.Errorf("%s", err) - lastErr = fmt.Errorf("Couldn't validate all " + - "blocks, filters, and filter headers.") - } - } - if logLevel != btclog.Off { - t.Logf("Finished checking %d blocks and their cfilters", - haveBest.Height) - } - return lastErr -} - -// startRescan starts a rescan in another goroutine, and logs all notifications -// from the rescan. At the end, the log should match one we precomputed based -// on the flow of the test. The rescan starts at the genesis block and the -// notifications continue until the `quit` channel is closed. -func startRescan(t *testing.T, svc *spvchain.ChainService, addr btcutil.Address, - startBlock *waddrmgr.BlockStamp, quit <-chan struct{}) (spvchain.Rescan, - <-chan error) { - rescan, errChan := svc.NewRescan( - spvchain.QuitChan(quit), - spvchain.WatchAddrs(addr), - spvchain.StartBlock(startBlock), - spvchain.NotificationHandlers( - btcrpcclient.NotificationHandlers{ - OnBlockConnected: func( - hash *chainhash.Hash, - height int32, time time.Time) { - rescanMtx.Lock() - gotLog = append(gotLog, - []byte("bc")...) - curBlockHeight = height - rescanMtx.Unlock() - }, - OnBlockDisconnected: func( - hash *chainhash.Hash, - height int32, time time.Time) { - rescanMtx.Lock() - delete(ourKnownTxsByBlock, *hash) - gotLog = append(gotLog, - []byte("bd")...) - curBlockHeight = height - 1 - rescanMtx.Unlock() - }, - OnRecvTx: func(tx *btcutil.Tx, - details *btcjson.BlockDetails) { - rescanMtx.Lock() - hash, err := chainhash. - NewHashFromStr( - details.Hash) - if err != nil { - t.Errorf("Couldn't "+ - "decode hash "+ - "%s: %s", - details.Hash, - err) - } - ourKnownTxsByBlock[*hash] = append( - ourKnownTxsByBlock[*hash], - tx) - gotLog = append(gotLog, - []byte("rv")...) - rescanMtx.Unlock() - }, - OnRedeemingTx: func(tx *btcutil.Tx, - details *btcjson.BlockDetails) { - rescanMtx.Lock() - hash, err := chainhash. - NewHashFromStr( - details.Hash) - if err != nil { - t.Errorf("Couldn't "+ - "decode hash "+ - "%s: %s", - details.Hash, - err) - } - ourKnownTxsByBlock[*hash] = append( - ourKnownTxsByBlock[*hash], - tx) - gotLog = append(gotLog, - []byte("rd")...) - rescanMtx.Unlock() - }, - OnFilteredBlockConnected: func( - height int32, - header *wire.BlockHeader, - relevantTxs []*btcutil.Tx) { - rescanMtx.Lock() - ourKnownTxsByFilteredBlock[header.BlockHash()] = - relevantTxs - gotLog = append(gotLog, - []byte("fc")...) - gotLog = append(gotLog, - uint8(len(relevantTxs))) - curFilteredBlockHeight = height - rescanMtx.Unlock() - }, - OnFilteredBlockDisconnected: func( - height int32, - header *wire.BlockHeader) { - rescanMtx.Lock() - delete(ourKnownTxsByFilteredBlock, - header.BlockHash()) - gotLog = append(gotLog, - []byte("fd")...) - curFilteredBlockHeight = - height - 1 - rescanMtx.Unlock() - }, - }), - ) - return rescan, errChan -} - -// checkRescanStatus returns the number of relevant transactions we currently -// know about and the currently known height. -func checkRescanStatus() (int, int32, error) { - var txCount [2]int - rescanMtx.RLock() - defer rescanMtx.RUnlock() - for _, list := range ourKnownTxsByBlock { - for range list { - txCount[0]++ - } - } - for _, list := range ourKnownTxsByFilteredBlock { - for range list { - txCount[1]++ - } - } - if txCount[0] != txCount[1] { - return 0, 0, fmt.Errorf("Conflicting transaction count " + - "between notifications.") - } - if curBlockHeight != curFilteredBlockHeight { - return 0, 0, fmt.Errorf("Conflicting block height between " + - "notifications.") - } - return txCount[0], curBlockHeight, nil -} - -// banPeer bans and disconnects the requested harness from the ChainService -// instance for BanDuration seconds. -func banPeer(svc *spvchain.ChainService, harness *rpctest.Harness) { - peers := svc.Peers() - for _, peer := range peers { - if peer.Addr() == harness.P2PAddress() { - svc.BanPeer(peer) - peer.Disconnect() - } - } -} diff --git a/spvsvc/spvsvc.go b/spvsvc/spvsvc.go deleted file mode 100644 index b321d10..0000000 --- a/spvsvc/spvsvc.go +++ /dev/null @@ -1,39 +0,0 @@ -package spvsvc - -import "github.com/btcsuite/btcwallet/spvsvc/spvchain" - -// SynchronizationService provides an SPV, p2p-based backend for a wallet to -// synchronize it with the network and send transactions it signs. -type SynchronizationService struct { - chain spvchain.ChainService -} - -// SynchronizationServiceOpt is the return type of functional options for -// creating a SynchronizationService object. -type SynchronizationServiceOpt func(*SynchronizationService) error - -// NewSynchronizationService creates a new SynchronizationService with -// functional options. -func NewSynchronizationService(opts ...SynchronizationServiceOpt) (*SynchronizationService, error) { - s := SynchronizationService{ - //userAgentName: defaultUserAgentName, - //userAgentVersion: defaultUserAgentVersion, - } - for _, opt := range opts { - err := opt(&s) - if err != nil { - return nil, err - } - } - return &s, nil -} - -// UserAgent is a functional option to set the user agent information as it -// appears to other nodes. -func UserAgent(agentName, agentVersion string) SynchronizationServiceOpt { - return func(s *SynchronizationService) error { - //s.userAgentName = agentName - //s.userAgentVersion = agentVersion - return nil - } -}