github.com/MetalBlockchain/metalgo@v1.11.9/indexer/index.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  	"errors"
     8  	"fmt"
     9  	"sync"
    10  
    11  	"go.uber.org/zap"
    12  
    13  	"github.com/MetalBlockchain/metalgo/database"
    14  	"github.com/MetalBlockchain/metalgo/database/prefixdb"
    15  	"github.com/MetalBlockchain/metalgo/database/versiondb"
    16  	"github.com/MetalBlockchain/metalgo/ids"
    17  	"github.com/MetalBlockchain/metalgo/snow"
    18  	"github.com/MetalBlockchain/metalgo/utils/logging"
    19  	"github.com/MetalBlockchain/metalgo/utils/timer/mockable"
    20  )
    21  
    22  // Maximum number of containers IDs that can be fetched at a time in a call to
    23  // GetContainerRange
    24  const MaxFetchedByRange = 1024
    25  
    26  var (
    27  	// Maps to the byte representation of the next accepted index
    28  	nextAcceptedIndexKey   = []byte{0x00}
    29  	indexToContainerPrefix = []byte{0x01}
    30  	containerToIDPrefix    = []byte{0x02}
    31  	errNoneAccepted        = errors.New("no containers have been accepted")
    32  	errNumToFetchInvalid   = fmt.Errorf("numToFetch must be in [1,%d]", MaxFetchedByRange)
    33  	errNoContainerAtIndex  = errors.New("no container at index")
    34  
    35  	_ snow.Acceptor = (*index)(nil)
    36  )
    37  
    38  // index indexes containers in their order of acceptance
    39  //
    40  // Invariant: index is thread-safe.
    41  // Invariant: index assumes that Accept is called, before the container is
    42  // committed to the database of the VM, in the order they were accepted.
    43  type index struct {
    44  	clock mockable.Clock
    45  	lock  sync.RWMutex
    46  	// The index of the next accepted transaction
    47  	nextAcceptedIndex uint64
    48  	// When [baseDB] is committed, writes to [baseDB]
    49  	vDB    *versiondb.Database
    50  	baseDB database.Database
    51  	// Both [indexToContainer] and [containerToIndex] have [vDB] underneath
    52  	// Index --> Container
    53  	indexToContainer database.Database
    54  	// Container ID --> Index
    55  	containerToIndex database.Database
    56  	log              logging.Logger
    57  }
    58  
    59  // Create a new thread-safe index.
    60  //
    61  // Invariant: Closes [baseDB] on close.
    62  func newIndex(
    63  	baseDB database.Database,
    64  	log logging.Logger,
    65  	clock mockable.Clock,
    66  ) (*index, error) {
    67  	vDB := versiondb.New(baseDB)
    68  	indexToContainer := prefixdb.New(indexToContainerPrefix, vDB)
    69  	containerToIndex := prefixdb.New(containerToIDPrefix, vDB)
    70  
    71  	i := &index{
    72  		clock:            clock,
    73  		baseDB:           baseDB,
    74  		vDB:              vDB,
    75  		indexToContainer: indexToContainer,
    76  		containerToIndex: containerToIndex,
    77  		log:              log,
    78  	}
    79  
    80  	// Get next accepted index from db
    81  	nextAcceptedIndex, err := database.GetUInt64(i.vDB, nextAcceptedIndexKey)
    82  	if err == database.ErrNotFound {
    83  		// Couldn't find it in the database. Must not have accepted any containers in previous runs.
    84  		i.log.Info("created new index",
    85  			zap.Uint64("nextAcceptedIndex", i.nextAcceptedIndex),
    86  		)
    87  		return i, nil
    88  	}
    89  	if err != nil {
    90  		return nil, fmt.Errorf("couldn't get next accepted index from database: %w", err)
    91  	}
    92  	i.nextAcceptedIndex = nextAcceptedIndex
    93  	i.log.Info("created new index",
    94  		zap.Uint64("nextAcceptedIndex", i.nextAcceptedIndex),
    95  	)
    96  	return i, nil
    97  }
    98  
    99  // Close this index
   100  func (i *index) Close() error {
   101  	return errors.Join(
   102  		i.indexToContainer.Close(),
   103  		i.containerToIndex.Close(),
   104  		i.vDB.Close(),
   105  		i.baseDB.Close(),
   106  	)
   107  }
   108  
   109  // Index that the given transaction is accepted
   110  // Returned error should be treated as fatal; the VM should not commit [containerID]
   111  // or any new containers as accepted.
   112  func (i *index) Accept(ctx *snow.ConsensusContext, containerID ids.ID, containerBytes []byte) error {
   113  	i.lock.Lock()
   114  	defer i.lock.Unlock()
   115  
   116  	// It may be the case that in a previous run of this node, this index committed [containerID]
   117  	// as accepted and then the node shut down before the VM committed [containerID] as accepted.
   118  	// In that case, when the node restarts Accept will be called with the same container.
   119  	// Make sure we don't index the same container twice in that event.
   120  	_, err := i.containerToIndex.Get(containerID[:])
   121  	if err == nil {
   122  		ctx.Log.Debug("not indexing already accepted container",
   123  			zap.Stringer("containerID", containerID),
   124  		)
   125  		return nil
   126  	}
   127  	if err != database.ErrNotFound {
   128  		return fmt.Errorf("couldn't get whether %s is accepted: %w", containerID, err)
   129  	}
   130  
   131  	ctx.Log.Debug("indexing container",
   132  		zap.Uint64("nextAcceptedIndex", i.nextAcceptedIndex),
   133  		zap.Stringer("containerID", containerID),
   134  	)
   135  	// Persist index --> Container
   136  	nextAcceptedIndexBytes := database.PackUInt64(i.nextAcceptedIndex)
   137  	bytes, err := Codec.Marshal(CodecVersion, Container{
   138  		ID:        containerID,
   139  		Bytes:     containerBytes,
   140  		Timestamp: i.clock.Time().UnixNano(),
   141  	})
   142  	if err != nil {
   143  		return fmt.Errorf("couldn't serialize container %s: %w", containerID, err)
   144  	}
   145  	if err := i.indexToContainer.Put(nextAcceptedIndexBytes, bytes); err != nil {
   146  		return fmt.Errorf("couldn't put accepted container %s into index: %w", containerID, err)
   147  	}
   148  
   149  	// Persist container ID --> index
   150  	if err := i.containerToIndex.Put(containerID[:], nextAcceptedIndexBytes); err != nil {
   151  		return fmt.Errorf("couldn't map container %s to index: %w", containerID, err)
   152  	}
   153  
   154  	// Persist next accepted index
   155  	i.nextAcceptedIndex++
   156  	if err := database.PutUInt64(i.vDB, nextAcceptedIndexKey, i.nextAcceptedIndex); err != nil {
   157  		return fmt.Errorf("couldn't put accepted container %s into index: %w", containerID, err)
   158  	}
   159  
   160  	// Atomically commit [i.vDB], [i.indexToContainer], [i.containerToIndex] to [i.baseDB]
   161  	return i.vDB.Commit()
   162  }
   163  
   164  // Returns the ID of the [index]th accepted container and the container itself.
   165  // For example, if [index] == 0, returns the first accepted container.
   166  // If [index] == 1, returns the second accepted container, etc.
   167  // Returns an error if there is no container at the given index.
   168  func (i *index) GetContainerByIndex(index uint64) (Container, error) {
   169  	i.lock.RLock()
   170  	defer i.lock.RUnlock()
   171  
   172  	return i.getContainerByIndex(index)
   173  }
   174  
   175  // Assumes [i.lock] is held
   176  func (i *index) getContainerByIndex(index uint64) (Container, error) {
   177  	lastAcceptedIndex, ok := i.lastAcceptedIndex()
   178  	if !ok || index > lastAcceptedIndex {
   179  		return Container{}, fmt.Errorf("%w %d", errNoContainerAtIndex, index)
   180  	}
   181  	indexBytes := database.PackUInt64(index)
   182  	return i.getContainerByIndexBytes(indexBytes)
   183  }
   184  
   185  // [indexBytes] is the byte representation of the index to fetch.
   186  // Assumes [i.lock] is held
   187  func (i *index) getContainerByIndexBytes(indexBytes []byte) (Container, error) {
   188  	containerBytes, err := i.indexToContainer.Get(indexBytes)
   189  	if err != nil {
   190  		i.log.Error("couldn't read container from database",
   191  			zap.Error(err),
   192  		)
   193  		return Container{}, fmt.Errorf("couldn't read from database: %w", err)
   194  	}
   195  	var container Container
   196  	if _, err := Codec.Unmarshal(containerBytes, &container); err != nil {
   197  		return Container{}, fmt.Errorf("couldn't unmarshal container: %w", err)
   198  	}
   199  	return container, nil
   200  }
   201  
   202  // GetContainerRange returns the IDs of containers at indices
   203  // [startIndex], [startIndex+1], ..., [startIndex+numToFetch-1].
   204  // [startIndex] should be <= i.lastAcceptedIndex().
   205  // [numToFetch] should be in [0, MaxFetchedByRange]
   206  func (i *index) GetContainerRange(startIndex, numToFetch uint64) ([]Container, error) {
   207  	// Check arguments for validity
   208  	if numToFetch == 0 || numToFetch > MaxFetchedByRange {
   209  		return nil, fmt.Errorf("%w but is %d", errNumToFetchInvalid, numToFetch)
   210  	}
   211  
   212  	i.lock.RLock()
   213  	defer i.lock.RUnlock()
   214  
   215  	lastAcceptedIndex, ok := i.lastAcceptedIndex()
   216  	if !ok {
   217  		return nil, errNoneAccepted
   218  	} else if startIndex > lastAcceptedIndex {
   219  		return nil, fmt.Errorf("start index (%d) > last accepted index (%d)", startIndex, lastAcceptedIndex)
   220  	}
   221  
   222  	// Calculate the last index we will fetch
   223  	lastIndex := min(startIndex+numToFetch-1, lastAcceptedIndex)
   224  	// [lastIndex] is always >= [startIndex] so this is safe.
   225  	// [numToFetch] is limited to [MaxFetchedByRange] so [containers] is bounded in size.
   226  	containers := make([]Container, int(lastIndex)-int(startIndex)+1)
   227  
   228  	n := 0
   229  	var err error
   230  	for j := startIndex; j <= lastIndex; j++ {
   231  		containers[n], err = i.getContainerByIndex(j)
   232  		if err != nil {
   233  			return nil, fmt.Errorf("couldn't get container at index %d: %w", j, err)
   234  		}
   235  		n++
   236  	}
   237  	return containers, nil
   238  }
   239  
   240  // Returns database.ErrNotFound if the container is not indexed as accepted
   241  func (i *index) GetIndex(id ids.ID) (uint64, error) {
   242  	i.lock.RLock()
   243  	defer i.lock.RUnlock()
   244  
   245  	return database.GetUInt64(i.containerToIndex, id[:])
   246  }
   247  
   248  func (i *index) GetContainerByID(id ids.ID) (Container, error) {
   249  	i.lock.RLock()
   250  	defer i.lock.RUnlock()
   251  
   252  	// Read index from database
   253  	indexBytes, err := i.containerToIndex.Get(id[:])
   254  	if err != nil {
   255  		return Container{}, err
   256  	}
   257  	return i.getContainerByIndexBytes(indexBytes)
   258  }
   259  
   260  // GetLastAccepted returns the last accepted container.
   261  // Returns an error if no containers have been accepted.
   262  func (i *index) GetLastAccepted() (Container, error) {
   263  	i.lock.RLock()
   264  	defer i.lock.RUnlock()
   265  
   266  	lastAcceptedIndex, exists := i.lastAcceptedIndex()
   267  	if !exists {
   268  		return Container{}, errNoneAccepted
   269  	}
   270  	return i.getContainerByIndex(lastAcceptedIndex)
   271  }
   272  
   273  // Assumes i.lock is held
   274  // Returns:
   275  //
   276  //  1. The index of the most recently accepted transaction, or 0 if no
   277  //     transactions have been accepted
   278  //  2. Whether at least 1 transaction has been accepted
   279  func (i *index) lastAcceptedIndex() (uint64, bool) {
   280  	return i.nextAcceptedIndex - 1, i.nextAcceptedIndex != 0
   281  }