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  }