github.com/mit-dci/lit@v0.0.0-20221102210550-8c3d3b49f2ce/watchtower/watchdb.go (about) 1 package watchtower 2 3 import ( 4 "fmt" 5 6 "github.com/mit-dci/lit/logging" 7 8 "github.com/mit-dci/lit/btcutil/chaincfg/chainhash" 9 "github.com/mit-dci/lit/elkrem" 10 "github.com/mit-dci/lit/lnutil" 11 "github.com/mit-dci/lit/wire" 12 13 "github.com/boltdb/bolt" 14 ) 15 16 /* 17 WatchDB has 3 top level buckets -- 2 small ones and one big one. 18 (also could write it so that the big one is a different file or different machine) 19 20 PKHMapBucket is k:v 21 localChannelId : PKH 22 23 ChannelBucket is full of PKH sub-buckets 24 PKH (lots) 25 | 26 |-KEYElkRcv : Serialized elkrem receiver (couple KB) 27 | 28 |-KEYIdx : channelIdx (4 bytes) 29 | 30 |-KEYStatic : ChanStatic (~100 bytes) 31 32 33 (could also add some metrics, like last write timestamp) 34 35 the big one: 36 37 TxidBucket is k:v 38 Txid[:16] : IdxSig (74 bytes) 39 40 TODO: both ComMsgs and IdxSigs need to support multiple signatures for HTLCs. 41 What's nice is that this is the *only* thing needed to support HTLCs. 42 43 44 Potential optimizations to try: 45 Store less than 16 bytes of the txid 46 Store 47 48 Leave as is for now, but could modify the txid to make it smaller. Could 49 HMAC it with a local key to prevent collision attacks and get the txid size down 50 to 8 bytes or so. An issue is then you can't re-export the states to other nodes. 51 Only reduces size by 24 bytes, or about 20%. Hm. Try this later. 52 53 ... actually the more I think about it, this is an easy win. 54 Also collision attacks seem ineffective; even random false positives would 55 be no big deal, just a couple ms of CPU to compute the grab tx and see that 56 it doesn't match. 57 58 Yeah can crunch down to 8 bytes, and have the value be 2+ idxSig structs. 59 In the rare cases where there's a collision, generate both scripts and check. 60 Quick to check. 61 62 To save another couple bytes could make the idx in the idxsig varints. 63 Only a 3% savings and kindof annoying so will leave that for now. 64 65 */ 66 67 var ( 68 BUCKETPKHMap = []byte("pkm") // bucket for idx:pkh mapping 69 BUCKETChandata = []byte("cda") // bucket for channel data (elks, points) 70 BUCKETTxid = []byte("txi") // big bucket with every txid 71 72 KEYStatic = []byte("sta") // static per channel data as value 73 KEYElkRcv = []byte("elk") // elkrem receiver 74 KEYIdx = []byte("idx") // index mapping 75 ) 76 77 // Opens the DB file for the LnNode 78 func (w *WatchTower) OpenDB(filepath string) error { 79 var err error 80 81 w.WatchDB, err = bolt.Open(filepath, 0644, nil) 82 if err != nil { 83 return err 84 } 85 // create buckets if they're not already there 86 err = w.WatchDB.Update(func(btx *bolt.Tx) error { 87 _, err := btx.CreateBucketIfNotExists(BUCKETPKHMap) 88 if err != nil { 89 return err 90 } 91 _, err = btx.CreateBucketIfNotExists(BUCKETChandata) 92 if err != nil { 93 return err 94 } 95 txidBkt, err := btx.CreateBucketIfNotExists(BUCKETTxid) 96 if err != nil { 97 return err 98 } 99 // if there are txids in the bucket, set watching to true 100 if txidBkt.Stats().KeyN != 0 { 101 w.Watching = true 102 } 103 return nil 104 }) 105 if err != nil { 106 return err 107 } 108 return nil 109 } 110 111 // AddNewChannel puts a new channel into the watchtower db. 112 // Probably need some way to prevent overwrites. 113 func (w *WatchTower) NewChannel(m lnutil.WatchDescMsg) error { 114 115 // exit if we didn't enable watchtower. 116 if w.WatchDB == nil { 117 fmt.Println("Node sending info thinking we are a watchtower, when we aren't") 118 return fmt.Errorf("Not a watchtower, can't keep track.") 119 } 120 121 // quick check if we support the cointype 122 _, ok := w.Hooks[m.CoinType] 123 if !ok { 124 return fmt.Errorf("Cointype %d not supported", m.CoinType) 125 } 126 127 // TODO change it so the user first requests supported cointypes, 128 // then sends the DescMsg without indicating cointype 129 130 return w.WatchDB.Update(func(btx *bolt.Tx) error { 131 // open index : pkh mapping bucket 132 mapBucket := btx.Bucket(BUCKETPKHMap) 133 if mapBucket == nil { 134 return fmt.Errorf("no PKHmap bucket") 135 } 136 // figure out this new channel's index 137 // 4B channels forever... could fix, but probably enough. 138 var newIdx uint32 139 cur := mapBucket.Cursor() 140 k, _ := cur.Last() // go to the end 141 if k != nil { 142 newIdx = lnutil.BtU32(k) + 1 // and add 1 143 } 144 logging.Infof("assigning new channel index %d\n", newIdx) 145 newIdxBytes := lnutil.U32tB(newIdx) 146 147 allChanbkt := btx.Bucket(BUCKETChandata) 148 if allChanbkt == nil { 149 return fmt.Errorf("no Chandata bucket") 150 } 151 // make new channel bucket 152 chanBucket, err := allChanbkt.CreateBucket(m.DestPKHScript[:]) 153 if err != nil { 154 return err 155 } 156 // save truncated descriptor for static info (drop elk0) 157 wdBytes := m.Bytes() 158 if len(wdBytes) < 96 { 159 return fmt.Errorf("watchdescriptor %d bytes, expect 96", len(wdBytes)) 160 } 161 chanBucket.Put(KEYStatic, wdBytes[:96]) 162 logging.Infof("saved new channel to pkh %x\n", m.DestPKHScript) 163 // save index 164 err = chanBucket.Put(KEYIdx, newIdxBytes) 165 if err != nil { 166 return err 167 } 168 // even though we haven't actually added anything to watch for, 169 // we're pretty sure there will be soon; the watch tower is "on" at this 170 // point so assert "watching". 171 w.Watching = true 172 173 // save into index mapping 174 return mapBucket.Put(newIdxBytes, m.DestPKHScript[:]) 175 // done 176 }) 177 } 178 179 // AddMsg adds a new message describing a penalty tx to the db. 180 // optimization would be to add a bunch of messages at once. Not a huge speedup though. 181 func (w *WatchTower) UpdateChannel(m lnutil.WatchStateMsg) error { 182 183 if w.WatchDB == nil { 184 fmt.Println("Node sending info thinking we are a watchtower, when we aren't") 185 return fmt.Errorf("Not a watchtower, can't keep track.") 186 } 187 188 return w.WatchDB.Update(func(btx *bolt.Tx) error { 189 190 // first get the channel bucket, update the elkrem and read the idx 191 allChanbkt := btx.Bucket(BUCKETChandata) 192 if allChanbkt == nil { 193 return fmt.Errorf("no Chandata bucket") 194 } 195 chanBucket := allChanbkt.Bucket(m.DestPKH[:]) 196 if chanBucket == nil { 197 return fmt.Errorf("no bucket for channel %x", m.DestPKH) 198 } 199 200 // deserialize elkrems. Future optimization: could keep 201 // all elkrem receivers in RAM for every channel, only writing here 202 // each time instead of reading then writing back. 203 elkr, err := elkrem.ElkremReceiverFromBytes(chanBucket.Get(KEYElkRcv)) 204 if err != nil { 205 return err 206 } 207 // add next elkrem hash. Should work. If it fails...? 208 err = elkr.AddNext(&m.Elk) 209 if err != nil { 210 return err 211 } 212 // logging.Infof("added elkrem %x at index %d OK\n", cm.Elk[:], elkr.UpTo()) 213 214 // get state number, after elk insertion. also convert to 8 bytes. 215 stateNumBytes := lnutil.U64tB(elkr.UpTo()) 216 // worked, so save it back. First serialize 217 elkBytes, err := elkr.ToBytes() 218 if err != nil { 219 return err 220 } 221 // then write back to DB. 222 err = chanBucket.Put(KEYElkRcv, elkBytes) 223 if err != nil { 224 return err 225 } 226 // get local index of this channel 227 cIdxBytes := chanBucket.Get(KEYIdx) 228 if cIdxBytes == nil { 229 return fmt.Errorf("channel %x has no index", m.DestPKH) 230 } 231 232 // we've updated the elkrem and saved it, so done with channel bucket. 233 // next go to txid bucket to save 234 235 txidbkt := btx.Bucket(BUCKETTxid) 236 if txidbkt == nil { 237 return fmt.Errorf("no txid bucket") 238 } 239 // create the sigIdx 74 bytes. A little ugly but only called here and 240 // pretty quick. Maybe make a function for this. 241 sigIdxBytes := make([]byte, 74) 242 copy(sigIdxBytes[:4], cIdxBytes) // first 4 bytes is the PKH index 243 copy(sigIdxBytes[4:10], stateNumBytes[2:]) // next 6 is state number 244 copy(sigIdxBytes[10:], m.Sig[:]) // the rest is signature 245 246 logging.Infof("chan %x (pkh %x) up to state %x\n", 247 cIdxBytes, m.DestPKH, stateNumBytes) 248 // save sigIdx into the txid bucket. 249 // TODO truncate txid, and deal with collisions. 250 return txidbkt.Put(m.ParTxid[:16], sigIdxBytes) 251 }) 252 } 253 254 // TODO implement DeleteChannel. Would be nice to delete old channels. 255 func (w *WatchTower) DeleteChannel(m lnutil.WatchDelMsg) error { 256 257 if w.WatchDB == nil { 258 fmt.Println("Node sending info thinking we are a watchtower, when we aren't") 259 return fmt.Errorf("Not a watchtower, can't keep track.") 260 } 261 return nil 262 } 263 264 // MatchTxid takes in a txid, checks against the DB, and if there's a hit, returns a 265 // IdxSig with which to make a JusticeTx. Hits should be rare. 266 func (w *WatchTower) MatchTxids( 267 cointype uint32, txids []chainhash.Hash) ([]chainhash.Hash, error) { 268 269 var err error 270 var hits []chainhash.Hash 271 272 err = w.WatchDB.View(func(btx *bolt.Tx) error { 273 // open the big bucket 274 txidbkt := btx.Bucket(BUCKETTxid) 275 if txidbkt == nil { 276 return fmt.Errorf("no txid bucket") 277 } 278 279 for i, txid := range txids { 280 if i == 0 { 281 // coinbase tx cannot be a bad tx 282 continue 283 } 284 b := txidbkt.Get(txid[:16]) 285 if b != nil { 286 logging.Infof("zomg hit %s\n", txid.String()) 287 hits = append(hits, txid) 288 } 289 } 290 return nil 291 }) 292 return hits, err 293 } 294 295 func (w *WatchTower) BlockHandler( 296 cointype uint32, bchan chan *wire.MsgBlock) { 297 298 logging.Infof("-- started BlockHandler type %d, block channel cap %d\n", 299 cointype, cap(bchan)) 300 301 for { 302 // block here, take in blocks 303 block := <-bchan 304 305 logging.Infof("tower check block %s %d txs\n", 306 block.BlockHash().String(), len(block.Transactions)) 307 308 // get all txids from the blocks 309 txids, err := block.TxHashes() 310 if err != nil { 311 logging.Errorf("BlockHandler/TxHashes error: %s", err.Error()) 312 } 313 314 // see if there are any hits from all the txids 315 // usually there aren't any so we can finish here 316 hits, err := w.MatchTxids(cointype, txids) 317 if err != nil { 318 logging.Errorf("BlockHandler/MatchTxids error: %s", err.Error()) 319 } 320 321 // if there were hits, need to build justice txs and send out 322 if len(hits) > 0 { 323 for _, hitTxid := range hits { 324 logging.Infof("zomg tx %s matched db\n", hitTxid.String()) 325 for _, tx := range block.Transactions { 326 // inefficient here, iterating through whole block. 327 // probably OK because this rarely hapens 328 curTxid := tx.TxHash() 329 if curTxid.IsEqual(&hitTxid) { 330 justice, err := w.BuildJusticeTx(cointype, tx) 331 if err != nil { 332 logging.Errorf("BuildJusticeTx error: %s", err.Error()) 333 } 334 logging.Infof("made & sent out justice tx %s\n", 335 justice.TxHash().String()) 336 err = w.Hooks[cointype].PushTx(justice) 337 if err != nil { 338 logging.Errorf("BuildJusticeTx error: %s", err.Error()) 339 } 340 } 341 } 342 } 343 } 344 } // end of indefinite for 345 346 // never returns 347 } 348 349 // Status returns a string describing what's in the watchtower. 350 /* 351 func (w *WatchTower) Status() (string, error) { 352 var err error 353 var s string 354 355 err = w.WatchDB.View(func(btx *bolt.Tx) error { 356 // open the big bucket 357 txidbkt := btx.Bucket(BUCKETTxid) 358 if txidbkt == nil { 359 return fmt.Errorf("no txid bucket") 360 } 361 362 return txidbkt.ForEach(func(txid, idxsig []byte) error { 363 s += fmt.Sprintf("\txid %x\t idxsig: %x\n", txid, idxsig) 364 return nil 365 }) 366 return nil 367 }) 368 return s, err 369 } 370 */