From e6fa102eb08c2b83ab75e85ca7860eea3a10dab0 Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Wed, 18 May 2022 16:30:42 +0200 Subject: [PATCH] core, eth, internal, rpc: implement final block (#24282) * eth: core: implement finalized block * eth/catalyst: fix final block * eth/catalyst: update finalized head gauge * internal/jsre/deps: updated web3.js to allow for finalized block * eth/catalyst: make sure only one thread can call fcu * eth/catalyst: nitpicks * eth/catalyst: use plain mutex * eth: nitpicks --- core/blockchain.go | 34 +++++++++++++++++++++++++++++----- core/blockchain_reader.go | 6 ++++++ core/rawdb/accessors_chain.go | 16 ++++++++++++++++ core/rawdb/database.go | 4 ++-- core/rawdb/schema.go | 3 +++ eth/api.go | 4 ++++ eth/api_backend.go | 6 ++++++ eth/catalyst/api.go | 13 +++++++++++-- eth/catalyst/api_test.go | 5 ++++- internal/jsre/deps/web3.js | 2 +- rpc/types.go | 16 +++++++++++++--- tests/testdata | 2 +- 12 files changed, 96 insertions(+), 15 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 7ab15d7f4..5ac12303c 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -47,9 +47,10 @@ import ( ) var ( - headBlockGauge = metrics.NewRegisteredGauge("chain/head/block", nil) - headHeaderGauge = metrics.NewRegisteredGauge("chain/head/header", nil) - headFastBlockGauge = metrics.NewRegisteredGauge("chain/head/receipt", nil) + headBlockGauge = metrics.NewRegisteredGauge("chain/head/block", nil) + headHeaderGauge = metrics.NewRegisteredGauge("chain/head/header", nil) + headFastBlockGauge = metrics.NewRegisteredGauge("chain/head/receipt", nil) + headFinalizedBlockGauge = metrics.NewRegisteredGauge("chain/head/finalized", nil) accountReadTimer = metrics.NewRegisteredTimer("chain/account/reads", nil) accountHashTimer = metrics.NewRegisteredTimer("chain/account/hashes", nil) @@ -187,8 +188,9 @@ type BlockChain struct { // Readers don't need to take it, they can just read the database. chainmu *syncx.ClosableMutex - currentBlock atomic.Value // Current head of the block chain - currentFastBlock atomic.Value // Current head of the fast-sync chain (may be above the block chain!) + currentBlock atomic.Value // Current head of the block chain + currentFastBlock atomic.Value // Current head of the fast-sync chain (may be above the block chain!) + currentFinalizedBlock atomic.Value // Current finalized head stateCache state.Database // State database to reuse between imports (contains state cache) bodyCache *lru.Cache // Cache for the most recent block bodies @@ -264,6 +266,7 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par var nilBlock *types.Block bc.currentBlock.Store(nilBlock) bc.currentFastBlock.Store(nilBlock) + bc.currentFinalizedBlock.Store(nilBlock) // Initialize the chain with ancient data if it isn't empty. var txIndexBlock uint64 @@ -460,8 +463,17 @@ func (bc *BlockChain) loadLastState() error { headFastBlockGauge.Update(int64(block.NumberU64())) } } + + // Restore the last known finalized block + if head := rawdb.ReadFinalizedBlockHash(bc.db); head != (common.Hash{}) { + if block := bc.GetBlockByHash(head); block != nil { + bc.currentFinalizedBlock.Store(block) + headFinalizedBlockGauge.Update(int64(block.NumberU64())) + } + } // Issue a status log for the user currentFastBlock := bc.CurrentFastBlock() + currentFinalizedBlock := bc.CurrentFinalizedBlock() headerTd := bc.GetTd(currentHeader.Hash(), currentHeader.Number.Uint64()) blockTd := bc.GetTd(currentBlock.Hash(), currentBlock.NumberU64()) @@ -470,6 +482,11 @@ func (bc *BlockChain) loadLastState() error { log.Info("Loaded most recent local header", "number", currentHeader.Number, "hash", currentHeader.Hash(), "td", headerTd, "age", common.PrettyAge(time.Unix(int64(currentHeader.Time), 0))) log.Info("Loaded most recent local full block", "number", currentBlock.Number(), "hash", currentBlock.Hash(), "td", blockTd, "age", common.PrettyAge(time.Unix(int64(currentBlock.Time()), 0))) log.Info("Loaded most recent local fast block", "number", currentFastBlock.Number(), "hash", currentFastBlock.Hash(), "td", fastTd, "age", common.PrettyAge(time.Unix(int64(currentFastBlock.Time()), 0))) + + if currentFinalizedBlock != nil { + finalTd := bc.GetTd(currentFinalizedBlock.Hash(), currentFinalizedBlock.NumberU64()) + log.Info("Loaded most recent local finalized block", "number", currentFinalizedBlock.Number(), "hash", currentFinalizedBlock.Hash(), "td", finalTd, "age", common.PrettyAge(time.Unix(int64(currentFinalizedBlock.Time()), 0))) + } if pivot := rawdb.ReadLastPivotNumber(bc.db); pivot != nil { log.Info("Loaded last fast-sync pivot marker", "number", *pivot) } @@ -484,6 +501,13 @@ func (bc *BlockChain) SetHead(head uint64) error { return err } +// SetFinalized sets the finalized block. +func (bc *BlockChain) SetFinalized(block *types.Block) { + bc.currentFinalizedBlock.Store(block) + rawdb.WriteFinalizedBlockHash(bc.db, block.Hash()) + headFinalizedBlockGauge.Update(int64(block.NumberU64())) +} + // setHeadBeyondRoot rewinds the local chain to a new head with the extra condition // that the rewind must pass the specified state root. This method is meant to be // used when rewinding with snapshots enabled to ensure that we go back further than diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 9e966df4e..b8d4233c6 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -49,6 +49,12 @@ func (bc *BlockChain) CurrentFastBlock() *types.Block { return bc.currentFastBlock.Load().(*types.Block) } +// CurrentFinalizedBlock retrieves the current finalized block of the canonical +// chain. The block is retrieved from the blockchain's internal cache. +func (bc *BlockChain) CurrentFinalizedBlock() *types.Block { + return bc.currentFinalizedBlock.Load().(*types.Block) +} + // HasHeader checks if a block header is present in the database or not, caching // it if present. func (bc *BlockChain) HasHeader(hash common.Hash, number uint64) bool { diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index c1a19926e..8ea2e2ca7 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -216,6 +216,22 @@ func WriteHeadFastBlockHash(db ethdb.KeyValueWriter, hash common.Hash) { } } +// ReadFinalizedBlockHash retrieves the hash of the finalized block. +func ReadFinalizedBlockHash(db ethdb.KeyValueReader) common.Hash { + data, _ := db.Get(headFinalizedBlockKey) + if len(data) == 0 { + return common.Hash{} + } + return common.BytesToHash(data) +} + +// WriteFinalizedBlockHash stores the hash of the finalized block. +func WriteFinalizedBlockHash(db ethdb.KeyValueWriter, hash common.Hash) { + if err := db.Put(headFinalizedBlockKey, hash.Bytes()); err != nil { + log.Crit("Failed to store last finalized block's hash", "err", err) + } +} + // ReadLastPivotNumber retrieves the number of the last pivot block. If the node // full synced, the last pivot will always be nil. func ReadLastPivotNumber(db ethdb.KeyValueReader) *uint64 { diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 63b6da760..2b870d16d 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -418,8 +418,8 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { default: var accounted bool for _, meta := range [][]byte{ - databaseVersionKey, headHeaderKey, headBlockKey, headFastBlockKey, lastPivotKey, - fastTrieProgressKey, snapshotDisabledKey, SnapshotRootKey, snapshotJournalKey, + databaseVersionKey, headHeaderKey, headBlockKey, headFastBlockKey, headFinalizedBlockKey, + lastPivotKey, fastTrieProgressKey, snapshotDisabledKey, SnapshotRootKey, snapshotJournalKey, snapshotGeneratorKey, snapshotRecoveryKey, txIndexTailKey, fastTxLookupLimitKey, uncleanShutdownKey, badBlockKey, transitionStatusKey, skeletonSyncStatusKey, } { diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 08f373488..041c9f044 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -39,6 +39,9 @@ var ( // headFastBlockKey tracks the latest known incomplete block's hash during fast sync. headFastBlockKey = []byte("LastFast") + // headFinalizedBlockKey tracks the latest known finalized block hash. + headFinalizedBlockKey = []byte("LastFinalized") + // lastPivotKey tracks the last pivot block used by fast sync (to reenable on sethead). lastPivotKey = []byte("LastPivot") diff --git a/eth/api.go b/eth/api.go index f81dfa922..ef69acb76 100644 --- a/eth/api.go +++ b/eth/api.go @@ -285,6 +285,8 @@ func (api *PublicDebugAPI) DumpBlock(blockNr rpc.BlockNumber) (state.Dump, error var block *types.Block if blockNr == rpc.LatestBlockNumber { block = api.eth.blockchain.CurrentBlock() + } else if blockNr == rpc.FinalizedBlockNumber { + block = api.eth.blockchain.CurrentFinalizedBlock() } else { block = api.eth.blockchain.GetBlockByNumber(uint64(blockNr)) } @@ -373,6 +375,8 @@ func (api *PublicDebugAPI) AccountRange(blockNrOrHash rpc.BlockNumberOrHash, sta var block *types.Block if number == rpc.LatestBlockNumber { block = api.eth.blockchain.CurrentBlock() + } else if number == rpc.FinalizedBlockNumber { + block = api.eth.blockchain.CurrentFinalizedBlock() } else { block = api.eth.blockchain.GetBlockByNumber(uint64(number)) } diff --git a/eth/api_backend.go b/eth/api_backend.go index 8eb6c02af..f942710e2 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -73,6 +73,9 @@ func (b *EthAPIBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumb if number == rpc.LatestBlockNumber { return b.eth.blockchain.CurrentBlock().Header(), nil } + if number == rpc.FinalizedBlockNumber { + return b.eth.blockchain.CurrentFinalizedBlock().Header(), nil + } return b.eth.blockchain.GetHeaderByNumber(uint64(number)), nil } @@ -107,6 +110,9 @@ func (b *EthAPIBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumbe if number == rpc.LatestBlockNumber { return b.eth.blockchain.CurrentBlock(), nil } + if number == rpc.FinalizedBlockNumber { + return b.eth.blockchain.CurrentFinalizedBlock(), nil + } return b.eth.blockchain.GetBlockByNumber(uint64(number)), nil } diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 152576660..81de68fbe 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -22,6 +22,7 @@ import ( "encoding/binary" "errors" "fmt" + "sync" "time" "github.com/ethereum/go-ethereum/common" @@ -60,6 +61,8 @@ type ConsensusAPI struct { eth *eth.Ethereum remoteBlocks *headerQueue // Cache of remote payloads received localBlocks *payloadQueue // Cache of local payloads generated + // Lock for the forkChoiceUpdated method + forkChoiceLock sync.Mutex } // NewConsensusAPI creates a new consensus api for the given backend. @@ -86,11 +89,15 @@ func NewConsensusAPI(eth *eth.Ethereum) *ConsensusAPI { // If there are payloadAttributes: // we try to assemble a block with the payloadAttributes and return its payloadID func (api *ConsensusAPI) ForkchoiceUpdatedV1(update beacon.ForkchoiceStateV1, payloadAttributes *beacon.PayloadAttributesV1) (beacon.ForkChoiceResponse, error) { + api.forkChoiceLock.Lock() + defer api.forkChoiceLock.Unlock() + log.Trace("Engine API request received", "method", "ForkchoiceUpdated", "head", update.HeadBlockHash, "finalized", update.FinalizedBlockHash, "safe", update.SafeBlockHash) if update.HeadBlockHash == (common.Hash{}) { log.Warn("Forkchoice requested update to zero hash") return beacon.STATUS_INVALID, nil // TODO(karalabe): Why does someone send us this? } + // Check whether we have the block yet in our database or not. If not, we'll // need to either trigger a sync, or to reject this forkchoice update for a // reason. @@ -154,7 +161,7 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV1(update beacon.ForkchoiceStateV1, pa if merger := api.eth.Merger(); !merger.PoSFinalized() { merger.FinalizePoS() } - // TODO (MariusVanDerWijden): If the finalized block is not in our canonical tree, somethings wrong + // If the finalized block is not in our canonical tree, somethings wrong finalBlock := api.eth.BlockChain().GetBlockByHash(update.FinalizedBlockHash) if finalBlock == nil { log.Warn("Final block not available in database", "hash", update.FinalizedBlockHash) @@ -163,8 +170,10 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV1(update beacon.ForkchoiceStateV1, pa log.Warn("Final block not in canonical chain", "number", block.NumberU64(), "hash", update.HeadBlockHash) return beacon.STATUS_INVALID, errors.New("final block not canonical") } + // Set the finalized block + api.eth.BlockChain().SetFinalized(finalBlock) } - // TODO (MariusVanDerWijden): Check if the safe block hash is in our canonical tree, if not somethings wrong + // Check if the safe block hash is in our canonical tree, if not somethings wrong if update.SafeBlockHash != (common.Hash{}) { safeBlock := api.eth.BlockChain().GetBlockByHash(update.SafeBlockHash) if safeBlock == nil { diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index 2ae6d2cd5..657819a11 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -476,7 +476,10 @@ func TestFullAPI(t *testing.T) { t.Fatalf("Failed to insert block: %v", err) } if ethservice.BlockChain().CurrentBlock().NumberU64() != payload.Number { - t.Fatalf("Chain head should be updated") + t.Fatal("Chain head should be updated") + } + if ethservice.BlockChain().CurrentFinalizedBlock().NumberU64() != payload.Number-1 { + t.Fatal("Finalized block should be updated") } parent = ethservice.BlockChain().CurrentBlock() } diff --git a/internal/jsre/deps/web3.js b/internal/jsre/deps/web3.js index 69686ce92..f82d93bdc 100644 --- a/internal/jsre/deps/web3.js +++ b/internal/jsre/deps/web3.js @@ -3696,7 +3696,7 @@ var outputBigNumberFormatter = function (number) { }; var isPredefinedBlockNumber = function (blockNumber) { - return blockNumber === 'latest' || blockNumber === 'pending' || blockNumber === 'earliest'; + return blockNumber === 'latest' || blockNumber === 'pending' || blockNumber === 'earliest' || blockNumber === 'finalized'; }; var inputDefaultBlockNumberFormatter = function (blockNumber) { diff --git a/rpc/types.go b/rpc/types.go index 46b08caf6..f4d05be48 100644 --- a/rpc/types.go +++ b/rpc/types.go @@ -61,9 +61,10 @@ type jsonWriter interface { type BlockNumber int64 const ( - PendingBlockNumber = BlockNumber(-2) - LatestBlockNumber = BlockNumber(-1) - EarliestBlockNumber = BlockNumber(0) + FinalizedBlockNumber = BlockNumber(-3) + PendingBlockNumber = BlockNumber(-2) + LatestBlockNumber = BlockNumber(-1) + EarliestBlockNumber = BlockNumber(0) ) // UnmarshalJSON parses the given JSON fragment into a BlockNumber. It supports: @@ -88,6 +89,9 @@ func (bn *BlockNumber) UnmarshalJSON(data []byte) error { case "pending": *bn = PendingBlockNumber return nil + case "finalized": + *bn = FinalizedBlockNumber + return nil } blckNum, err := hexutil.DecodeUint64(input) @@ -112,6 +116,8 @@ func (bn BlockNumber) MarshalText() ([]byte, error) { return []byte("latest"), nil case PendingBlockNumber: return []byte("pending"), nil + case FinalizedBlockNumber: + return []byte("finalized"), nil default: return hexutil.Uint64(bn).MarshalText() } @@ -158,6 +164,10 @@ func (bnh *BlockNumberOrHash) UnmarshalJSON(data []byte) error { bn := PendingBlockNumber bnh.BlockNumber = &bn return nil + case "finalized": + bn := FinalizedBlockNumber + bnh.BlockNumber = &bn + return nil default: if len(input) == 66 { hash := common.Hash{} diff --git a/tests/testdata b/tests/testdata index 092a8834d..a380655e5 160000 --- a/tests/testdata +++ b/tests/testdata @@ -1 +1 @@ -Subproject commit 092a8834dc445e683103689d6f0e75a5d380a190 +Subproject commit a380655e5ffab1a5ea0f4d860224bdb19013f06a