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  */