From 2dcdb458e89f0b7b805f1cb6f8b9da877102c91c Mon Sep 17 00:00:00 2001
From: Roy Lee <roylee17@gmail.com>
Date: Sun, 5 Aug 2018 13:59:25 -0700
Subject: [PATCH] [lbry] blockchain: connect to ClaimTrie

Co-authored-by: Brannon King <countprimes@gmail.com>
---
 blockchain/chain.go     |  90 +++++++++++++++++++++++++
 blockchain/claimtrie.go | 144 ++++++++++++++++++++++++++++++++++++++++
 btcd.go                 |   5 ++
 3 files changed, 239 insertions(+)
 create mode 100644 blockchain/claimtrie.go

diff --git a/blockchain/chain.go b/blockchain/chain.go
index b4a871b9..3a00eba5 100644
--- a/blockchain/chain.go
+++ b/blockchain/chain.go
@@ -17,6 +17,8 @@ import (
 	"github.com/btcsuite/btcd/txscript"
 	"github.com/btcsuite/btcd/wire"
 	"github.com/btcsuite/btcutil"
+
+	"github.com/btcsuite/btcd/claimtrie"
 )
 
 const (
@@ -180,6 +182,8 @@ type BlockChain struct {
 	// certain blockchain events.
 	notificationsLock sync.RWMutex
 	notifications     []NotificationCallback
+
+	claimTrie *claimtrie.ClaimTrie
 }
 
 // HaveBlock returns whether or not the chain instance has the block represented
@@ -570,6 +574,11 @@ func (b *BlockChain) connectBlock(node *blockNode, block *btcutil.Block,
 			"spent transaction out information")
 	}
 
