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 }