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 {