+	// Handle LBRY Claim Scripts
+	if err := b.ParseClaimScripts(block, node, view, false); err != nil {
+		return ruleError(ErrBadClaimTrie, err.Error())
+	}
+
 	// No warnings about unknown rules until the chain is current.
 	if b.isCurrent() {
 		// Warn if any unknown new rules are either about to activate or
@@ -761,6 +770,10 @@ func (b *BlockChain) disconnectBlock(node *blockNode, block *btcutil.Block, view
 		return err
 	}
 
+	if err = b.claimTrie.ResetHeight(node.parent.height); err != nil {
+		return err
+	}
+
 	// Prune fully spent entries and mark all entries in the view unmodified
 	// now that the modifications have been committed to the database.
 	view.commit()
@@ -1614,6 +1627,11 @@ func (b *BlockChain) LocateHeaders(locator BlockLocator, hashStop *chainhash.Has
 	return headers
 }
 
+// ClaimTrie returns the claimTrie associated wit hthe chain.
+func (b *BlockChain) ClaimTrie() *claimtrie.ClaimTrie {
+	return b.claimTrie
+}
+
 // IndexManager provides a generic interface that the is called when blocks are
 // connected and disconnected to and from the tip of the main chain for the
 // purpose of supporting optional indexes.
@@ -1796,6 +1814,20 @@ func New(config *Config) (*BlockChain, error) {
 		return nil, err
 	}
 
+	ct, err := claimtrie.New()
+	if err != nil {
+		return nil, err
+	}
+	b.claimTrie = ct
+
+	// ct.ResetHeight(760140) // TODO: add an optional CLI parameter for this
+
+	err = rebuildMissingClaimTrieData(&b, config.Interrupt)
+	if err != nil {
+		ct.Close()
+		return nil, err
+	}
+
 	bestNode := b.bestChain.Tip()
 	log.Infof("Chain state (height %d, hash %v, totaltx %d, work %v)",
 		bestNode.height, bestNode.hash, b.stateSnapshot.TotalTxns,
@@ -1803,3 +1835,61 @@ func New(config *Config) (*BlockChain, error) {
 
 	return &b, nil
 }
+
+func rebuildMissingClaimTrieData(b *BlockChain, done <-chan struct{}) error {
+	target := b.bestChain.Height()
+	if b.claimTrie.Height() == target {
+		return nil
+	}
+	if b.claimTrie.Height() > target {
+		return b.claimTrie.ResetHeight(target)
+	}
+
+	start := time.Now().Add(-6 * time.Second)
+	// TODO: move this view inside the loop (or recreate it every 5 sec.)
+	// as accumulating all inputs has potential to use a huge amount of RAM
+	// but we need to get the spent inputs working for that to be possible
+	view := NewUtxoViewpoint()
+	for h := int32(0); h < target; h++ {
+		select {
+		case <-done:
+			return fmt.Errorf("rebuild unfinished at height %d", b.claimTrie.Height())
+		default:
+		}
+
+		n := b.bestChain.NodeByHeight(h + 1)
+
+		var block *btcutil.Block
+		err := b.db.View(func(dbTx database.Tx) error {
+			var err error
+			block, err = dbFetchBlockByNode(dbTx, n)
+			return err
+		})
+		if err != nil {
+			return err
+		}
+
+		err = view.fetchInputUtxos(b.db, block)
+		if err != nil {
+			return err
+		}
+
+		err = view.connectTransactions(block, nil)
+		if err != nil {
+			return err
+		}
+
+		if h >= b.claimTrie.Height() {
+			err = b.ParseClaimScripts(block, n, view, true)
+			if err != nil {
+				return err
+			}
+		}
+		if time.Since(start).Seconds() > 5.0 {
+			start = time.Now()
+			log.Infof("Rebuilding claim trie data to %d. At: %d", target, h)
+		}
+	}
+	log.Infof("Completed rebuilding claim trie data to %d", b.claimTrie.Height())
+	return nil
+}
diff --git a/blockchain/claimtrie.go b/blockchain/claimtrie.go
new file mode 100644
index 00000000..b821c52b
--- /dev/null
+++ b/blockchain/claimtrie.go
@@ -0,0 +1,144 @@
+package blockchain
+
+import (
+	"bytes"
+	"fmt"
+
+	"github.com/pkg/errors"
+
+	"github.com/btcsuite/btcd/txscript"
+	"github.com/btcsuite/btcd/wire"
+	"github.com/btcsuite/btcutil"
+
+	"github.com/btcsuite/btcd/claimtrie"
+	"github.com/btcsuite/btcd/claimtrie/node"
+)
+
+// Hack: print which block mismatches happened, but keep recording.
+var mismatchedPrinted bool
+
+func (b *BlockChain) ParseClaimScripts(block *btcutil.Block, node *blockNode, view *UtxoViewpoint, failOnHashMiss bool) error {
+	ht := block.Height()
+
+	for _, tx := range block.Transactions() {
+		h := handler{ht, tx, view, map[string][]byte{}}
+		if err := h.handleTxIns(b.claimTrie); err != nil {
+			return err
+		}
+		if err := h.handleTxOuts(b.claimTrie); err != nil {
+			return err
+		}
+	}
+
+	// Hack: let the claimtrie know the expected Hash.
+	b.claimTrie.ReportHash(ht, node.claimTrie)
+
+	err := b.claimTrie.AppendBlock()
+	if err != nil {
+		return err
+	}
+	hash := b.claimTrie.MerkleHash()
+
+	if node.claimTrie != *hash {
+		if failOnHashMiss {
+			return fmt.Errorf("height: %d, ct.MerkleHash: %s != node.ClaimTrie: %s", ht, *hash, node.claimTrie)
+		}
+		if !mismatchedPrinted {
+			fmt.Printf("\n\nHeight: %d, ct.MerkleHash: %s != node.ClaimTrie: %s, Error: %s\n", ht, *hash, node.claimTrie, err)
+			mismatchedPrinted = true
+		}
+	}
+	return nil
+}
+
+type handler struct {
+	ht    int32
+	tx    *btcutil.Tx
+	view  *UtxoViewpoint
+	spent map[string][]byte
+}
+
+func (h *handler) handleTxIns(ct *claimtrie.ClaimTrie) error {
+	if IsCoinBase(h.tx) {
+		return nil
+	}
+	for _, txIn := range h.tx.MsgTx().TxIn {
+		op := txIn.PreviousOutPoint
+		e := h.view.LookupEntry(op)
+		if e == nil {
+			return fmt.Errorf("missing input in view for %s", op.String())
+		}
+		cs, err := txscript.DecodeClaimScript(e.pkScript)
+		if err == txscript.ErrNotClaimScript {
+			continue
+		}
+		if err != nil {
+			return err
+		}
+
+		var id node.ClaimID
+		name := cs.Name() // name of the previous one (that we're now spending)
+
+		switch cs.Opcode() {
+		case txscript.OP_CLAIMNAME: // OP code from previous transaction
+			id = node.NewClaimID(op) // claimID of the previous item now being spent
+			h.spent[id.String()] = node.NormalizeIfNecessary(name, ct.Height())
+			err = ct.SpendClaim(name, op, id)
+		case txscript.OP_UPDATECLAIM:
+			copy(id[:], cs.ClaimID())
+			h.spent[id.String()] = node.NormalizeIfNecessary(name, ct.Height())
+			err = ct.SpendClaim(name, op, id)
+		case txscript.OP_SUPPORTCLAIM:
+			copy(id[:], cs.ClaimID())
+			err = ct.SpendSupport(name, op, id)
+		}
+		if err != nil {
+			return errors.Wrapf(err, "handleTxIns")
+		}
+	}
+	return nil
+}
+
+func (h *handler) handleTxOuts(ct *claimtrie.ClaimTrie) error {
+	for i, txOut := range h.tx.MsgTx().TxOut {
+		op := wire.NewOutPoint(h.tx.Hash(), uint32(i))
+		cs, err := txscript.DecodeClaimScript(txOut.PkScript)
+		if err == txscript.ErrNotClaimScript {
+			continue
+		}
+		if err != nil {
+			return err
+		}
+
+		var id node.ClaimID
+		name := cs.Name()
+		amt := txOut.Value
+		value := cs.Value()
+
+		switch cs.Opcode() {
+		case txscript.OP_CLAIMNAME:
+			id = node.NewClaimID(*op)
+			err = ct.AddClaim(name, *op, id, amt, value)
+		case txscript.OP_SUPPORTCLAIM:
+			copy(id[:], cs.ClaimID())
+			err = ct.AddSupport(name, value, *op, amt, id)
+		case txscript.OP_UPDATECLAIM:
+			// old code wouldn't run the update if name or claimID didn't match existing data
+			// that was a safety feature, but it should have rejected the transaction instead
+			// TODO: reject transactions with invalid update commands
+			copy(id[:], cs.ClaimID())
+			normName := node.NormalizeIfNecessary(name, ct.Height())
+			if !bytes.Equal(h.spent[id.String()], normName) {
+				fmt.Printf("Invalid update operation: name or ID mismatch for %s, %s\n", normName, id.String())
+				continue
+			}
+
+			delete(h.spent, id.String())
+			err = ct.UpdateClaim(name, *op, amt, id, value)
+		}
+		if err != nil {
+			return errors.Wrapf(err, "handleTxOuts")
+		}
+	}
+	return nil
+}
diff --git a/btcd.go b/btcd.go
index b93851ba..2e6915a8 100644
--- a/btcd.go
+++ b/btcd.go
@@ -16,6 +16,7 @@ import (
 	"runtime/pprof"
 
 	"github.com/btcsuite/btcd/blockchain/indexers"
+	"github.com/btcsuite/btcd/claimtrie/param"
 	"github.com/btcsuite/btcd/database"
 	"github.com/btcsuite/btcd/limits"
 
@@ -147,6 +148,8 @@ func btcdMain(serverChan chan<- *server) error {
 		return nil
 	}
 
+	param.SetNetwork(activeNetParams.Params.Net, netName(activeNetParams)) // prep the claimtrie params
+
 	// Create server and start it.
 	server, err := newServer(cfg.Listeners, cfg.AgentBlacklist,
 		cfg.AgentWhitelist, db, activeNetParams.Params, interrupt)
@@ -161,6 +164,8 @@ func btcdMain(serverChan chan<- *server) error {
 		server.Stop()
 		server.WaitForShutdown()
 		srvrLog.Infof("Server shutdown complete")
+		// TODO: tie into the sync manager for shutdown instead
+		server.chain.ClaimTrie().Close()
 	}()
 	server.Start()
 	if serverChan != nil {