diff --git a/builder/builder.go b/builder/builder.go index bcdab8fc1..bd3769913 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -279,7 +279,7 @@ func (b *Builder) onSealedBlock(opts SubmitBlockOpts) error { case spec.DataVersionCapella: err = b.validator.ValidateBuilderSubmissionV2(&blockvalidation.BuilderBlockValidationRequestV2{SubmitBlockRequest: *versionedBlockRequest.Capella, RegisteredGasLimit: opts.ValidatorData.GasLimit}) case spec.DataVersionDeneb: - err = b.validator.ValidateBuilderSubmissionV3(&blockvalidation.BuilderBlockValidationRequestV3{SubmitBlockRequest: *versionedBlockRequest.Deneb, RegisteredGasLimit: opts.ValidatorData.GasLimit, ParentBeaconBlockRoot: *opts.Block.BeaconRoot()}) + _, err = b.validator.ValidateBuilderSubmissionV3(&blockvalidation.BuilderBlockValidationRequestV3{SubmitBlockRequest: *versionedBlockRequest.Deneb, RegisteredGasLimit: opts.ValidatorData.GasLimit, ParentBeaconBlockRoot: *opts.Block.BeaconRoot()}) } if err != nil { log.Error("could not validate block", "version", dataVersion.String(), "err", err) diff --git a/core/blockchain.go b/core/blockchain.go index e1b1ea1bc..0a60f3125 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2454,31 +2454,31 @@ func (bc *BlockChain) SetBlockValidatorAndProcessorForTesting(v Validator, p Pro // - `useBalanceDiffProfit` if set to false, proposer payment is assumed to be in the last transaction of the block // otherwise we use proposer balance changes after the block to calculate proposer payment (see details in the code) // - `excludeWithdrawals` if set to true, withdrawals to the fee recipient are excluded from the balance change -func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Address, expectedProfit *big.Int, registeredGasLimit uint64, vmConfig vm.Config, useBalanceDiffProfit, excludeWithdrawals bool) error { +func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Address, expectedProfit *big.Int, registeredGasLimit uint64, vmConfig vm.Config, useBalanceDiffProfit, excludeWithdrawals bool) (*uint256.Int, error) { header := block.Header() if err := bc.engine.VerifyHeader(bc, header); err != nil { - return err + return nil, err } current := bc.CurrentBlock() reorg, err := bc.forker.ReorgNeeded(current, header) if err == nil && reorg { - return errors.New("block requires a reorg") + return nil, errors.New("block requires a reorg") } parent := bc.GetHeader(block.ParentHash(), block.NumberU64()-1) if parent == nil { - return errors.New("parent not found") + return nil, errors.New("parent not found") } calculatedGasLimit := CalcGasLimit(parent.GasLimit, registeredGasLimit) if calculatedGasLimit != header.GasLimit { - return errors.New("incorrect gas limit set") + return nil, errors.New("incorrect gas limit set") } statedb, err := bc.StateAt(parent.Root) if err != nil { - return err + return nil, err } // The chain importer is starting and stopping trie prefetchers. If a bad @@ -2488,13 +2488,15 @@ func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Ad defer statedb.StopPrefetcher() feeRecipientBalanceBefore := new(uint256.Int).Set(statedb.GetBalance(feeRecipient)) + builderBalanceBefore := new(big.Int).Set(statedb.GetBalance(header.Coinbase).ToBig()) receipts, _, usedGas, err := bc.processor.Process(block, statedb, vmConfig) if err != nil { - return err + return nil, err } feeRecipientBalanceAfter := new(uint256.Int).Set(statedb.GetBalance(feeRecipient)) + builderBalanceAfter := new(big.Int).Set(statedb.GetBalance(header.Coinbase).ToBig()) amtBeforeOrWithdrawn := new(uint256.Int).Set(feeRecipientBalanceBefore) if excludeWithdrawals { @@ -2508,88 +2510,109 @@ func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Ad if bc.Config().IsShanghai(header.Number, header.Time) { if header.WithdrawalsHash == nil { - return fmt.Errorf("withdrawals hash is missing") + return nil, fmt.Errorf("withdrawals hash is missing") } // withdrawals hash and withdrawals validated later in ValidateBody } else { if header.WithdrawalsHash != nil { - return fmt.Errorf("withdrawals hash present before shanghai") + return nil, fmt.Errorf("withdrawals hash present before shanghai") } if block.Withdrawals() != nil { - return fmt.Errorf("withdrawals list present in block body before shanghai") + return nil, fmt.Errorf("withdrawals list present in block body before shanghai") } } if err := bc.validator.ValidateBody(block); err != nil { - return err + return nil, err } if err := bc.validator.ValidateState(block, statedb, receipts, usedGas); err != nil { - return err + return nil, err } + // Coinbase balance diff + builderBalanceDelta := new(big.Int).Sub(builderBalanceAfter, builderBalanceBefore) + // Validate proposer payment if useBalanceDiffProfit && feeRecipientBalanceAfter.Cmp(amtBeforeOrWithdrawn) >= 0 { feeRecipientBalanceDelta := new(uint256.Int).Set(feeRecipientBalanceAfter) feeRecipientBalanceDelta = feeRecipientBalanceDelta.Sub(feeRecipientBalanceDelta, amtBeforeOrWithdrawn) - uint256ExpectedProfit, ok := uint256.FromBig(expectedProfit) - if !ok { + uint256ExpectedProfit, overflow := uint256.FromBig(expectedProfit) + if !overflow { if feeRecipientBalanceDelta.Cmp(uint256ExpectedProfit) >= 0 { if feeRecipientBalanceDelta.Cmp(uint256ExpectedProfit) > 0 { log.Warn("builder claimed profit is lower than calculated profit", "expected", expectedProfit, "actual", feeRecipientBalanceDelta) } - return nil + return bc.calculateTrueBlockValue(builderBalanceDelta, feeRecipientBalanceDelta.ToBig(), feeRecipient == header.Coinbase) } log.Warn("proposer payment not enough, trying last tx payment validation", "expected", expectedProfit, "actual", feeRecipientBalanceDelta) } } if len(receipts) == 0 { - return errors.New("no proposer payment receipt") + return nil, errors.New("no proposer payment receipt") } lastReceipt := receipts[len(receipts)-1] if lastReceipt.Status != types.ReceiptStatusSuccessful { - return errors.New("proposer payment not successful") + return nil, errors.New("proposer payment not successful") } txIndex := lastReceipt.TransactionIndex if txIndex+1 != uint(len(block.Transactions())) { - return fmt.Errorf("proposer payment index not last transaction in the block (%d of %d)", txIndex, len(block.Transactions())-1) + return nil, fmt.Errorf("proposer payment index not last transaction in the block (%d of %d)", txIndex, len(block.Transactions())-1) } paymentTx := block.Transaction(lastReceipt.TxHash) if paymentTx == nil { - return errors.New("payment tx not in the block") + return nil, errors.New("payment tx not in the block") } paymentTo := paymentTx.To() if paymentTo == nil || *paymentTo != feeRecipient { - return fmt.Errorf("payment tx not to the proposers fee recipient (%v)", paymentTo) + return nil, fmt.Errorf("payment tx not to the proposers fee recipient (%v)", paymentTo) } if paymentTx.Value().Cmp(expectedProfit) != 0 { - return fmt.Errorf("inaccurate payment %s, expected %s", paymentTx.Value().String(), expectedProfit.String()) + return nil, fmt.Errorf("inaccurate payment %s, expected %s", paymentTx.Value().String(), expectedProfit.String()) } if len(paymentTx.Data()) != 0 { - return fmt.Errorf("malformed proposer payment, contains calldata") + return nil, fmt.Errorf("malformed proposer payment, contains calldata") } if paymentTx.GasPrice().Cmp(block.BaseFee()) != 0 { - return fmt.Errorf("malformed proposer payment, gas price not equal to base fee") + return nil, fmt.Errorf("malformed proposer payment, gas price not equal to base fee") } if paymentTx.GasTipCap().Cmp(block.BaseFee()) != 0 && paymentTx.GasTipCap().Sign() != 0 { - return fmt.Errorf("malformed proposer payment, unexpected gas tip cap") + return nil, fmt.Errorf("malformed proposer payment, unexpected gas tip cap") } if paymentTx.GasFeeCap().Cmp(block.BaseFee()) != 0 { - return fmt.Errorf("malformed proposer payment, unexpected gas fee cap") + return nil, fmt.Errorf("malformed proposer payment, unexpected gas fee cap") } - return nil + return bc.calculateTrueBlockValue(builderBalanceDelta, paymentTx.Value(), feeRecipient == header.Coinbase) +} + +func (bc *BlockChain) calculateTrueBlockValue(builderBalanceDelta, proposerPaymentValue *big.Int, feeRecipientIsCoinbase bool) (*uint256.Int, error) { + if feeRecipientIsCoinbase { + uint256ProposerPaymentValue, overflow := uint256.FromBig(proposerPaymentValue) + if overflow { + log.Warn("proposer payment value overflow when converting to uint256", "value", proposerPaymentValue) + return nil, nil + } + return uint256ProposerPaymentValue, nil + } + trueBlockValue := new(big.Int).Add(builderBalanceDelta, proposerPaymentValue) + uint256TrueBlockValue, overflow := uint256.FromBig(trueBlockValue) + if overflow { + log.Warn("true block value overflow when converting to uint256", "value", trueBlockValue) + return nil, nil + } + return uint256TrueBlockValue, nil } // SetTrieFlushInterval configures how often in-memory tries are persisted to disk. diff --git a/eth/block-validation/api.go b/eth/block-validation/api.go index 30f02ac46..d58307c54 100644 --- a/eth/block-validation/api.go +++ b/eth/block-validation/api.go @@ -22,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rpc" + "github.com/holiman/uint256" ) type BlacklistedAddresses []common.Address @@ -153,7 +154,8 @@ func (api *BlockValidationAPI) ValidateBuilderSubmissionV1(params *BuilderBlockV return err } - return api.validateBlock(block, params.Message, params.RegisteredGasLimit) + _, err = api.validateBlock(block, params.Message, params.RegisteredGasLimit) + return err } type BuilderBlockValidationRequestV2 struct { @@ -192,7 +194,8 @@ func (api *BlockValidationAPI) ValidateBuilderSubmissionV2(params *BuilderBlockV return err } - return api.validateBlock(block, params.Message, params.RegisteredGasLimit) + _, err = api.validateBlock(block, params.Message, params.RegisteredGasLimit) + return err } type BuilderBlockValidationRequestV3 struct { @@ -222,44 +225,57 @@ func (r *BuilderBlockValidationRequestV3) UnmarshalJSON(data []byte) error { return nil } -func (api *BlockValidationAPI) ValidateBuilderSubmissionV3(params *BuilderBlockValidationRequestV3) error { +type BuilderBlockValidationResponse struct { + BlockValue *uint256.Int +} + +func (r *BuilderBlockValidationResponse) MarshalJSON() ([]byte, error) { + type validationResponseJSON struct { + BlockValue string `json:"block_value"` + } + return json.Marshal(&validationResponseJSON{ + BlockValue: fmt.Sprintf("%d", r.BlockValue), + }) +} + +func (api *BlockValidationAPI) ValidateBuilderSubmissionV3(params *BuilderBlockValidationRequestV3) (*BuilderBlockValidationResponse, error) { // TODO: fuzztest, make sure the validation is sound payload := params.ExecutionPayload blobsBundle := params.BlobsBundle - log.Info("blobs bundle", "blobs", len(blobsBundle.Blobs), "commits", len(blobsBundle.Commitments), "proofs", len(blobsBundle.Proofs)) + log.Info("received block and blobs bundle", "hash", payload.BlockHash.String(), "blobs", len(blobsBundle.Blobs), "commits", len(blobsBundle.Commitments), "proofs", len(blobsBundle.Proofs)) block, err := engine.ExecutionPayloadV3ToBlock(payload, blobsBundle, params.ParentBeaconBlockRoot) if err != nil { - return err + return nil, err } - err = api.validateBlock(block, params.Message, params.RegisteredGasLimit) + blockValue, err := api.validateBlock(block, params.Message, params.RegisteredGasLimit) if err != nil { log.Error("invalid payload", "hash", block.Hash, "number", block.NumberU64(), "parentHash", block.ParentHash, "err", err) - return err + return nil, err } err = validateBlobsBundle(block.Transactions(), blobsBundle) if err != nil { log.Error("invalid blobs bundle", "err", err) - return err + return nil, err } - return nil + return &BuilderBlockValidationResponse{blockValue}, nil } -func (api *BlockValidationAPI) validateBlock(block *types.Block, msg *builderApiV1.BidTrace, registeredGasLimit uint64) error { +func (api *BlockValidationAPI) validateBlock(block *types.Block, msg *builderApiV1.BidTrace, registeredGasLimit uint64) (*uint256.Int, error) { if msg.ParentHash != phase0.Hash32(block.ParentHash()) { - return fmt.Errorf("incorrect ParentHash %s, expected %s", msg.ParentHash.String(), block.ParentHash().String()) + return nil, fmt.Errorf("incorrect ParentHash %s, expected %s", msg.ParentHash.String(), block.ParentHash().String()) } if msg.BlockHash != phase0.Hash32(block.Hash()) { - return fmt.Errorf("incorrect BlockHash %s, expected %s", msg.BlockHash.String(), block.Hash().String()) + return nil, fmt.Errorf("incorrect BlockHash %s, expected %s", msg.BlockHash.String(), block.Hash().String()) } if msg.GasLimit != block.GasLimit() { - return fmt.Errorf("incorrect GasLimit %d, expected %d", msg.GasLimit, block.GasLimit()) + return nil, fmt.Errorf("incorrect GasLimit %d, expected %d", msg.GasLimit, block.GasLimit()) } if msg.GasUsed != block.GasUsed() { - return fmt.Errorf("incorrect GasUsed %d, expected %d", msg.GasUsed, block.GasUsed()) + return nil, fmt.Errorf("incorrect GasUsed %d, expected %d", msg.GasUsed, block.GasUsed()) } feeRecipient := common.BytesToAddress(msg.ProposerFeeRecipient[:]) @@ -269,13 +285,13 @@ func (api *BlockValidationAPI) validateBlock(block *types.Block, msg *builderApi 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(block.NumberU64()), isPostMerge, block.Time())) @@ -283,19 +299,19 @@ func (api *BlockValidationAPI) validateBlock(block *types.Block, msg *builderApi vmconfig = vm.Config{Tracer: tracer} } - err := api.eth.BlockChain().ValidatePayload(block, feeRecipient, expectedProfit, registeredGasLimit, vmconfig, api.useBalanceDiffProfit, api.excludeWithdrawals) + blockValue, err := api.eth.BlockChain().ValidatePayload(block, feeRecipient, expectedProfit, registeredGasLimit, vmconfig, api.useBalanceDiffProfit, api.excludeWithdrawals) if err != nil { - 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 blockValue, nil } func validateBlobsBundle(txs types.Transactions, blobsBundle *builderApiDeneb.BlobsBundle) error { diff --git a/eth/block-validation/api_test.go b/eth/block-validation/api_test.go index 4d8afc6ff..c6eb98794 100644 --- a/eth/block-validation/api_test.go +++ b/eth/block-validation/api_test.go @@ -390,15 +390,20 @@ func TestValidateBuilderSubmissionV3(t *testing.T) { ParentBeaconBlockRoot: common.Hash{42}, } - require.ErrorContains(t, api.ValidateBuilderSubmissionV3(blockRequest), "inaccurate payment") + response, err := api.ValidateBuilderSubmissionV3(blockRequest) + require.ErrorContains(t, err, "inaccurate payment") + require.Nil(t, response) blockRequest.Message.Value = uint256.NewInt(132912184722468) - require.NoError(t, api.ValidateBuilderSubmissionV3(blockRequest)) + response, err = api.ValidateBuilderSubmissionV3(blockRequest) + require.NoError(t, err) + require.Equal(t, response.BlockValue, uint256.NewInt(132912184722468)) blockRequest.Message.GasLimit += 1 blockRequest.ExecutionPayload.GasLimit += 1 updatePayloadHashV3(t, blockRequest) - require.ErrorContains(t, api.ValidateBuilderSubmissionV3(blockRequest), "incorrect gas limit set") + _, err = api.ValidateBuilderSubmissionV3(blockRequest) + require.ErrorContains(t, err, "incorrect gas limit set") blockRequest.Message.GasLimit -= 1 blockRequest.ExecutionPayload.GasLimit -= 1 @@ -411,7 +416,8 @@ func TestValidateBuilderSubmissionV3(t *testing.T) { testAddr: {}, }, } - require.ErrorContains(t, api.ValidateBuilderSubmissionV3(blockRequest), "transaction from blacklisted address 0x71562b71999873DB5b286dF957af199Ec94617F7") + _, err = api.ValidateBuilderSubmissionV3(blockRequest) + require.ErrorContains(t, err, "transaction from blacklisted address 0x71562b71999873DB5b286dF957af199Ec94617F7") // Test tx to blacklisted address api.accessVerifier = &AccessVerifier{ @@ -419,12 +425,14 @@ func TestValidateBuilderSubmissionV3(t *testing.T) { {0x16}: {}, }, } - require.ErrorContains(t, api.ValidateBuilderSubmissionV3(blockRequest), "transaction to blacklisted address 0x1600000000000000000000000000000000000000") + _, err = api.ValidateBuilderSubmissionV3(blockRequest) + require.ErrorContains(t, err, "transaction to blacklisted address 0x1600000000000000000000000000000000000000") api.accessVerifier = nil blockRequest.Message.GasUsed = 10 - require.ErrorContains(t, api.ValidateBuilderSubmissionV3(blockRequest), "incorrect GasUsed 10, expected 119996") + _, err = api.ValidateBuilderSubmissionV3(blockRequest) + require.ErrorContains(t, err, "incorrect GasUsed 10, expected 119996") blockRequest.Message.GasUsed = execData.GasUsed newTestKey, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f290") @@ -441,7 +449,8 @@ func TestValidateBuilderSubmissionV3(t *testing.T) { copy(invalidPayload.ReceiptsRoot[:], hexutil.MustDecode("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")[:32]) blockRequest.ExecutionPayload = invalidPayload updatePayloadHashV3(t, blockRequest) - require.ErrorContains(t, api.ValidateBuilderSubmissionV3(blockRequest), "could not apply tx 4", "insufficient funds for gas * price + value") + _, err = api.ValidateBuilderSubmissionV3(blockRequest) + require.ErrorContains(t, err, "could not apply tx 4", "insufficient funds for gas * price + value") } func updatePayloadHash(t *testing.T, blockRequest *BuilderBlockValidationRequest) {