github.com/MetalBlockchain/metalgo@v1.11.9/indexer/indexer.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package indexer
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"sync"
    10  
    11  	"github.com/gorilla/rpc/v2"
    12  	"go.uber.org/zap"
    13  
    14  	"github.com/MetalBlockchain/metalgo/api/server"
    15  	"github.com/MetalBlockchain/metalgo/chains"
    16  	"github.com/MetalBlockchain/metalgo/database"
    17  	"github.com/MetalBlockchain/metalgo/database/prefixdb"
    18  	"github.com/MetalBlockchain/metalgo/ids"
    19  	"github.com/MetalBlockchain/metalgo/snow"
    20  	"github.com/MetalBlockchain/metalgo/snow/engine/avalanche/vertex"
    21  	"github.com/MetalBlockchain/metalgo/snow/engine/common"
    22  	"github.com/MetalBlockchain/metalgo/snow/engine/snowman/block"
    23  	"github.com/MetalBlockchain/metalgo/utils/constants"
    24  	"github.com/MetalBlockchain/metalgo/utils/json"
    25  	"github.com/MetalBlockchain/metalgo/utils/logging"
    26  	"github.com/MetalBlockchain/metalgo/utils/timer/mockable"
    27  	"github.com/MetalBlockchain/metalgo/utils/wrappers"
    28  )
    29  
    30  const (
    31  	indexNamePrefix         = "index-"
    32  	txPrefix                = 0x01
    33  	vtxPrefix               = 0x02
    34  	blockPrefix             = 0x03
    35  	isIncompletePrefix      = 0x04
    36  	previouslyIndexedPrefix = 0x05
    37  )
    38  
    39  var (
    40  	_ Indexer = (*indexer)(nil)
    41  
    42  	hasRunKey = []byte{0x07}
    43  )
    44  
    45  // Config for an indexer
    46  type Config struct {
    47  	DB                   database.Database
    48  	Log                  logging.Logger
    49  	IndexingEnabled      bool
    50  	AllowIncompleteIndex bool
    51  	BlockAcceptorGroup   snow.AcceptorGroup
    52  	TxAcceptorGroup      snow.AcceptorGroup
    53  	VertexAcceptorGroup  snow.AcceptorGroup
    54  	APIServer            server.PathAdder
    55  	ShutdownF            func()
    56  }
    57  
    58  // Indexer causes accepted containers for a given chain
    59  // to be indexed by their ID and by the order in which
    60  // they were accepted by this node.
    61  // Indexer is threadsafe.
    62  type Indexer interface {
    63  	chains.Registrant
    64  	// Close will do nothing and return nil after the first call
    65  	io.Closer
    66  }
    67  
    68  // NewIndexer returns a new Indexer and registers a new endpoint on the given API server.
    69  func NewIndexer(config Config) (Indexer, error) {
    70  	indexer := &indexer{
    71  		log:                  config.Log,
    72  		db:                   config.DB,
    73  		allowIncompleteIndex: config.AllowIncompleteIndex,
    74  		indexingEnabled:      config.IndexingEnabled,
    75  		blockAcceptorGroup:   config.BlockAcceptorGroup,
    76  		txAcceptorGroup:      config.TxAcceptorGroup,
    77  		vertexAcceptorGroup:  config.VertexAcceptorGroup,
    78  		txIndices:            map[ids.ID]*index{},
    79  		vtxIndices:           map[ids.ID]*index{},
    80  		blockIndices:         map[ids.ID]*index{},
    81  		pathAdder:            config.APIServer,
    82  		shutdownF:            config.ShutdownF,
    83  	}
    84  
    85  	hasRun, err := indexer.hasRun()
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	indexer.hasRunBefore = hasRun
    90  	return indexer, indexer.markHasRun()
    91  }
    92  
    93  type indexer struct {
    94  	clock  mockable.Clock
    95  	lock   sync.RWMutex
    96  	log    logging.Logger
    97  	db     database.Database
    98  	closed bool
    99  
   100  	// Called in a goroutine on shutdown
   101  	shutdownF func()
   102  
   103  	// true if this is not the first run using this database
   104  	hasRunBefore bool
   105  
   106  	// Used to add API endpoint for new indices
   107  	pathAdder server.PathAdder
   108  
   109  	// If true, allow running in such a way that could allow the creation
   110  	// of an index which could be missing accepted containers.
   111  	allowIncompleteIndex bool
   112  
   113  	// If false, don't create index for a chain when RegisterChain is called
   114  	indexingEnabled bool
   115  
   116  	// Chain ID --> index of blocks of that chain (if applicable)
   117  	blockIndices map[ids.ID]*index
   118  	// Chain ID --> index of vertices of that chain (if applicable)
   119  	vtxIndices map[ids.ID]*index
   120  	// Chain ID --> index of txs of that chain (if applicable)
   121  	txIndices map[ids.ID]*index
   122  
   123  	// Notifies of newly accepted blocks
   124  	blockAcceptorGroup snow.AcceptorGroup
   125  	// Notifies of newly accepted transactions
   126  	txAcceptorGroup snow.AcceptorGroup
   127  	// Notifies of newly accepted vertices
   128  	vertexAcceptorGroup snow.AcceptorGroup
   129  }
   130  
   131  // Assumes [ctx.Lock] is not held
   132  func (i *indexer) RegisterChain(chainName string, ctx *snow.ConsensusContext, vm common.VM) {
   133  	i.lock.Lock()
   134  	defer i.lock.Unlock()
   135  
   136  	if i.closed {
   137  		i.log.Debug("not registering chain to indexer",
   138  			zap.String("reason", "indexer is closed"),
   139  			zap.String("chainName", chainName),
   140  		)
   141  		return
   142  	} else if ctx.SubnetID != constants.PrimaryNetworkID {
   143  		i.log.Debug("not registering chain to indexer",
   144  			zap.String("reason", "not in the primary network"),
   145  			zap.String("chainName", chainName),
   146  		)
   147  		return
   148  	}
   149  
   150  	chainID := ctx.ChainID
   151  	if i.blockIndices[chainID] != nil || i.txIndices[chainID] != nil || i.vtxIndices[chainID] != nil {
   152  		i.log.Warn("chain is already being indexed",
   153  			zap.Stringer("chainID", chainID),
   154  		)
   155  		return
   156  	}
   157  
   158  	// If the index is incomplete, make sure that's OK. Otherwise, cause node to die.
   159  	isIncomplete, err := i.isIncomplete(chainID)
   160  	if err != nil {
   161  		i.log.Error("couldn't get whether chain is incomplete",
   162  			zap.String("chainName", chainName),
   163  			zap.Error(err),
   164  		)
   165  		if err := i.close(); err != nil {
   166  			i.log.Error("failed to close indexer",
   167  				zap.Error(err),
   168  			)
   169  		}
   170  		return
   171  	}
   172  
   173  	// See if this chain was indexed in a previous run
   174  	previouslyIndexed, err := i.previouslyIndexed(chainID)
   175  	if err != nil {
   176  		i.log.Error("couldn't get whether chain was previously indexed",
   177  			zap.String("chainName", chainName),
   178  			zap.Error(err),
   179  		)
   180  		if err := i.close(); err != nil {
   181  			i.log.Error("failed to close indexer",
   182  				zap.Error(err),
   183  			)
   184  		}
   185  		return
   186  	}
   187  
   188  	if !i.indexingEnabled { // Indexing is disabled
   189  		if previouslyIndexed && !i.allowIncompleteIndex {
   190  			// We indexed this chain in a previous run but not in this run.
   191  			// This would create an incomplete index, which is not allowed, so exit.
   192  			i.log.Fatal("running would cause index to become incomplete but incomplete indices are disabled",
   193  				zap.String("chainName", chainName),
   194  			)
   195  			if err := i.close(); err != nil {
   196  				i.log.Error("failed to close indexer",
   197  					zap.Error(err),
   198  				)
   199  			}
   200  			return
   201  		}
   202  
   203  		// Creating an incomplete index is allowed. Mark index as incomplete.
   204  		err := i.markIncomplete(chainID)
   205  		if err == nil {
   206  			return
   207  		}
   208  		i.log.Fatal("couldn't mark chain as incomplete",
   209  			zap.String("chainName", chainName),
   210  			zap.Error(err),
   211  		)
   212  		if err := i.close(); err != nil {
   213  			i.log.Error("failed to close indexer",
   214  				zap.Error(err),
   215  			)
   216  		}
   217  		return
   218  	}
   219  
   220  	if !i.allowIncompleteIndex && isIncomplete && (previouslyIndexed || i.hasRunBefore) {
   221  		i.log.Fatal("index is incomplete but incomplete indices are disabled. Shutting down",
   222  			zap.String("chainName", chainName),
   223  		)
   224  		if err := i.close(); err != nil {
   225  			i.log.Error("failed to close indexer",
   226  				zap.Error(err),
   227  			)
   228  		}
   229  		return
   230  	}
   231  
   232  	// Mark that in this run, this chain was indexed
   233  	if err := i.markPreviouslyIndexed(chainID); err != nil {
   234  		i.log.Error("couldn't mark chain as indexed",
   235  			zap.String("chainName", chainName),
   236  			zap.Error(err),
   237  		)
   238  		if err := i.close(); err != nil {
   239  			i.log.Error("failed to close indexer",
   240  				zap.Error(err),
   241  			)
   242  		}
   243  		return
   244  	}
   245  
   246  	index, err := i.registerChainHelper(chainID, blockPrefix, chainName, "block", i.blockAcceptorGroup)
   247  	if err != nil {
   248  		i.log.Fatal("failed to create index",
   249  			zap.String("chainName", chainName),
   250  			zap.String("endpoint", "block"),
   251  			zap.Error(err),
   252  		)
   253  		if err := i.close(); err != nil {
   254  			i.log.Error("failed to close indexer",
   255  				zap.Error(err),
   256  			)
   257  		}
   258  		return
   259  	}
   260  	i.blockIndices[chainID] = index
   261  
   262  	switch vm.(type) {
   263  	case vertex.DAGVM:
   264  		vtxIndex, err := i.registerChainHelper(chainID, vtxPrefix, chainName, "vtx", i.vertexAcceptorGroup)
   265  		if err != nil {
   266  			i.log.Fatal("couldn't create index",
   267  				zap.String("chainName", chainName),
   268  				zap.String("endpoint", "vtx"),
   269  				zap.Error(err),
   270  			)
   271  			if err := i.close(); err != nil {
   272  				i.log.Error("failed to close indexer",
   273  					zap.Error(err),
   274  				)
   275  			}
   276  			return
   277  		}
   278  		i.vtxIndices[chainID] = vtxIndex
   279  
   280  		txIndex, err := i.registerChainHelper(chainID, txPrefix, chainName, "tx", i.txAcceptorGroup)
   281  		if err != nil {
   282  			i.log.Fatal("couldn't create index",
   283  				zap.String("chainName", chainName),
   284  				zap.String("endpoint", "tx"),
   285  				zap.Error(err),
   286  			)
   287  			if err := i.close(); err != nil {
   288  				i.log.Error("failed to close indexer",
   289  					zap.Error(err),
   290  				)
   291  			}
   292  			return
   293  		}
   294  		i.txIndices[chainID] = txIndex
   295  	case block.ChainVM:
   296  	default:
   297  		vmType := fmt.Sprintf("%T", vm)
   298  		i.log.Error("got unexpected vm type",
   299  			zap.String("vmType", vmType),
   300  		)
   301  		if err := i.close(); err != nil {
   302  			i.log.Error("failed to close indexer",
   303  				zap.Error(err),
   304  			)
   305  		}
   306  	}
   307  }
   308  
   309  func (i *indexer) registerChainHelper(
   310  	chainID ids.ID,
   311  	prefixEnd byte,
   312  	name, endpoint string,
   313  	acceptorGroup snow.AcceptorGroup,
   314  ) (*index, error) {
   315  	prefix := make([]byte, ids.IDLen+wrappers.ByteLen)
   316  	copy(prefix, chainID[:])
   317  	prefix[ids.IDLen] = prefixEnd
   318  	indexDB := prefixdb.New(prefix, i.db)
   319  	index, err := newIndex(indexDB, i.log, i.clock)
   320  	if err != nil {
   321  		_ = indexDB.Close()
   322  		return nil, err
   323  	}
   324  
   325  	// Register index to learn about new accepted vertices
   326  	if err := acceptorGroup.RegisterAcceptor(chainID, fmt.Sprintf("%s%s", indexNamePrefix, chainID), index, true); err != nil {
   327  		_ = index.Close()
   328  		return nil, err
   329  	}
   330  
   331  	// Create an API endpoint for this index
   332  	apiServer := rpc.NewServer()
   333  	codec := json.NewCodec()
   334  	apiServer.RegisterCodec(codec, "application/json")
   335  	apiServer.RegisterCodec(codec, "application/json;charset=UTF-8")
   336  	if err := apiServer.RegisterService(&service{index: index}, "index"); err != nil {
   337  		_ = index.Close()
   338  		return nil, err
   339  	}
   340  	if err := i.pathAdder.AddRoute(apiServer, "index/"+name, "/"+endpoint); err != nil {
   341  		_ = index.Close()
   342  		return nil, err
   343  	}
   344  	return index, nil
   345  }
   346  
   347  // Close this indexer. Stops indexing all chains.
   348  // Closes [i.db]. Assumes Close is only called after
   349  // the node is done making decisions.
   350  // Calling Close after it has been called does nothing.
   351  func (i *indexer) Close() error {
   352  	i.lock.Lock()
   353  	defer i.lock.Unlock()
   354  
   355  	return i.close()
   356  }
   357  
   358  func (i *indexer) close() error {
   359  	if i.closed {
   360  		return nil
   361  	}
   362  	i.closed = true
   363  
   364  	errs := &wrappers.Errs{}
   365  	for chainID, txIndex := range i.txIndices {
   366  		errs.Add(
   367  			txIndex.Close(),
   368  			i.txAcceptorGroup.DeregisterAcceptor(chainID, fmt.Sprintf("%s%s", indexNamePrefix, chainID)),
   369  		)
   370  	}
   371  	for chainID, vtxIndex := range i.vtxIndices {
   372  		errs.Add(
   373  			vtxIndex.Close(),
   374  			i.vertexAcceptorGroup.DeregisterAcceptor(chainID, fmt.Sprintf("%s%s", indexNamePrefix, chainID)),
   375  		)
   376  	}
   377  	for chainID, blockIndex := range i.blockIndices {
   378  		errs.Add(
   379  			blockIndex.Close(),
   380  			i.blockAcceptorGroup.DeregisterAcceptor(chainID, fmt.Sprintf("%s%s", indexNamePrefix, chainID)),
   381  		)
   382  	}
   383  	errs.Add(i.db.Close())
   384  
   385  	go i.shutdownF()
   386  	return errs.Err
   387  }
   388  
   389  func (i *indexer) markIncomplete(chainID ids.ID) error {
   390  	key := make([]byte, ids.IDLen+wrappers.ByteLen)
   391  	copy(key, chainID[:])
   392  	key[ids.IDLen] = isIncompletePrefix
   393  	return i.db.Put(key, nil)
   394  }
   395  
   396  // Returns true if this chain is incomplete
   397  func (i *indexer) isIncomplete(chainID ids.ID) (bool, error) {
   398  	key := make([]byte, ids.IDLen+wrappers.ByteLen)
   399  	copy(key, chainID[:])
   400  	key[ids.IDLen] = isIncompletePrefix
   401  	return i.db.Has(key)
   402  }
   403  
   404  func (i *indexer) markPreviouslyIndexed(chainID ids.ID) error {
   405  	key := make([]byte, ids.IDLen+wrappers.ByteLen)
   406  	copy(key, chainID[:])
   407  	key[ids.IDLen] = previouslyIndexedPrefix
   408  	return i.db.Put(key, nil)
   409  }
   410  
   411  // Returns true if this chain is incomplete
   412  func (i *indexer) previouslyIndexed(chainID ids.ID) (bool, error) {
   413  	key := make([]byte, ids.IDLen+wrappers.ByteLen)
   414  	copy(key, chainID[:])
   415  	key[ids.IDLen] = previouslyIndexedPrefix
   416  	return i.db.Has(key)
   417  }
   418  
   419  // Mark that the node has run at least once
   420  func (i *indexer) markHasRun() error {
   421  	return i.db.Put(hasRunKey, nil)
   422  }
   423  
   424  // Returns true if the node has run before
   425  func (i *indexer) hasRun() (bool, error) {
   426  	return i.db.Has(hasRunKey)
   427  }