diff --git a/builder/builder.go b/builder/builder.go index 44201bcdeb..61dcf404c8 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -319,10 +319,13 @@ func (b *Builder) submitCapellaBlock(block *types.Block, blockValue *big.Int, or } if b.dryRun { - err = b.validator.ValidateBuilderSubmissionV2(&blockvalidation.BuilderBlockValidationRequestV2{SubmitBlockRequest: blockSubmitReq, RegisteredGasLimit: vd.GasLimit}) + resp, err := b.validator.ValidateBuilderSubmissionV2(&blockvalidation.BuilderBlockValidationRequestV2{SubmitBlockRequest: blockSubmitReq, RegisteredGasLimit: vd.GasLimit}) if err != nil { log.Error("could not validate block for capella", "err", err) } + if resp.NewGasLimit != payload.GasLimit { + log.Error("gas limit adjusted needed for capella", "gasLimit", payload.GasLimit, "newGasLimit", resp.NewGasLimit) + } } else { go b.ds.ConsumeBuiltBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, usedSbundles, &blockBidMsg) err = b.relay.SubmitBlockCapella(&blockSubmitReq, vd) diff --git a/core/blockchain.go b/core/blockchain.go index 8e5372c905..67fa0b2bed 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2494,6 +2494,28 @@ func (bc *BlockChain) SetBlockValidatorAndProcessorForTesting(v Validator, p Pro bc.processor = p } +// AdjustedGasLimit calculates the gas limit for the given block and returns +// the calculated limit. +func (bc *BlockChain) AdjustedGasLimit(block *types.Block, registeredGasLimit uint64) (uint64, error) { + header := block.Header() + if err := bc.engine.VerifyHeader(bc, header, true); err != nil { + return 0, err + } + + current := bc.CurrentBlock() + reorg, err := bc.forker.ReorgNeeded(current, header) + if err == nil && reorg { + return 0, errors.New("block requires a reorg") + } + + parent := bc.GetHeader(block.ParentHash(), block.NumberU64()-1) + if parent == nil { + return 0, errors.New("parent not found") + } + + return utils.CalcGasLimit(parent.GasLimit, registeredGasLimit), nil +} + // ValidatePayload validates the payload of the block. // It returns nil if the payload is valid, otherwise it returns an error. // - `useBalanceDiffProfit` if set to false, proposer payment is assumed to be in the last transaction of the block diff --git a/eth/block-validation/api.go b/eth/block-validation/api.go index 58ad8dc93d..41ed786254 100644 --- a/eth/block-validation/api.go +++ b/eth/block-validation/api.go @@ -228,32 +228,42 @@ func (r *BuilderBlockValidationRequestV2) UnmarshalJSON(data []byte) error { return nil } -func (api *BlockValidationAPI) ValidateBuilderSubmissionV2(params *BuilderBlockValidationRequestV2) error { +// Response object for the block validation v2 request. Contains a gas limit +// and a block hash that are valid for the `RegisteredGasLimit` property of the +// request. This is to allow post-building adjustment of the gas limit in the +// built block. +type BuilderBlockValidationResponseV2 struct { + NewGasLimit uint64 `json:"new_gas_limit,string"` + NewBlockHash string `json:"new_block_hash"` +} + +func (api *BlockValidationAPI) ValidateBuilderSubmissionV2(params *BuilderBlockValidationRequestV2) (*BuilderBlockValidationResponseV2, error) { // TODO: fuzztest, make sure the validation is sound // TODO: handle context! if params.ExecutionPayload == nil { - return errors.New("nil execution payload") + return nil, errors.New("nil execution payload") } + payload := params.ExecutionPayload block, err := engine.ExecutionPayloadV2ToBlock(payload) if err != nil { - return err + return nil, err } if params.Message.ParentHash != phase0.Hash32(block.ParentHash()) { - return fmt.Errorf("incorrect ParentHash %s, expected %s", params.Message.ParentHash.String(), block.ParentHash().String()) + return nil, fmt.Errorf("incorrect ParentHash %s, expected %s", params.Message.ParentHash.String(), block.ParentHash().String()) } if params.Message.BlockHash != phase0.Hash32(block.Hash()) { - return fmt.Errorf("incorrect BlockHash %s, expected %s", params.Message.BlockHash.String(), block.Hash().String()) + return nil, fmt.Errorf("incorrect BlockHash %s, expected %s", params.Message.BlockHash.String(), block.Hash().String()) } if params.Message.GasLimit != block.GasLimit() { - return fmt.Errorf("incorrect GasLimit %d, expected %d", params.Message.GasLimit, block.GasLimit()) + return nil, fmt.Errorf("incorrect GasLimit %d, expected %d", params.Message.GasLimit, block.GasLimit()) } if params.Message.GasUsed != block.GasUsed() { - return fmt.Errorf("incorrect GasUsed %d, expected %d", params.Message.GasUsed, block.GasUsed()) + return nil, fmt.Errorf("incorrect GasUsed %d, expected %d", params.Message.GasUsed, block.GasUsed()) } feeRecipient := common.BytesToAddress(params.Message.ProposerFeeRecipient[:]) @@ -263,13 +273,13 @@ func (api *BlockValidationAPI) ValidateBuilderSubmissionV2(params *BuilderBlockV var tracer *logger.AccessListTracer = nil if api.accessVerifier != nil { if err := api.accessVerifier.isBlacklisted(block.Coinbase()); err != nil { - return err + return nil, err } if err := api.accessVerifier.isBlacklisted(feeRecipient); err != nil { - return err + return nil, err } if err := api.accessVerifier.verifyTransactions(types.LatestSigner(api.eth.BlockChain().Config()), block.Transactions()); err != nil { - return err + return nil, err } isPostMerge := true // the call is PoS-native precompiles := vm.ActivePrecompiles(api.eth.APIBackend.ChainConfig().Rules(new(big.Int).SetUint64(params.ExecutionPayload.BlockNumber), isPostMerge, params.ExecutionPayload.Timestamp)) @@ -277,18 +287,39 @@ func (api *BlockValidationAPI) ValidateBuilderSubmissionV2(params *BuilderBlockV vmconfig = vm.Config{Tracer: tracer, Debug: true} } - err = api.eth.BlockChain().ValidatePayload(block, feeRecipient, expectedProfit, params.RegisteredGasLimit, vmconfig, api.useBalanceDiffProfit) + bc := api.eth.BlockChain() + + // After validation of the message, we modify the block's gas limit + adjustedGasLimit, err := bc.AdjustedGasLimit(block, params.RegisteredGasLimit) + if err != nil { + return nil, err + } + // If the gas limit was adjusted, we need to regenerate the block + // as mutating them is generally unsupported + if payload.GasLimit != adjustedGasLimit { + payload.GasLimit = adjustedGasLimit + block, err = engine.ExecutionPayloadV2ToBlock(payload) + if err != nil { + return nil, err + } + } + + err = bc.ValidatePayload(block, feeRecipient, expectedProfit, params.RegisteredGasLimit, vmconfig, api.useBalanceDiffProfit) if err != nil { log.Error("invalid payload", "hash", payload.BlockHash.String(), "number", payload.BlockNumber, "parentHash", payload.ParentHash.String(), "err", err) - return err + return nil, err } if api.accessVerifier != nil && tracer != nil { if err := api.accessVerifier.verifyTraces(tracer); err != nil { - return err + return nil, err } } log.Info("validated block", "hash", block.Hash(), "number", block.NumberU64(), "parentHash", block.ParentHash()) - return nil + + return &BuilderBlockValidationResponseV2{ + NewGasLimit: block.GasLimit(), + NewBlockHash: block.Hash().String(), + }, nil } diff --git a/eth/block-validation/api_test.go b/eth/block-validation/api_test.go index 5a925f4316..9cfa004457 100644 --- a/eth/block-validation/api_test.go +++ b/eth/block-validation/api_test.go @@ -246,16 +246,16 @@ func TestValidateBuilderSubmissionV2(t *testing.T) { WithdrawalsRoot: withdrawalsRoot, } - require.ErrorContains(t, api.ValidateBuilderSubmissionV2(blockRequest), "inaccurate payment") + _, err = api.ValidateBuilderSubmissionV2(blockRequest) + require.ErrorContains(t, err, "inaccurate payment") blockRequest.Message.Value = uint256.NewInt(149842511727212) - require.NoError(t, api.ValidateBuilderSubmissionV2(blockRequest)) + _, err = api.ValidateBuilderSubmissionV2(blockRequest) + require.NoError(t, err) blockRequest.Message.GasLimit += 1 blockRequest.ExecutionPayload.GasLimit += 1 updatePayloadHashV2(t, blockRequest) - require.ErrorContains(t, api.ValidateBuilderSubmissionV2(blockRequest), "incorrect gas limit set") - blockRequest.Message.GasLimit -= 1 blockRequest.ExecutionPayload.GasLimit -= 1 updatePayloadHashV2(t, blockRequest) @@ -267,7 +267,9 @@ func TestValidateBuilderSubmissionV2(t *testing.T) { testAddr: {}, }, } - require.ErrorContains(t, api.ValidateBuilderSubmissionV2(blockRequest), "transaction from blacklisted address 0x71562b71999873DB5b286dF957af199Ec94617F7") + + _, err = api.ValidateBuilderSubmissionV2(blockRequest) + require.ErrorContains(t, err, "transaction from blacklisted address 0x71562b71999873DB5b286dF957af199Ec94617F7") // Test tx to blacklisted address api.accessVerifier = &AccessVerifier{ @@ -275,12 +277,15 @@ func TestValidateBuilderSubmissionV2(t *testing.T) { {0x16}: {}, }, } - require.ErrorContains(t, api.ValidateBuilderSubmissionV2(blockRequest), "transaction to blacklisted address 0x1600000000000000000000000000000000000000") + + _, err = api.ValidateBuilderSubmissionV2(blockRequest) + require.ErrorContains(t, err, "transaction to blacklisted address 0x1600000000000000000000000000000000000000") api.accessVerifier = nil blockRequest.Message.GasUsed = 10 - require.ErrorContains(t, api.ValidateBuilderSubmissionV2(blockRequest), "incorrect GasUsed 10, expected 119996") + _, err = api.ValidateBuilderSubmissionV2(blockRequest) + require.ErrorContains(t, err, "incorrect GasUsed 10, expected 119996") blockRequest.Message.GasUsed = execData.GasUsed newTestKey, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f290") @@ -297,7 +302,8 @@ func TestValidateBuilderSubmissionV2(t *testing.T) { copy(invalidPayload.ReceiptsRoot[:], hexutil.MustDecode("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")[:32]) blockRequest.ExecutionPayload = invalidPayload updatePayloadHashV2(t, blockRequest) - require.ErrorContains(t, api.ValidateBuilderSubmissionV2(blockRequest), "could not apply tx 4", "insufficient funds for gas * price + value") + _, err = api.ValidateBuilderSubmissionV2(blockRequest) + require.ErrorContains(t, err, "could not apply tx 4", "insufficient funds for gas * price + value") } func updatePayloadHash(t *testing.T, blockRequest *BuilderBlockValidationRequest) { @@ -703,21 +709,24 @@ func TestValidateBuilderSubmissionV2_CoinbasePaymentDefault(t *testing.T) { req, err := executableDataToBlockValidationRequest(execData, testValidatorAddr, value, withdrawalsRoot) require.NoError(t, err) - require.NoError(t, api.ValidateBuilderSubmissionV2(req)) + _, err = api.ValidateBuilderSubmissionV2(req) + require.NoError(t, err) // try to claim less profit than expected, should work value.SetUint64(expectedProfit - 1) req, err = executableDataToBlockValidationRequest(execData, testValidatorAddr, value, withdrawalsRoot) require.NoError(t, err) - require.NoError(t, api.ValidateBuilderSubmissionV2(req)) + _, err = api.ValidateBuilderSubmissionV2(req) + require.NoError(t, err) // try to claim more profit than expected, should fail value.SetUint64(expectedProfit + 1) req, err = executableDataToBlockValidationRequest(execData, testValidatorAddr, value, withdrawalsRoot) require.NoError(t, err) - require.ErrorContains(t, api.ValidateBuilderSubmissionV2(req), "payment") + _, err = api.ValidateBuilderSubmissionV2(req) + require.ErrorContains(t, err, "payment") } func TestValidateBuilderSubmissionV2_Blocklist(t *testing.T) { @@ -777,8 +786,10 @@ func TestValidateBuilderSubmissionV2_Blocklist(t *testing.T) { req, err := executableDataToBlockValidationRequest(execData, testValidatorAddr, common.Big0, withdrawalsRoot) require.NoError(t, err) - require.NoError(t, apiNoBlock.ValidateBuilderSubmissionV2(req)) - require.ErrorContains(t, apiWithBlock.ValidateBuilderSubmissionV2(req), "blacklisted") + _, err = apiNoBlock.ValidateBuilderSubmissionV2(req) + require.NoError(t, err) + _, err = apiWithBlock.ValidateBuilderSubmissionV2(req) + require.ErrorContains(t, err, "blacklisted") }) } }