github.com/mit-dci/lit@v0.0.0-20221102210550-8c3d3b49f2ce/wallit/dbio.go (about) 1 package wallit 2 3 import ( 4 "bytes" 5 "encoding/binary" 6 "fmt" 7 8 "github.com/mit-dci/lit/logging" 9 10 "github.com/boltdb/bolt" 11 "github.com/mit-dci/lit/btcutil" 12 "github.com/mit-dci/lit/btcutil/blockchain" 13 "github.com/mit-dci/lit/btcutil/chaincfg/chainhash" 14 "github.com/mit-dci/lit/consts" 15 "github.com/mit-dci/lit/lnutil" 16 "github.com/mit-dci/lit/portxo" 17 "github.com/mit-dci/lit/wire" 18 ) 19 20 // const strings for db usage 21 var ( 22 // storage of all utxos. top level is outpoints. 23 BKToutpoint = []byte("DuffelBag") 24 // storage of all addresses being watched. top level is pkscripts 25 BKTadr = []byte("adr") 26 27 BKTStxos = []byte("SpentTxs") // for bookkeeping / not sure 28 BKTTxns = []byte("Txns") // all txs we care about, for replays 29 BKTState = []byte("MiscState") // misc states of DB 30 31 // BKTWatch = []byte("watch") // outpoints we're watching for someone else 32 // these are in the state bucket 33 KEYNumKeys = []byte("NumKeys") // number of p2pkh keys used 34 35 KEYTipHeight = []byte("TipHeight") // height synced to 36 ) 37 38 // make a new change output. I guess this is supposed to be on a different 39 // branch than regular addresses... 40 func (w *Wallit) NewChangeOut(amt int64) (*wire.TxOut, error) { 41 change160, err := w.NewAdr160() // change is always witnessy 42 if err != nil { 43 return nil, err 44 } 45 46 changeScript := lnutil.DirectWPKHScriptFromPKH(change160) 47 48 changeOut := wire.NewTxOut(amt, changeScript) 49 return changeOut, nil 50 } 51 52 // AddPorTxoAdr adds an externally sourced address to the db. Looks at the keygen 53 // to derive hash160. 54 func (w *Wallit) AddPorTxoAdr(kg portxo.KeyGen) error { 55 // write to db file 56 return w.StateDB.Update(func(btx *bolt.Tx) error { 57 adrb := btx.Bucket(BKTadr) 58 if adrb == nil { 59 return fmt.Errorf("no adr bucket") 60 } 61 62 adr160 := w.PathPubHash160(kg) 63 logging.Infof("adding addr %x\n", adr160) 64 // add the 20-byte key-hash into the db 65 return adrb.Put(adr160[:], kg.Bytes()) 66 }) 67 } 68 69 // AdrDump returns all the addresses in the wallit. 70 // currently returns 20 byte arrays, which 71 // can then be converted somewhere else into bech32 addresses (or old base58) 72 func (w *Wallit) AdrDump() ([][20]byte, error) { 73 var i, last uint32 // number of addresses made so far 74 var adrSlice [][20]byte 75 76 err := w.StateDB.View(func(btx *bolt.Tx) error { 77 sta := btx.Bucket(BKTState) 78 if sta == nil { 79 return fmt.Errorf("no state bucket") 80 } 81 82 oldNBytes := sta.Get(KEYNumKeys) 83 last = lnutil.BtU32(oldNBytes) 84 // update the db with number of created keys 85 return nil 86 }) 87 if err != nil { 88 return nil, err 89 } 90 91 if last > consts.MaxKeys { 92 return nil, fmt.Errorf("Got %d keys stored, expect something reasonable", last) 93 } 94 95 // TODO: maybe store address hashes instead of recomputing them 96 // can speed things up a lot here, at a pretty small disk cost 97 for i = 0; i < last; i++ { 98 nKg := GetWalletKeygen(i, w.Param.HDCoinType) 99 nAdr160 := w.PathPubHash160(nKg) 100 101 adrSlice = append(adrSlice, nAdr160) 102 } 103 return adrSlice, nil 104 } 105 106 // NewAdr creates a new, never before seen address, and increments the 107 // DB counter, and returns the hash160 of the pubkey. 108 func (w *Wallit) NewAdr160() ([20]byte, error) { 109 var err error 110 var empty160 [20]byte 111 if w.Param == nil { 112 return empty160, fmt.Errorf("NewAdr error: nil param") 113 } 114 115 var n uint32 // number of addresses made so far 116 117 err = w.StateDB.View(func(btx *bolt.Tx) error { 118 sta := btx.Bucket(BKTState) 119 if sta == nil { 120 return fmt.Errorf("no state bucket") 121 } 122 123 oldNBytes := sta.Get(KEYNumKeys) 124 n = lnutil.BtU32(oldNBytes) 125 // update the db with number of created keys 126 return nil 127 }) 128 if n > consts.MaxKeyLimit { 129 return empty160, fmt.Errorf("Got %d keys stored, expect something reasonable", n) 130 } 131 132 nKg := GetWalletKeygen(n, w.Param.HDCoinType) 133 nAdr160 := w.PathPubHash160(nKg) 134 135 if nAdr160 == empty160 { 136 return empty160, fmt.Errorf("NewAdr error: got nil h160") 137 } 138 logging.Infof("adr %d hash is %x\n", n, nAdr160) 139 140 kgBytes := nKg.Bytes() 141 142 // total number of keys (now +1) into 4 bytes 143 nKeyNumBytes := lnutil.U32tB(n + 1) 144 145 // write to db file 146 err = w.StateDB.Update(func(btx *bolt.Tx) error { 147 adrb := btx.Bucket(BKTadr) 148 if adrb == nil { 149 return fmt.Errorf("no adr bucket") 150 } 151 sta := btx.Bucket(BKTState) 152 if sta == nil { 153 return fmt.Errorf("no state bucket") 154 } 155 156 // add the 20-byte key-hash into the db 157 err = adrb.Put(nAdr160[:], kgBytes) 158 if err != nil { 159 return err 160 } 161 162 // update the db with number of created keys 163 return sta.Put(KEYNumKeys, nKeyNumBytes) 164 }) 165 if err != nil { 166 return empty160, err 167 } 168 169 err = w.Hook.RegisterAddress(nAdr160) 170 if err != nil { 171 return empty160, err 172 } 173 174 return nAdr160, nil 175 } 176 177 // SetDBSyncHeight sets sync height of the db, indicated the latest block 178 // of which it has ingested all the transactions. 179 func (w *Wallit) SetDBSyncHeight(n int32) error { 180 var buf bytes.Buffer 181 _ = binary.Write(&buf, binary.BigEndian, n) 182 183 return w.StateDB.Update(func(btx *bolt.Tx) error { 184 sta := btx.Bucket(BKTState) 185 return sta.Put(KEYTipHeight, buf.Bytes()) 186 }) 187 } 188 189 // SyncHeight returns the chain height to which the db has synced 190 func (w *Wallit) GetDBSyncHeight() (int32, error) { 191 var n int32 192 err := w.StateDB.View(func(btx *bolt.Tx) error { 193 sta := btx.Bucket(BKTState) 194 if sta == nil { 195 return fmt.Errorf("no state") 196 } 197 t := sta.Get(KEYTipHeight) 198 199 if t == nil { // no height written, so 0 200 return nil 201 } 202 203 // read 4 byte tip height to n 204 err := binary.Read(bytes.NewBuffer(t), binary.BigEndian, &n) 205 if err != nil { 206 return err 207 } 208 return nil 209 }) 210 if err != nil { 211 return 0, err 212 } 213 return n, nil 214 } 215 216 // SaveTx unconditionally saves a tx in the DB, usually for sending out to nodes 217 func (w *Wallit) SaveTx(tx *wire.MsgTx) error { 218 // open db 219 return w.StateDB.Update(func(btx *bolt.Tx) error { 220 // get the outpoint watch bucket 221 txbkt := btx.Bucket(BKTTxns) 222 if txbkt == nil { 223 return fmt.Errorf("tx bucket not in db") 224 } 225 var buf bytes.Buffer 226 tx.Serialize(&buf) 227 txid := tx.TxHash() 228 return txbkt.Put(txid[:], buf.Bytes()) 229 }) 230 } 231 232 func (w *Wallit) UtxoDump() ([]*portxo.PorTxo, error) { 233 return w.GetAllUtxos() 234 } 235 236 // GetAllUtxos returns a slice of all portxos in the db. empty slice is OK. 237 // Doesn't return watch only outpoints 238 func (w *Wallit) GetAllUtxos() ([]*portxo.PorTxo, error) { 239 var utxos []*portxo.PorTxo 240 err := w.StateDB.View(func(btx *bolt.Tx) error { 241 dufb := btx.Bucket(BKToutpoint) 242 if dufb == nil { 243 return fmt.Errorf("no duffel bag") 244 } 245 return dufb.ForEach(func(k, v []byte) error { 246 247 // 0 len v means it's a watch-only utxo, not spendable 248 if len(v) == 0 { 249 // logging.Infof("not nil, 0 len slice\n") 250 return nil 251 } 252 253 // have to copy k and v here, otherwise append will crash it. 254 // not quite sure why but append does weird stuff I guess. 255 // create a new utxo 256 x := make([]byte, len(k)+len(v)) 257 copy(x, k) 258 copy(x[len(k):], v) 259 newU, err := portxo.PorTxoFromBytes(x) 260 if err != nil { 261 return err 262 } 263 // and add it to ram 264 utxos = append(utxos, newU) 265 return nil 266 }) 267 }) 268 if err != nil { 269 return nil, err 270 } 271 return utxos, nil 272 } 273 274 // RegisterWatchOP registers an outpoint to watch. Called from ReallySend() 275 func (w *Wallit) RegisterWatchOP(op wire.OutPoint) error { 276 opArr := lnutil.OutPointToBytes(op) 277 // open db 278 return w.StateDB.Update(func(btx *bolt.Tx) error { 279 // get the outpoint watch bucket 280 dufb := btx.Bucket(BKToutpoint) 281 if dufb == nil { 282 return fmt.Errorf("watch bucket not in db") 283 } 284 return dufb.Put(opArr[:], nil) 285 }) 286 } 287 288 // UnregisterWatchOP unregisters an outpoint to watch. Used to remove watched HTLC OPs if we claim them ourselves. 289 func (w *Wallit) UnregisterWatchOP(op wire.OutPoint) error { 290 opArr := lnutil.OutPointToBytes(op) 291 // open db 292 return w.StateDB.Update(func(btx *bolt.Tx) error { 293 // get the outpoint watch bucket 294 dufb := btx.Bucket(BKToutpoint) 295 if dufb == nil { 296 return fmt.Errorf("watch bucket not in db") 297 } 298 return dufb.Delete(opArr[:]) 299 }) 300 } 301 302 // GainUtxo registers the utxo in the duffel bag 303 // don't register address; they shouldn't be re-used ever anyway. 304 func (w *Wallit) GainUtxo(u portxo.PorTxo) error { 305 logging.Infof("gaining exported utxo %s at height %d\n", 306 u.Op.String(), u.Height) 307 // serialize porTxo 308 utxoBytes, err := u.Bytes() 309 if err != nil { 310 return err 311 } 312 313 // open db 314 return w.StateDB.Update(func(btx *bolt.Tx) error { 315 // get the outpoint watch bucket 316 dufb := btx.Bucket(BKToutpoint) 317 if dufb == nil { 318 return fmt.Errorf("duffel bag not in db") 319 } 320 321 // add utxo itself 322 return dufb.Put(utxoBytes[:36], utxoBytes[36:]) 323 }) 324 } 325 326 func NewPorTxo(tx *wire.MsgTx, idx uint32, height int32, 327 kg portxo.KeyGen) (*portxo.PorTxo, error) { 328 // extract base portxo from tx 329 ptxo, err := portxo.ExtractFromTx(tx, idx) 330 if err != nil { 331 return nil, err 332 } 333 334 ptxo.Height = height 335 ptxo.KeyGen = kg 336 337 return ptxo, nil 338 } 339 340 // NewPorTxoBytesFromKGBytes just calls NewPorTxo() and de/serializes 341 // quick shortcut for use in the ingest() function 342 func NewPorTxoBytesFromKGBytes( 343 tx *wire.MsgTx, idx uint32, height int32, kgb []byte) ([]byte, error) { 344 345 if len(kgb) != 53 { 346 return nil, fmt.Errorf("keygen %d bytes, expect 53", len(kgb)) 347 } 348 349 var kgarr [53]byte 350 copy(kgarr[:], kgb) 351 352 kg := portxo.KeyGenFromBytes(kgarr) 353 354 ptxo, err := NewPorTxo(tx, idx, height, kg) 355 if err != nil { 356 return nil, err 357 } 358 return ptxo.Bytes() 359 } 360 361 // Rollback rewinds the wallet state to a previous height. It removes new UTXOs 362 func (w *Wallit) RollBack(rollHeight int32) error { 363 // Assume this is an actual reord / rewind. If you supply a height *greater* 364 // than the current height, all bets are off. ( probably nothing will 365 // happen; but don't do it) 366 367 // I still don't 100% get how these bolt tx things get encapsulated. 368 return w.StateDB.Update(func(btx *bolt.Tx) error { 369 // range through utxos and remove all above target height 370 logging.Infof("Rollback height %d\n", rollHeight) 371 372 dufb := btx.Bucket(BKToutpoint) 373 374 if dufb == nil { 375 return fmt.Errorf("no duffel bag") 376 } 377 378 // build slice of stuff to delete 379 var killOPs [][]byte 380 381 err := dufb.ForEach(func(k, v []byte) error { 382 var txHeight int32 383 // 0 len v means it's a watch-only utxo, not spendable 384 // we have no way of getting rid of these. Maybe should! 385 if len(v) == 0 { 386 return nil 387 } 388 389 // all we care about is the height, which starts 8 btyes in to the v 390 buf := bytes.NewBuffer(v) 391 392 // drop first 8 bytes (amt) 393 _ = buf.Next(8) 394 395 err := binary.Read(buf, binary.BigEndian, &txHeight) 396 if err != nil { 397 return err 398 } 399 400 logging.Infof("tx height %d\n", txHeight) 401 if txHeight > rollHeight { 402 // need to kill this TX. we could save it somewhere else? 403 // just mark to get rid of it for now. 404 killOPs = append(killOPs, k) 405 } 406 return nil 407 }) 408 if err != nil { 409 return err 410 } 411 412 // now delete em all 413 for _, op := range killOPs { 414 err = dufb.Delete(op) 415 if err != nil { 416 return err 417 } 418 } 419 420 // Don't re-animate old txos at all; just hope that they get back into 421 // blocks, which they probably will. 422 423 // The right way to do this (TODO) would be to make a rebroadcast pool 424 // where if the stored txs above the reorg height aren't re-confirmed, 425 // then it will attempt to rebroadcast them. 426 427 logging.Infof("Rollback db. %d utxos lost\n", len(killOPs)) 428 429 return nil 430 }) 431 } 432 433 // Ingest -- take in a tx from the ChainHook 434 func (w *Wallit) Ingest(tx *wire.MsgTx, height int32) (uint32, error) { 435 return w.IngestMany([]*wire.MsgTx{tx}, height) 436 } 437 438 //TODO !!!!!!!!!!!!!!!111 439 // IngestMany puts txs into the DB atomically. This can result in a 440 // gain, a loss, or no result. 441 442 // This should always work; there shouldn't be false positives getting to here, 443 // as those should be handled on the ChainHook level. 444 // IngestMany can probably work OK even if the txs are out of order. 445 // But don't do that, that's weird and untested. 446 // also it'll error if you give it more than 1M txs, so don't. 447 func (w *Wallit) IngestMany(txs []*wire.MsgTx, height int32) (uint32, error) { 448 var hits uint32 449 var err error 450 451 cachedShas := make([]*chainhash.Hash, len(txs)) // cache every txid 452 hitTxs := make([]bool, len(txs)) // keep track of which txs to store 453 454 // not worth making a struct but these 2 go together 455 456 // spentOPs are all the outpoints being spent by this 457 // batch of txs, serialized into 36 byte arrays 458 spentOPs := make([][36]byte, 0, len(txs)) // at least 1 txin per tx 459 // spendTxIdx tells which tx (in the txs slice) the utxo loss came from 460 spentTxIdx := make([]uint32, 0, len(txs)) 461 462 if len(txs) < 1 || len(txs) > consts.MaxTxLen { 463 return 0, fmt.Errorf("tried to ingest %d txs, expect 1 to %d", len(txs), consts.MaxTxLen) 464 } 465 466 // initial in-ram work on all txs. 467 for i, tx := range txs { 468 // tx has been OK'd by SPV; check tx sanity 469 utilTx := btcutil.NewTx(tx) // convert for validation 470 // checks basic stuff like there are inputs and ouputs 471 err = blockchain.CheckTransactionSanity(utilTx) 472 if err != nil { 473 return hits, err 474 } 475 // cache all txids 476 cachedShas[i] = utilTx.Hash() 477 // before entering into db, serialize all inputs of ingested txs 478 for _, txin := range tx.TxIn { 479 spentOPs = append(spentOPs, lnutil.OutPointToBytes(txin.PreviousOutPoint)) 480 spentTxIdx = append(spentTxIdx, uint32(i)) // save tx it came from 481 } 482 } 483 484 // now do the db write (this is the expensive / slow part) 485 err = w.StateDB.Update(func(btx *bolt.Tx) error { 486 // get all 4 buckets 487 dufb := btx.Bucket(BKToutpoint) 488 adrb := btx.Bucket(BKTadr) 489 old := btx.Bucket(BKTStxos) 490 txns := btx.Bucket(BKTTxns) 491 492 // first gain utxos. 493 // for each txout, see if the pkscript matches something we're watching. 494 for i, tx := range txs { 495 for j, out := range tx.TxOut { 496 // Don't try to Get() a nil. I think? works ok though? 497 keygenBytes := adrb.Get(lnutil.KeyHashFromPkScript(out.PkScript)) 498 if keygenBytes != nil { 499 // address matches something we're watching, cool. 500 // logging.Infof("txout script:%x matched kg: %x\n", out.PkScript, keygenBytes) 501 502 // build new portxo 503 txob, err := NewPorTxoBytesFromKGBytes( 504 tx, uint32(j), height, keygenBytes) 505 if err != nil { 506 return err 507 } 508 509 // Make sure this isn't a duplicate / already been spent 510 // the first 36 bytes of the serialized portxo is the outpoint 511 spendTx := old.Get(txob[:36]) 512 if spendTx != nil { 513 // this outpoint has already been spent 514 continue 515 } 516 517 // if we've never seen this outpoint before, register it 518 // with the chainhook. If we've already seen it (maybe getting 519 // confirmed now) we don't need to re-register. 520 existing := dufb.Get(txob[:36]) 521 if existing == nil { 522 err = w.Hook.RegisterOutPoint(wire.OutPoint{Hash: tx.TxHash(), Index: uint32(j)}) 523 if err != nil { 524 return err 525 } 526 } 527 528 // add hits now though 529 hits++ 530 hitTxs[i] = true 531 err = dufb.Put(txob[:36], txob[36:]) 532 if err != nil { 533 return err 534 } 535 } 536 } 537 } 538 539 // iterate through txids, then outpoint bucket to see if height changes 540 // use seek prefix as we know the txid which could match any outpoint 541 // with that hash (in practice usually just 0, 1, 2) 542 for i, txid := range cachedShas { 543 cur := dufb.Cursor() 544 pre := txid.CloneBytes() 545 // iterate through all outpoints that start with the txid (if any) 546 // k is first 36 bytes of portxo, which is the outpoint. 547 // v is the rest of the portxo data, or nothing if it's watch only 548 for k, v := cur.Seek(pre); bytes.HasPrefix(k, pre); k, v = cur.Next() { 549 // note if v is not empty, we'll get back the exported portxo 550 // a second time, so we don't need to do the detection here. 551 // only do this if OPEventChan has been initialized 552 if len(v) == 0 && cap(w.OPEventChan) != 0 { 553 // confirmation of unknown / watch only outpoint, send up to ln 554 // confirmation match detected; return OP event with nil tx 555 // logging.Infof("|||| zomg match ") 556 hitTxs[i] = true // flag to save tx in db 557 558 var opArr [36]byte 559 copy(opArr[:], k) 560 op := lnutil.OutPointFromBytes(opArr) 561 562 // build new outpoint event 563 var ev lnutil.OutPointEvent 564 ev.Op = *op // assign outpoint 565 ev.Height = height // assign height (may be 0) 566 ev.Tx = nil // doesn't do anything but... for clarity 567 w.OPEventChan <- ev // send into the channel... 568 } 569 } 570 } 571 572 // iterate through spent outpoints, then outpoint bucket and look for matches 573 // this makes us lose money, which is regrettable, but we need to know. 574 // could lose stuff we just gained, that's OK. 575 for i, curOP := range spentOPs { 576 v := dufb.Get(curOP[:]) 577 if v != nil && len(v) == 0 && cap(w.OPEventChan) != 0 { 578 // logging.Infof("|||watch only here zomg\n") 579 hitTxs[spentTxIdx[i]] = true // just save everything 580 op := lnutil.OutPointFromBytes(curOP) 581 // build new outpoint event 582 var ev lnutil.OutPointEvent 583 ev.Op = *op 584 ev.Height = height 585 ev.Tx = txs[spentTxIdx[i]] 586 w.OPEventChan <- ev 587 } 588 if v != nil && len(v) > 0 { 589 hitTxs[spentTxIdx[i]] = true 590 // do all this just to figure out value we lost 591 x := make([]byte, len(curOP)+len(v)) 592 copy(x, curOP[:]) 593 copy(x[len(curOP):], v) 594 lostTxo, err := portxo.PorTxoFromBytes(x) 595 if err != nil { 596 return err 597 } 598 // print lost portxo 599 logging.Infof(lostTxo.String()) 600 601 // after marking for deletion, save stxo to old bucket 602 var st Stxo // generate spent txo 603 st.PorTxo = *lostTxo // assign outpoint 604 st.SpendHeight = height // spent at height 605 st.SpendTxid = *cachedShas[spentTxIdx[i]] // spent by txid 606 stxb, err := st.ToBytes() // serialize 607 if err != nil { 608 return err 609 } 610 // stxos are saved in the DB like portxos, with k:op, v:the rest 611 err = old.Put(stxb[:36], stxb[36:]) // write stxo 612 if err != nil { 613 return err 614 } 615 err = dufb.Delete(curOP[:]) 616 if err != nil { 617 return err 618 } 619 } 620 } 621 622 // save all txs with hits 623 for i, tx := range txs { 624 if hitTxs[i] == true { 625 hits++ 626 var buf bytes.Buffer 627 tx.Serialize(&buf) // always store witness version 628 err = txns.Put(cachedShas[i].CloneBytes(), buf.Bytes()) 629 if err != nil { 630 return err 631 } 632 } 633 } 634 return nil 635 }) 636 637 logging.Infof("ingest %d txs, %d hits\n", len(txs), hits) 638 return hits, err 639 }