wormhole/node/pkg/watchers/near/finalizer.go

130 lines
4.8 KiB
Go

package near
import (
"context"
"errors"
"github.com/certusone/wormhole/node/pkg/watchers/near/nearapi"
lru "github.com/hashicorp/golang-lru"
"go.uber.org/zap"
)
type Finalizer struct {
// internal cache of which blocks have been finalized, mapping blockHack => blockTimestamp.
// The timestamp is persisted because we'll need it again later.
// thread-safe
finalizedBlocksCache *lru.Cache
nearAPI nearapi.NearApi
eventChan chan eventType
mainnet bool
}
func newFinalizer(eventChan chan eventType, nearAPI nearapi.NearApi, mainnet bool) Finalizer {
finalizedBlocksCache, _ := lru.New(workerCountTxProcessing * queueSize)
return Finalizer{
finalizedBlocksCache,
nearAPI,
eventChan,
mainnet,
}
}
func (f Finalizer) isFinalizedCached(logger *zap.Logger, ctx context.Context, blockHash string) (nearapi.BlockHeader, bool) {
if err := nearapi.IsWellFormedHash(blockHash); err != nil {
// SECURITY defense-in-depth: check if block hash is well-formed
logger.Error("blockHash invalid", zap.String("error_type", "invalid_hash"), zap.String("blockHash", blockHash), zap.Error(err))
return nearapi.BlockHeader{}, false
}
if b, ok := f.finalizedBlocksCache.Get(blockHash); ok {
blockHeader := b.(nearapi.BlockHeader)
// SECURITY In blocks < 74473147 message timestamps were computed differently and we don't want to re-observe these messages
if !f.mainnet || blockHeader.Height > 74473147 {
return blockHeader, true
}
}
return nearapi.BlockHeader{}, false
}
// isFinalized() checks if a block is finalized by looking at the local cache first. If there is an error during execution it returns false.
// If it is not found in the cache, we walk forward up to nearBlockchainMaxGaps blocks by height,
// starting at the block's height+2 and check if their value of "last_final_block" matches
// the block in question.
// we start at height+2 because NEAR consensus takes at least two blocks to reach finality.
func (f Finalizer) isFinalized(logger *zap.Logger, ctx context.Context, queriedBlockHash string) (nearapi.BlockHeader, bool) {
logger.Debug("checking block finalization", zap.String("method", "isFinalized"), zap.String("parameters", queriedBlockHash))
// check cache first
if block, ok := f.isFinalizedCached(logger, ctx, queriedBlockHash); ok {
return block, true
}
logger.Debug("block finalization cache miss", zap.String("method", "isFinalized"), zap.String("parameters", queriedBlockHash))
f.eventChan <- EVENT_FINALIZED_CACHE_MISS
queriedBlock, err := f.nearAPI.GetBlock(ctx, queriedBlockHash)
if err != nil {
return nearapi.BlockHeader{}, false
}
startingBlockHeight := queriedBlock.Header.Height
for i := 0; i < nearBlockchainMaxGaps; i++ {
blockHeightToQuery := startingBlockHeight + uint64(2+i) // we start at height+2 because NEAR consensus takes at least two blocks to reach finality.
block, err := f.nearAPI.GetBlockByHeight(ctx, blockHeightToQuery)
if err != nil {
break
}
// SECURITY defense-in-depth check
if block.Header.Height != blockHeightToQuery {
// SECURITY violation: Block height is different than what we queried for
logger.Panic("NEAR RPC Inconsistent", zap.String("error_type", "nearapi_inconsistent"), zap.String("inconsistency", "block_height_result_different_from_query"))
}
someFinalBlockHash := block.Header.LastFinalBlock
// SECURITY defense-in-depth check
if someFinalBlockHash == "" || block.Header.Height == 0 || block.Header.Timestamp == 0 {
break
}
if queriedBlockHash == someFinalBlockHash {
f.setFinalized(logger, ctx, queriedBlock.Header)
// block was marked as finalized in the cache, so this should succeed now.
// We don't return directly because setFinalized() contains some sanity checks.
return f.isFinalizedCached(logger, ctx, queriedBlockHash)
}
}
// it seems like the block has not been finalized yet
return nearapi.BlockHeader{}, false
}
func (f Finalizer) setFinalized(logger *zap.Logger, ctx context.Context, blockHeader nearapi.BlockHeader) {
// SECURITY defense-in-depth: don't cache obviously corrupted data.
if nearapi.IsWellFormedHash(blockHeader.Hash) != nil || blockHeader.Timestamp == 0 || blockHeader.Height == 0 {
return
}
f.finalizedBlocksCache.Add(blockHeader.Hash, blockHeader)
}
func (f Finalizer) setFinalizedHash(logger *zap.Logger, ctx context.Context, blockHash string) error { //nolint Ignore unused function for now; might come in handy later
logger.Debug("setFinalizedHash()", zap.String("blockHash", blockHash))
// SECURITY defense-in-depth: don't cache obviously corrupted data.
if nearapi.IsWellFormedHash(blockHash) != nil {
return errors.New("blockHash length is not the expected length")
}
block, err := f.nearAPI.GetBlock(ctx, blockHash)
if err != nil {
return err
}
f.finalizedBlocksCache.Add(blockHash, block.Header)
return nil
}