github.com/MetalBlockchain/metalgo@v1.11.9/vms/platformvm/validators/manager.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package validators
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"time"
    11  
    12  	"github.com/MetalBlockchain/metalgo/cache"
    13  	"github.com/MetalBlockchain/metalgo/ids"
    14  	"github.com/MetalBlockchain/metalgo/snow/validators"
    15  	"github.com/MetalBlockchain/metalgo/utils/constants"
    16  	"github.com/MetalBlockchain/metalgo/utils/logging"
    17  	"github.com/MetalBlockchain/metalgo/utils/timer/mockable"
    18  	"github.com/MetalBlockchain/metalgo/utils/window"
    19  	"github.com/MetalBlockchain/metalgo/vms/platformvm/block"
    20  	"github.com/MetalBlockchain/metalgo/vms/platformvm/config"
    21  	"github.com/MetalBlockchain/metalgo/vms/platformvm/metrics"
    22  	"github.com/MetalBlockchain/metalgo/vms/platformvm/status"
    23  	"github.com/MetalBlockchain/metalgo/vms/platformvm/txs"
    24  )
    25  
    26  const (
    27  	validatorSetsCacheSize        = 64
    28  	maxRecentlyAcceptedWindowSize = 64
    29  	minRecentlyAcceptedWindowSize = 16
    30  	recentlyAcceptedWindowTTL     = 2 * time.Minute
    31  )
    32  
    33  var (
    34  	_ validators.State = (*manager)(nil)
    35  
    36  	errUnfinalizedHeight = errors.New("failed to fetch validator set at unfinalized height")
    37  )
    38  
    39  // Manager adds the ability to introduce newly accepted blocks IDs to the State
    40  // interface.
    41  type Manager interface {
    42  	validators.State
    43  
    44  	// OnAcceptedBlockID registers the ID of the latest accepted block.
    45  	// It is used to update the [recentlyAccepted] sliding window.
    46  	OnAcceptedBlockID(blkID ids.ID)
    47  }
    48  
    49  type State interface {
    50  	GetTx(txID ids.ID) (*txs.Tx, status.Status, error)
    51  
    52  	GetLastAccepted() ids.ID
    53  	GetStatelessBlock(blockID ids.ID) (block.Block, error)
    54  
    55  	// ApplyValidatorWeightDiffs iterates from [startHeight] towards the genesis
    56  	// block until it has applied all of the diffs up to and including
    57  	// [endHeight]. Applying the diffs modifies [validators].
    58  	//
    59  	// Invariant: If attempting to generate the validator set for
    60  	// [endHeight - 1], [validators] must initially contain the validator
    61  	// weights for [startHeight].
    62  	//
    63  	// Note: Because this function iterates towards the genesis, [startHeight]
    64  	// should normally be greater than or equal to [endHeight].
    65  	ApplyValidatorWeightDiffs(
    66  		ctx context.Context,
    67  		validators map[ids.NodeID]*validators.GetValidatorOutput,
    68  		startHeight uint64,
    69  		endHeight uint64,
    70  		subnetID ids.ID,
    71  	) error
    72  
    73  	// ApplyValidatorPublicKeyDiffs iterates from [startHeight] towards the
    74  	// genesis block until it has applied all of the diffs up to and including
    75  	// [endHeight]. Applying the diffs modifies [validators].
    76  	//
    77  	// Invariant: If attempting to generate the validator set for
    78  	// [endHeight - 1], [validators] must initially contain the validator
    79  	// weights for [startHeight].
    80  	//
    81  	// Note: Because this function iterates towards the genesis, [startHeight]
    82  	// should normally be greater than or equal to [endHeight].
    83  	ApplyValidatorPublicKeyDiffs(
    84  		ctx context.Context,
    85  		validators map[ids.NodeID]*validators.GetValidatorOutput,
    86  		startHeight uint64,
    87  		endHeight uint64,
    88  	) error
    89  }
    90  
    91  func NewManager(
    92  	log logging.Logger,
    93  	cfg config.Config,
    94  	state State,
    95  	metrics metrics.Metrics,
    96  	clk *mockable.Clock,
    97  ) Manager {
    98  	return &manager{
    99  		log:     log,
   100  		cfg:     cfg,
   101  		state:   state,
   102  		metrics: metrics,
   103  		clk:     clk,
   104  		caches:  make(map[ids.ID]cache.Cacher[uint64, map[ids.NodeID]*validators.GetValidatorOutput]),
   105  		recentlyAccepted: window.New[ids.ID](
   106  			window.Config{
   107  				Clock:   clk,
   108  				MaxSize: maxRecentlyAcceptedWindowSize,
   109  				MinSize: minRecentlyAcceptedWindowSize,
   110  				TTL:     recentlyAcceptedWindowTTL,
   111  			},
   112  		),
   113  	}
   114  }
   115  
   116  // TODO: Remove requirement for the P-chain's context lock to be held when
   117  // calling exported functions.
   118  type manager struct {
   119  	log     logging.Logger
   120  	cfg     config.Config
   121  	state   State
   122  	metrics metrics.Metrics
   123  	clk     *mockable.Clock
   124  
   125  	// Maps caches for each subnet that is currently tracked.
   126  	// Key: Subnet ID
   127  	// Value: cache mapping height -> validator set map
   128  	caches map[ids.ID]cache.Cacher[uint64, map[ids.NodeID]*validators.GetValidatorOutput]
   129  
   130  	// sliding window of blocks that were recently accepted
   131  	recentlyAccepted window.Window[ids.ID]
   132  }
   133  
   134  // GetMinimumHeight returns the height of the most recent block beyond the
   135  // horizon of our recentlyAccepted window.
   136  //
   137  // Because the time between blocks is arbitrary, we're only guaranteed that
   138  // the window's configured TTL amount of time has passed once an element
   139  // expires from the window.
   140  //
   141  // To try to always return a block older than the window's TTL, we return the
   142  // parent of the oldest element in the window (as an expired element is always
   143  // guaranteed to be sufficiently stale). If we haven't expired an element yet
   144  // in the case of a process restart, we default to the lastAccepted block's
   145  // height which is likely (but not guaranteed) to also be older than the
   146  // window's configured TTL.
   147  //
   148  // If [UseCurrentHeight] is true, we override the block selection policy
   149  // described above and we will always return the last accepted block height
   150  // as the minimum.
   151  func (m *manager) GetMinimumHeight(ctx context.Context) (uint64, error) {
   152  	if m.cfg.UseCurrentHeight {
   153  		return m.getCurrentHeight(ctx)
   154  	}
   155  
   156  	oldest, ok := m.recentlyAccepted.Oldest()
   157  	if !ok {
   158  		return m.getCurrentHeight(ctx)
   159  	}
   160  
   161  	blk, err := m.state.GetStatelessBlock(oldest)
   162  	if err != nil {
   163  		return 0, err
   164  	}
   165  
   166  	// We subtract 1 from the height of [oldest] because we want the height of
   167  	// the last block accepted before the [recentlyAccepted] window.
   168  	//
   169  	// There is guaranteed to be a block accepted before this window because the
   170  	// first block added to [recentlyAccepted] window is >= height 1.
   171  	return blk.Height() - 1, nil
   172  }
   173  
   174  func (m *manager) GetCurrentHeight(ctx context.Context) (uint64, error) {
   175  	return m.getCurrentHeight(ctx)
   176  }
   177  
   178  // TODO: Pass the context into the state.
   179  func (m *manager) getCurrentHeight(context.Context) (uint64, error) {
   180  	lastAcceptedID := m.state.GetLastAccepted()
   181  	lastAccepted, err := m.state.GetStatelessBlock(lastAcceptedID)
   182  	if err != nil {
   183  		return 0, err
   184  	}
   185  	return lastAccepted.Height(), nil
   186  }
   187  
   188  func (m *manager) GetValidatorSet(
   189  	ctx context.Context,
   190  	targetHeight uint64,
   191  	subnetID ids.ID,
   192  ) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
   193  	validatorSetsCache := m.getValidatorSetCache(subnetID)
   194  
   195  	if validatorSet, ok := validatorSetsCache.Get(targetHeight); ok {
   196  		m.metrics.IncValidatorSetsCached()
   197  		return validatorSet, nil
   198  	}
   199  
   200  	// get the start time to track metrics
   201  	startTime := m.clk.Time()
   202  
   203  	var (
   204  		validatorSet  map[ids.NodeID]*validators.GetValidatorOutput
   205  		currentHeight uint64
   206  		err           error
   207  	)
   208  	if subnetID == constants.PrimaryNetworkID {
   209  		validatorSet, currentHeight, err = m.makePrimaryNetworkValidatorSet(ctx, targetHeight)
   210  	} else {
   211  		validatorSet, currentHeight, err = m.makeSubnetValidatorSet(ctx, targetHeight, subnetID)
   212  	}
   213  	if err != nil {
   214  		return nil, err
   215  	}
   216  
   217  	// cache the validator set
   218  	validatorSetsCache.Put(targetHeight, validatorSet)
   219  
   220  	duration := m.clk.Time().Sub(startTime)
   221  	m.metrics.IncValidatorSetsCreated()
   222  	m.metrics.AddValidatorSetsDuration(duration)
   223  	m.metrics.AddValidatorSetsHeightDiff(currentHeight - targetHeight)
   224  	return validatorSet, nil
   225  }
   226  
   227  func (m *manager) getValidatorSetCache(subnetID ids.ID) cache.Cacher[uint64, map[ids.NodeID]*validators.GetValidatorOutput] {
   228  	// Only cache tracked subnets
   229  	if subnetID != constants.PrimaryNetworkID && !m.cfg.TrackedSubnets.Contains(subnetID) {
   230  		return &cache.Empty[uint64, map[ids.NodeID]*validators.GetValidatorOutput]{}
   231  	}
   232  
   233  	validatorSetsCache, exists := m.caches[subnetID]
   234  	if exists {
   235  		return validatorSetsCache
   236  	}
   237  
   238  	validatorSetsCache = &cache.LRU[uint64, map[ids.NodeID]*validators.GetValidatorOutput]{
   239  		Size: validatorSetsCacheSize,
   240  	}
   241  	m.caches[subnetID] = validatorSetsCache
   242  	return validatorSetsCache
   243  }
   244  
   245  func (m *manager) makePrimaryNetworkValidatorSet(
   246  	ctx context.Context,
   247  	targetHeight uint64,
   248  ) (map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) {
   249  	validatorSet, currentHeight, err := m.getCurrentPrimaryValidatorSet(ctx)
   250  	if err != nil {
   251  		return nil, 0, err
   252  	}
   253  	if currentHeight < targetHeight {
   254  		return nil, 0, fmt.Errorf("%w with SubnetID = %s: current P-chain height (%d) < requested P-Chain height (%d)",
   255  			errUnfinalizedHeight,
   256  			constants.PrimaryNetworkID,
   257  			currentHeight,
   258  			targetHeight,
   259  		)
   260  	}
   261  
   262  	// Rebuild primary network validators at [targetHeight]
   263  	//
   264  	// Note: Since we are attempting to generate the validator set at
   265  	// [targetHeight], we want to apply the diffs from
   266  	// (targetHeight, currentHeight]. Because the state interface is implemented
   267  	// to be inclusive, we apply diffs in [targetHeight + 1, currentHeight].
   268  	lastDiffHeight := targetHeight + 1
   269  	err = m.state.ApplyValidatorWeightDiffs(
   270  		ctx,
   271  		validatorSet,
   272  		currentHeight,
   273  		lastDiffHeight,
   274  		constants.PlatformChainID,
   275  	)
   276  	if err != nil {
   277  		return nil, 0, err
   278  	}
   279  
   280  	err = m.state.ApplyValidatorPublicKeyDiffs(
   281  		ctx,
   282  		validatorSet,
   283  		currentHeight,
   284  		lastDiffHeight,
   285  	)
   286  	return validatorSet, currentHeight, err
   287  }
   288  
   289  func (m *manager) getCurrentPrimaryValidatorSet(
   290  	ctx context.Context,
   291  ) (map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) {
   292  	primaryMap := m.cfg.Validators.GetMap(constants.PrimaryNetworkID)
   293  	currentHeight, err := m.getCurrentHeight(ctx)
   294  	return primaryMap, currentHeight, err
   295  }
   296  
   297  func (m *manager) makeSubnetValidatorSet(
   298  	ctx context.Context,
   299  	targetHeight uint64,
   300  	subnetID ids.ID,
   301  ) (map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) {
   302  	subnetValidatorSet, primaryValidatorSet, currentHeight, err := m.getCurrentValidatorSets(ctx, subnetID)
   303  	if err != nil {
   304  		return nil, 0, err
   305  	}
   306  	if currentHeight < targetHeight {
   307  		return nil, 0, fmt.Errorf("%w with SubnetID = %s: current P-chain height (%d) < requested P-Chain height (%d)",
   308  			errUnfinalizedHeight,
   309  			subnetID,
   310  			currentHeight,
   311  			targetHeight,
   312  		)
   313  	}
   314  
   315  	// Rebuild subnet validators at [targetHeight]
   316  	//
   317  	// Note: Since we are attempting to generate the validator set at
   318  	// [targetHeight], we want to apply the diffs from
   319  	// (targetHeight, currentHeight]. Because the state interface is implemented
   320  	// to be inclusive, we apply diffs in [targetHeight + 1, currentHeight].
   321  	lastDiffHeight := targetHeight + 1
   322  	err = m.state.ApplyValidatorWeightDiffs(
   323  		ctx,
   324  		subnetValidatorSet,
   325  		currentHeight,
   326  		lastDiffHeight,
   327  		subnetID,
   328  	)
   329  	if err != nil {
   330  		return nil, 0, err
   331  	}
   332  
   333  	// Update the subnet validator set to include the public keys at
   334  	// [currentHeight]. When we apply the public key diffs, we will convert
   335  	// these keys to represent the public keys at [targetHeight]. If the subnet
   336  	// validator is not currently a primary network validator, it doesn't have a
   337  	// key at [currentHeight].
   338  	for nodeID, vdr := range subnetValidatorSet {
   339  		if primaryVdr, ok := primaryValidatorSet[nodeID]; ok {
   340  			vdr.PublicKey = primaryVdr.PublicKey
   341  		} else {
   342  			vdr.PublicKey = nil
   343  		}
   344  	}
   345  
   346  	err = m.state.ApplyValidatorPublicKeyDiffs(
   347  		ctx,
   348  		subnetValidatorSet,
   349  		currentHeight,
   350  		lastDiffHeight,
   351  	)
   352  	return subnetValidatorSet, currentHeight, err
   353  }
   354  
   355  func (m *manager) getCurrentValidatorSets(
   356  	ctx context.Context,
   357  	subnetID ids.ID,
   358  ) (map[ids.NodeID]*validators.GetValidatorOutput, map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) {
   359  	subnetMap := m.cfg.Validators.GetMap(subnetID)
   360  	primaryMap := m.cfg.Validators.GetMap(constants.PrimaryNetworkID)
   361  	currentHeight, err := m.getCurrentHeight(ctx)
   362  	return subnetMap, primaryMap, currentHeight, err
   363  }
   364  
   365  func (m *manager) GetSubnetID(_ context.Context, chainID ids.ID) (ids.ID, error) {
   366  	if chainID == constants.PlatformChainID {
   367  		return constants.PrimaryNetworkID, nil
   368  	}
   369  
   370  	chainTx, _, err := m.state.GetTx(chainID)
   371  	if err != nil {
   372  		return ids.Empty, fmt.Errorf(
   373  			"problem retrieving blockchain %q: %w",
   374  			chainID,
   375  			err,
   376  		)
   377  	}
   378  	chain, ok := chainTx.Unsigned.(*txs.CreateChainTx)
   379  	if !ok {
   380  		return ids.Empty, fmt.Errorf("%q is not a blockchain", chainID)
   381  	}
   382  	return chain.SubnetID, nil
   383  }
   384  
   385  func (m *manager) OnAcceptedBlockID(blkID ids.ID) {
   386  	m.recentlyAccepted.Add(blkID)
   387  }