Skip to content

4.4 Code Geth Block Building

The block building and execution is triggered engine_forkchoiceUpdatedV1, engine_forkchoiceUpdatedV2 or engine_forkchoiceUpdatedV3 when payloadAttributes is supplied. When the consensus client is proposing, it will do a followup call by engine_getPayloadV1, engine_getPayloadV2 or engine_getPayloadV3. The 2 clients communicates on which block by a payloadId, which is a process id inside the execution client side.

Block verification is triggered by engine_newPayloadV1 or engine_newPayloadV2 from consensus client. For consensus client side, refer to 3.5 Code - Lodestar - Block Building#Receive Gossip Topic beacon_block or beacon_block_and_blobs_sidecar. Execution engine should update its local block chain according to fork choice updates. This communication allows the consensus client and the execution client to sync.

Additionally, block execution can also be triggered when the consensus client receives REST call POST /eth/v1/beacon/blocks

Block Proposing and The Interaction with Consensus Client#

When the consensus client calls engine_getPayload, it is handled by getPayload:

func (api *ConsensusAPI) getPayload(payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) {
    log.Trace("Engine API request received", "method", "GetPayload", "id", payloadID)
    data := api.localBlocks.get(payloadID)
    if data == nil {
        return nil, engine.UnknownPayload
    }
    return data, nil
}

The parameter payloadId is the build process id. But how does the consensus client gets to know payloadId? The answer is from notifyForkchoiceUpdate call in https://github.com/ChainSafe/lodestar/blob/v1.5.1/packages/beacon-node/src/execution/engine/http.ts
this.payloadIdCache.add({headBlockHash, finalizedBlockHash, ...payloadAttributesRpc}, payloadId);

and later in https://github.com/ChainSafe/lodestar/blob/v1.5.1/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts

const payloadIdCached = chain.executionEngine.payloadIdCache.get({
    // ...
  });

Therefore block assembly is actually triggered already upon fork choice update.

Fork Choice Handling#

In https://github.com/ethereum/go-ethereum/blob/v1.11.2/eth/catalyst/api.go, the JSON-RPC call(made by 3.5 Code - Lodestar - Block Building#importBlock) to endpoint engine_forkchoiceUpdatedV1, engine_forkchoiceUpdatedV2 or engine_forkchoiceUpdatedV3 all get handled by func (api *ConsensusAPI) forkchoiceUpdated.

Block Building#

I commented the block building part:

func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payloadAttributes *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) {
    // ...
    /* 
    I left out a lot of code above.
    They are to make sure the execution client is synchronized,
    and some special handling related to the merge.
    */
    api.eth.SetSynced()

    // ...

    /* 
    Consensus client tells the block is finalized.
    Later the execution client can prune the fork choices.
    */
    // If the beacon client also advertised a finalized block, mark the local
    // chain final and completely in PoS mode.
    if update.FinalizedBlockHash != (common.Hash{}) {
        if merger := api.eth.Merger(); !merger.PoSFinalized() {
            merger.FinalizePoS()
        }
        // 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)
            return engine.STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("final block not available in database"))
        } else if rawdb.ReadCanonicalHash(api.eth.ChainDb(), finalBlock.NumberU64()) != update.FinalizedBlockHash {
            log.Warn("Final block not in canonical chain", "number", block.NumberU64(), "hash", update.HeadBlockHash)
            return engine.STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("final block not in canonical chain"))
        }
        // Set the finalized block
        api.eth.BlockChain().SetFinalized(finalBlock)
    }

    // ...

    /* 
    If payloadAttributes are supplied, 
    the execution client will start block building,
    and returns the process id.
    */
    // If payload generation was requested, create a new block to be potentially
    // sealed by the beacon client. The payload will be requested later, and we
    // will replace it arbitrarily many times in between.
    if payloadAttributes != nil {
        args := &miner.BuildPayloadArgs{
            Parent:       update.HeadBlockHash,
            Timestamp:    payloadAttributes.Timestamp,
            FeeRecipient: payloadAttributes.SuggestedFeeRecipient,
            Random:       payloadAttributes.Random,
            Withdrawals:  payloadAttributes.Withdrawals,
        }
        id := args.Id()
        // If we already are busy generating this work, then we do not need
        // to start a second process.
        if api.localBlocks.has(id) {
            return valid(&id), nil
        }
        payload, err := api.eth.Miner().BuildPayload(args)
        if err != nil {
            log.Error("Failed to build payload", "err", err)
            return valid(nil), engine.InvalidPayloadAttributes.With(err)
        }
        /* 
        store the block in 'localBlocks',
        which will be used later by 'getPayload' call
        */
        api.localBlocks.put(id, payload)
        return valid(&id), nil
    }
    return valid(nil), nil
}

engine_getPayloadV1, engine_getPayloadV2 and engine_getPayloadV3 are all handled by func getPayload:

data := api.localBlocks.get(payloadID)

, where it retrieves the local block by payloadId communicated during the fork choice update.

Block Execution#

Block execution is also triggered upon fork choice update.

Code trace:
1. https://github.com/ethereum/go-ethereum/blob/v1.11.2/eth/catalyst/api.go, forkchoiceUpdated -> api.eth.Miner().BuildPayload
2. https://github.com/ethereum/go-ethereum/blob/v1.11.2/miner/miner.go, BuildPayload -> miner.worker.buildPayload
3. https://github.com/ethereum/go-ethereum/blob/v1.11.2/miner/payload_building.go, w.getSealingBlock, where 'sealing' means the proces of validating and adding a block to the blockchain.
4. https://github.com/ethereum/go-ethereum/blob/v1.11.2/miner/worker.go

worker.go implements multiple go routines that communicates through multiple event channel. Therefore block building is asynchronous.
1. getSealingBlock sends a request to getWorkCh channel:

case w.getWorkCh <- req:

2. The message gets picked up in mainLoop:
case req := <-w.getWorkCh:
     block, fees, err := w.generateWork(req.params)

3. generateWork calls fillTransactions
1. It calls commitTransactions -> applyTransaction
2. https://github.com/ethereum/go-ethereum/blob/v1.11.2/core/state_processor.go, applyTransaction, the further detail will be discussed in 4.5 Code - Geth - Transactions and EVM
4. generateWork then w.engine.FinalizeAndAssemble
1. https://github.com/ethereum/go-ethereum/blob/v1.11.2/consensus/beacon/consensus.go, FinalizeAndAssemble, where it processes withdraws, and finalize the block. (I skipped clique and Ethash module to focus on proof-of-stake)

New Payload Handling#

It is handled by func (api *ConsensusAPI) newPayload(params engine.ExecutableData). Simplified snippet:

/* Happy flow: when the block is indeed there */
if block := api.eth.BlockChain().GetBlockByHash(params.BlockHash); block != nil {
    log.Warn("Ignoring already known beacon payload", "number", params.Number, "hash", params.BlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)))
    hash := block.Hash()
    return engine.PayloadStatusV1{Status: engine.VALID, LatestValidHash: &hash}, nil
}
// ...
/* Less happy flow: when the block is not (yet) there, wait */
parent := api.eth.BlockChain().GetBlock(block.ParentHash(), block.NumberU64()-1)
if parent == nil {
    return api.delayPayloadImport(block)
}

It responds Status: engine.VALID, Status: engine.SYNCING or Status: engine.INVALID, which will be handled by the consensus client.