277 lines
8 KiB
Go
277 lines
8 KiB
Go
package spvsvc
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/addrmgr"
|
|
"github.com/btcsuite/btcd/connmgr"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcwallet/spvsvc/spvchain"
|
|
"github.com/btcsuite/btcwallet/wallet"
|
|
)
|
|
|
|
// SynchronizationService provides an SPV, p2p-based backend for a wallet to
|
|
// synchronize it with the network and send transactions it signs.
|
|
type SynchronizationService struct {
|
|
wallet *wallet.Wallet
|
|
chainService 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
|
|
}
|
|
}
|
|
|
|
// AddrManager is a functional option to create an address manager for the
|
|
// synchronization service. It takes a string as an argument to specify the
|
|
// directory in which to store addresses.
|
|
func AddrManager(dir string) SynchronizationServiceOpt {
|
|
return func(s *SynchronizationService) error {
|
|
m := addrmgr.New(dir, spvLookup)
|
|
s.addrManager = m
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ConnManagerOpt is the return type of functional options to create a
|
|
// connection manager for the synchronization service.
|
|
type ConnManagerOpt func(*connmgr.Config) error
|
|
|
|
// ConnManager is a functional option to create a connection manager for the
|
|
// synchronization service.
|
|
func ConnManager(opts ...ConnManagerOpt) SynchronizationServiceOpt {
|
|
return func(s *SynchronizationService) error {
|
|
c := connmgr.Config{
|
|
TargetOutbound: defaultTargetOutbound,
|
|
RetryDuration: connectionRetryInterval,
|
|
GetNewAddress: s.getNewAddress,
|
|
}
|
|
for _, opt := range opts {
|
|
err := opt(&c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
connManager, err := connmgr.New(&c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.connManager = connManager
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// TargetOutbound is a functional option to specify how many outbound
|
|
// connections should be made by the ConnManager to peers. Defaults to 8.
|
|
func TargetOutbound(target uint32) ConnManagerOpt {
|
|
return func(c *connmgr.Config) error {
|
|
c.TargetOutbound = target
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RetryDuration is a functional option to specify how long to wait before
|
|
// retrying a connection request. Defaults to 5s.
|
|
func RetryDuration(duration time.Duration) ConnManagerOpt {
|
|
return func(c *connmgr.Config) error {
|
|
c.RetryDuration = duration
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (s *SynchronizationService) getNewAddress() (net.Addr, error) {
|
|
if s.addrManager == nil {
|
|
return nil, log.Error("Couldn't get address for new " +
|
|
"connection: address manager is nil.")
|
|
}
|
|
s.addrManager.Start()
|
|
for tries := 0; tries < 100; tries++ {
|
|
addr := s.addrManager.GetAddress()
|
|
if addr == nil {
|
|
break
|
|
}
|
|
// If we already have peers in this group, skip this address
|
|
key := addrmgr.GroupKey(addr.NetAddress())
|
|
if s.outboundGroupCount(key) != 0 {
|
|
continue
|
|
}
|
|
if tries < 30 && time.Since(addr.LastAttempt()) < 10*time.Minute {
|
|
continue
|
|
}
|
|
if tries < 50 && fmt.Sprintf("%d", addr.NetAddress().Port) !=
|
|
s.wallet.ChainParams().DefaultPort {
|
|
continue
|
|
}
|
|
addrString := addrmgr.NetAddressKey(addr.NetAddress())
|
|
return addrStringToNetAddr(addrString)
|
|
}
|
|
return nil, log.Error("Couldn't get address for new connection: no " +
|
|
"valid addresses known.")
|
|
}
|
|
|
|
func (s *SynchronizationService) outboundGroupCount(key string) int {
|
|
replyChan := make(chan int)
|
|
s.query <- getOutboundGroup{key: key, reply: replyChan}
|
|
return <-replyChan
|
|
}
|
|
|
|
// SynchronizeWallet associates a wallet with the consensus RPC client,
|
|
// synchronizes the wallet with the latest changes to the blockchain, and
|
|
// continuously updates the wallet through RPC notifications.
|
|
//
|
|
// This function does not return without error until the wallet is synchronized
|
|
// to the current chain state.
|
|
func (s *SynchronizationService) SynchronizeWallet(w *wallet.Wallet) error {
|
|
s.wallet = w
|
|
|
|
s.wg.Add(3)
|
|
go s.notificationQueueHandler()
|
|
go s.processQueuedNotifications()
|
|
go s.queryHandler()
|
|
|
|
return s.syncWithNetwork(w)
|
|
}
|
|
|
|
func (s *SynchronizationService) queryHandler() {
|
|
|
|
}
|
|
|
|
func (s *SynchronizationService) processQueuedNotifications() {
|
|
for n := range s.dequeueNotification {
|
|
var err error
|
|
notificationSwitch:
|
|
switch n := n.(type) {
|
|
case *wire.MsgBlock:
|
|
if n.BlockHash().String() != "" {
|
|
break notificationSwitch
|
|
}
|
|
case *wire.MsgHeaders:
|
|
case *wire.MsgInv:
|
|
case *wire.MsgReject:
|
|
}
|
|
|
|
if err != nil {
|
|
log.Errorf("Cannot handle peer notification: %v", err)
|
|
}
|
|
}
|
|
s.wg.Done()
|
|
}
|
|
|
|
// syncWithNetwork brings the wallet up to date with the current chain server
|
|
// connection. It creates a rescan request and blocks until the rescan has
|
|
// finished.
|
|
func (s *SynchronizationService) syncWithNetwork(w *wallet.Wallet) error {
|
|
/*chainClient := s.rpcClient
|
|
|
|
// Request notifications for connected and disconnected blocks.
|
|
//
|
|
// TODO(jrick): Either request this notification only once, or when
|
|
// btcrpcclient is modified to allow some notification request to not
|
|
// automatically resent on reconnect, include the notifyblocks request
|
|
// as well. I am leaning towards allowing off all btcrpcclient
|
|
// notification re-registrations, in which case the code here should be
|
|
// left as is.
|
|
err := chainClient.NotifyBlocks()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Request notifications for transactions sending to all wallet
|
|
// addresses.
|
|
addrs, unspent, err := w.ActiveData()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO(jrick): How should this handle a synced height earlier than
|
|
// the chain server best block?
|
|
|
|
// When no addresses have been generated for the wallet, the rescan can
|
|
// be skipped.
|
|
//
|
|
// TODO: This is only correct because activeData above returns all
|
|
// addresses ever created, including those that don't need to be watched
|
|
// anymore. This code should be updated when this assumption is no
|
|
// longer true, but worst case would result in an unnecessary rescan.
|
|
if len(addrs) == 0 && len(unspent) == 0 {
|
|
// TODO: It would be ideal if on initial sync wallet saved the
|
|
// last several recent blocks rather than just one. This would
|
|
// avoid a full rescan for a one block reorg of the current
|
|
// chain tip.
|
|
hash, height, err := chainClient.GetBestBlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return w.Manager.SetSyncedTo(&waddrmgr.BlockStamp{
|
|
Hash: *hash,
|
|
Height: height,
|
|
})
|
|
}
|
|
|
|
// Compare previously-seen blocks against the chain server. If any of
|
|
// these blocks no longer exist, rollback all of the missing blocks
|
|
// before catching up with the rescan.
|
|
iter := w.Manager.NewIterateRecentBlocks()
|
|
rollback := iter == nil
|
|
syncBlock := waddrmgr.BlockStamp{
|
|
Hash: *w.ChainParams().GenesisHash,
|
|
Height: 0,
|
|
}
|
|
for cont := iter != nil; cont; cont = iter.Prev() {
|
|
bs := iter.BlockStamp()
|
|
log.Debugf("Checking for previous saved block with height %v hash %v",
|
|
bs.Height, bs.Hash)
|
|
_, err = chainClient.GetBlock(&bs.Hash)
|
|
if err != nil {
|
|
rollback = true
|
|
continue
|
|
}
|
|
|
|
log.Debug("Found matching block.")
|
|
syncBlock = bs
|
|
break
|
|
}
|
|
if rollback {
|
|
err = w.Manager.SetSyncedTo(&syncBlock)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Rollback unconfirms transactions at and beyond the passed
|
|
// height, so add one to the new synced-to height to prevent
|
|
// unconfirming txs from the synced-to block.
|
|
err = w.TxStore.Rollback(syncBlock.Height + 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return s.initialRescan(addrs, unspent, w.Manager.SyncedTo()) */
|
|
return nil
|
|
}
|