github.com/decred/dcrlnd@v0.7.6/watchtower/lookout/lookout.go (about)

     1  package lookout
     2  
     3  import (
     4  	"sync"
     5  	"sync/atomic"
     6  
     7  	"github.com/decred/dcrd/chaincfg/v3"
     8  	"github.com/decred/dcrd/wire"
     9  	"github.com/decred/dcrlnd/chainntnfs"
    10  	"github.com/decred/dcrlnd/watchtower/blob"
    11  )
    12  
    13  // Config houses the Lookout's required resources to properly fulfill it's duty,
    14  // including block fetching, querying accepted state updates, and construction
    15  // and publication of justice transactions.
    16  type Config struct {
    17  	// DB provides persistent access to the watchtower's accepted state
    18  	// updates such that they can be queried as new blocks arrive from the
    19  	// network.
    20  	DB DB
    21  
    22  	// EpochRegistrar supports the ability to register for events corresponding to
    23  	// newly created blocks.
    24  	EpochRegistrar EpochRegistrar
    25  
    26  	// BlockFetcher supports the ability to fetch blocks from the backend or
    27  	// network.
    28  	BlockFetcher BlockFetcher
    29  
    30  	// Punisher handles the responsibility of crafting and broadcasting
    31  	// justice transaction for any breached transactions.
    32  	Punisher Punisher
    33  
    34  	// NetParams specifies the chain parameters for the chain the lookout is
    35  	// associated with.
    36  	NetParams *chaincfg.Params
    37  }
    38  
    39  // Lookout will check any incoming blocks against the transactions found in the
    40  // database, and in case of matches send the information needed to create a
    41  // penalty transaction to the punisher.
    42  type Lookout struct {
    43  	started  int32 // atomic
    44  	shutdown int32 // atomic
    45  
    46  	cfg *Config
    47  
    48  	wg   sync.WaitGroup
    49  	quit chan struct{}
    50  }
    51  
    52  // New constructs a new Lookout from the given LookoutConfig.
    53  func New(cfg *Config) *Lookout {
    54  	return &Lookout{
    55  		cfg:  cfg,
    56  		quit: make(chan struct{}),
    57  	}
    58  }
    59  
    60  // Start safely spins up the Lookout and begins monitoring for breaches.
    61  func (l *Lookout) Start() error {
    62  	if !atomic.CompareAndSwapInt32(&l.started, 0, 1) {
    63  		return nil
    64  	}
    65  
    66  	log.Infof("Starting lookout")
    67  
    68  	startEpoch, err := l.cfg.DB.GetLookoutTip()
    69  	if err != nil {
    70  		return err
    71  	}
    72  
    73  	if startEpoch == nil {
    74  		log.Infof("Starting lookout from chain tip")
    75  	} else {
    76  		log.Infof("Starting lookout from epoch(height=%d hash=%v)",
    77  			startEpoch.Height, startEpoch.Hash)
    78  	}
    79  
    80  	events, err := l.cfg.EpochRegistrar.RegisterBlockEpochNtfn(startEpoch)
    81  	if err != nil {
    82  		log.Errorf("Unable to register for block epochs: %v", err)
    83  		return err
    84  	}
    85  
    86  	l.wg.Add(1)
    87  	go l.watchBlocks(events)
    88  
    89  	log.Infof("Lookout started successfully")
    90  
    91  	return nil
    92  }
    93  
    94  // Stop safely shuts down the Lookout.
    95  func (l *Lookout) Stop() error {
    96  	if !atomic.CompareAndSwapInt32(&l.shutdown, 0, 1) {
    97  		return nil
    98  	}
    99  
   100  	log.Infof("Stopping lookout")
   101  
   102  	close(l.quit)
   103  	l.wg.Wait()
   104  
   105  	log.Infof("Lookout stopped successfully")
   106  
   107  	return nil
   108  }
   109  
   110  // watchBlocks serially pulls incoming epochs from the epoch source and searches
   111  // our accepted state updates for any breached transactions. If any are found,
   112  // we will attempt to decrypt the state updates' encrypted blobs and exact
   113  // justice for the victim.
   114  //
   115  // This method MUST be run as a goroutine.
   116  func (l *Lookout) watchBlocks(epochs *chainntnfs.BlockEpochEvent) {
   117  	defer l.wg.Done()
   118  	defer epochs.Cancel()
   119  
   120  	for {
   121  		select {
   122  		case epoch := <-epochs.Epochs:
   123  			log.Debugf("Fetching block for (height=%d, hash=%s)",
   124  				epoch.Height, epoch.Hash)
   125  
   126  			// Fetch the full block from the backend corresponding
   127  			// to the newly arriving epoch.
   128  			block, err := l.cfg.BlockFetcher.GetBlock(epoch.Hash)
   129  			if err != nil {
   130  				// TODO(conner): add retry logic?
   131  				log.Errorf("Unable to fetch block for "+
   132  					"(height=%x, hash=%s): %v",
   133  					epoch.Height, epoch.Hash, err)
   134  				continue
   135  			}
   136  
   137  			// Process the block to see if it contains any breaches
   138  			// that we are monitoring on behalf of our clients.
   139  			err = l.processEpoch(epoch, block)
   140  			if err != nil {
   141  				log.Errorf("Unable to process %v: %v",
   142  					epoch, err)
   143  			}
   144  
   145  		case <-l.quit:
   146  			return
   147  		}
   148  	}
   149  }
   150  
   151  // processEpoch accepts an Epoch and queries the database for any matching state
   152  // updates for the confirmed transactions. If any are found, the lookout
   153  // responds by attempting to decrypt the encrypted blob and publishing the
   154  // justice transaction.
   155  func (l *Lookout) processEpoch(epoch *chainntnfs.BlockEpoch,
   156  	block *wire.MsgBlock) error {
   157  
   158  	numTxnsInBlock := len(block.Transactions)
   159  
   160  	log.Debugf("Scanning %d transaction in block (height=%d, hash=%s) "+
   161  		"for breaches", numTxnsInBlock, epoch.Height, epoch.Hash)
   162  
   163  	// Iterate over the transactions contained in the block, deriving a
   164  	// breach hint for each transaction and constructing an index mapping
   165  	// the hint back to it's original transaction.
   166  	hintToTx := make(map[blob.BreachHint]*wire.MsgTx, numTxnsInBlock)
   167  	txHints := make([]blob.BreachHint, 0, numTxnsInBlock)
   168  	for _, tx := range block.Transactions {
   169  		hash := tx.TxHash()
   170  		hint := blob.NewBreachHintFromHash(&hash)
   171  
   172  		txHints = append(txHints, hint)
   173  		hintToTx[hint] = tx
   174  	}
   175  
   176  	// Query the database to see if any of the breach hints cause a match
   177  	// with any of our accepted state updates.
   178  	matches, err := l.cfg.DB.QueryMatches(txHints)
   179  	if err != nil {
   180  		return err
   181  	}
   182  
   183  	// No matches were found, we are done.
   184  	if len(matches) == 0 {
   185  		log.Debugf("No breaches found in (height=%d, hash=%s)",
   186  			epoch.Height, epoch.Hash)
   187  		return nil
   188  	}
   189  
   190  	breachCountStr := "breach"
   191  	if len(matches) > 1 {
   192  		breachCountStr = "breaches"
   193  	}
   194  
   195  	log.Infof("Found %d %s in (height=%d, hash=%s)",
   196  		len(matches), breachCountStr, epoch.Height, epoch.Hash)
   197  
   198  	// For each match, use our index to retrieve the original transaction,
   199  	// which corresponds to the breaching commitment transaction. If the
   200  	// decryption succeeds, we will accumlate the assembled justice
   201  	// descriptors in a single slice
   202  	successes := make([]*JusticeDescriptor, 0, len(matches))
   203  	for _, match := range matches {
   204  		commitTx := hintToTx[match.Hint]
   205  		log.Infof("Dispatching punisher for client %s, breach-txid=%s",
   206  			match.ID, commitTx.TxHash())
   207  
   208  		// The decryption key for the state update should be the full
   209  		// txid of the breaching commitment transaction.
   210  		// The decryption key for the state update should be computed as
   211  		//   key = SHA256(txid).
   212  		breachTxID := commitTx.TxHash()
   213  		breachKey := blob.NewBreachKeyFromHash(&breachTxID)
   214  
   215  		// Now, decrypt the blob of justice that we received in the
   216  		// state update. This will contain all information required to
   217  		// sweep the breached commitment outputs.
   218  		justiceKit, err := blob.Decrypt(
   219  			breachKey, match.EncryptedBlob,
   220  			match.SessionInfo.Policy.BlobType,
   221  		)
   222  		if err != nil {
   223  			// If the decryption fails, this implies either that the
   224  			// client sent an invalid blob, or that the breach hint
   225  			// caused a match on the txid, but this isn't actually
   226  			// the right transaction.
   227  			log.Debugf("Unable to decrypt blob for client %s, "+
   228  				"breach-txid %s: %v", match.ID,
   229  				commitTx.TxHash(), err)
   230  			continue
   231  		}
   232  
   233  		justiceDesc := &JusticeDescriptor{
   234  			BreachedCommitTx: commitTx,
   235  			SessionInfo:      match.SessionInfo,
   236  			JusticeKit:       justiceKit,
   237  			NetParams:        l.cfg.NetParams,
   238  		}
   239  		successes = append(successes, justiceDesc)
   240  	}
   241  
   242  	// TODO(conner): mark successfully decrypted blob so that we can
   243  	// reliably rebroadcast on startup
   244  
   245  	// Now, we'll dispatch a punishment for each successful match in
   246  	// parallel. This will assemble the justice transaction for each and
   247  	// watch for their confirmation on chain.
   248  	for _, justiceDesc := range successes {
   249  		l.wg.Add(1)
   250  		go l.dispatchPunisher(justiceDesc)
   251  	}
   252  
   253  	return l.cfg.DB.SetLookoutTip(epoch)
   254  }
   255  
   256  // dispatchPunisher accepts a justice descriptor corresponding to a successfully
   257  // decrypted blob.  The punisher will then construct the witness scripts and
   258  // witness stacks for the breached outputs. If construction of the justice
   259  // transaction is successful, it will be published to the network to retrieve
   260  // the funds and claim the watchtower's reward.
   261  //
   262  // This method MUST be run as a goroutine.
   263  func (l *Lookout) dispatchPunisher(desc *JusticeDescriptor) {
   264  	defer l.wg.Done()
   265  
   266  	// Give the justice descriptor to the punisher to construct and publish
   267  	// the justice transaction. The lookout's quit channel is provided so
   268  	// that long-running tasks that watch for on-chain events can be
   269  	// canceled during shutdown since this method is waitgrouped.
   270  	err := l.cfg.Punisher.Punish(desc, l.quit)
   271  	if err != nil {
   272  		log.Errorf("Unable to punish breach-txid %s for %s: %v",
   273  			desc.BreachedCommitTx.TxHash(), desc.SessionInfo.ID,
   274  			err)
   275  		return
   276  	}
   277  
   278  	log.Infof("Punishment for client %s with breach-txid=%s dispatched",
   279  		desc.SessionInfo.ID, desc.BreachedCommitTx.TxHash())
   280  }