2022-11-11 07:24:55 -08:00
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 {
2022-11-29 03:26:46 -08:00
finalizedBlocksCache , _ := lru . New ( workerCountTxProcessing * queueSize )
2022-11-11 07:24:55 -08:00
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
}