diff --git a/README.md b/README.md index feea16af17..6e23b0b694 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,13 @@ $ geth --help blocks. For example, if a slot is 12 seconds long, and the offset is 2 seconds, the builder will submit blocks at 10 seconds into the slot. [$FLASHBOTS_BUILDER_SUBMISSION_OFFSET] + + --builder.validation_blacklist value + Path to file containing blacklisted addresses, json-encoded list of strings + + --builder.validation_force_last_tx_payment (default: true) + Block validation API will enforce that the last tx in the block is payment to + the proposer. --builder.validator_checks (default: false) Enable the validator checks diff --git a/builder/config.go b/builder/config.go index efadb78074..c1eb4fe783 100644 --- a/builder/config.go +++ b/builder/config.go @@ -21,6 +21,7 @@ type Config struct { RemoteRelayEndpoint string `toml:",omitempty"` SecondaryRemoteRelayEndpoints []string `toml:",omitempty"` ValidationBlocklist string `toml:",omitempty"` + ValidationForceLastTxPayment bool `toml:",omitempty"` BuilderRateLimitDuration string `toml:",omitempty"` BuilderRateLimitMaxBurst int `toml:",omitempty"` BuilderRateLimitResubmitInterval string `toml:",omitempty"` @@ -49,6 +50,7 @@ var DefaultConfig = Config{ RemoteRelayEndpoint: "", SecondaryRemoteRelayEndpoints: nil, ValidationBlocklist: "", + ValidationForceLastTxPayment: true, BuilderRateLimitDuration: RateLimitIntervalDefault.String(), BuilderRateLimitMaxBurst: RateLimitBurstDefault, DiscardRevertibleTxOnErr: false, diff --git a/builder/service.go b/builder/service.go index 55f4300c67..411fa462d7 100644 --- a/builder/service.go +++ b/builder/service.go @@ -214,7 +214,7 @@ func Register(stack *node.Node, backend *eth.Ethereum, cfg *Config) error { return fmt.Errorf("failed to load validation blocklist %w", err) } } - validator = blockvalidation.NewBlockValidationAPI(backend, accessVerifier) + validator = blockvalidation.NewBlockValidationAPI(backend, accessVerifier, cfg.ValidationForceLastTxPayment) } // Set up builder rate limiter based on environment variables or CLI flags. diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 2fd2e65973..4267a8bd44 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -177,6 +177,9 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) { if ctx.IsSet(utils.BuilderBlockValidationBlacklistSourceFilePath.Name) { bvConfig.BlacklistSourceFilePath = ctx.String(utils.BuilderBlockValidationBlacklistSourceFilePath.Name) } + if ctx.IsSet(utils.BuilderBlockValidationForceLastTxPayment.Name) { + bvConfig.ForceLastTxPayment = ctx.Bool(utils.BuilderBlockValidationForceLastTxPayment.Name) + } if err := blockvalidationapi.Register(stack, eth, bvConfig); err != nil { utils.Fatalf("Failed to register the Block Validation API: %v", err) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 7795f0b7d9..2e9938658a 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -162,6 +162,7 @@ var ( utils.BuilderPriceCutoffPercentFlag, utils.BuilderEnableValidatorChecks, utils.BuilderBlockValidationBlacklistSourceFilePath, + utils.BuilderBlockValidationForceLastTxPayment, utils.BuilderEnableLocalRelay, utils.BuilderSecondsInSlot, utils.BuilderSlotsInEpoch, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index d70ed9b12b..865b84375b 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -735,6 +735,12 @@ var ( Aliases: []string{"builder.validation_blacklist"}, Category: flags.BuilderCategory, } + BuilderBlockValidationForceLastTxPayment = &cli.BoolFlag{ + Name: "builder.validation_force_last_tx_payment", + Usage: "Block validation API will enforce that the last tx in the block is payment to the proposer.", + Value: true, + Category: flags.BuilderCategory, + } BuilderEnableLocalRelay = &cli.BoolFlag{ Name: "builder.local_relay", Usage: "Enable the local relay", @@ -1711,6 +1717,7 @@ func SetBuilderConfig(ctx *cli.Context, cfg *builder.Config) { if ctx.IsSet(BuilderBlockValidationBlacklistSourceFilePath.Name) { cfg.ValidationBlocklist = ctx.String(BuilderBlockValidationBlacklistSourceFilePath.Name) } + cfg.ValidationForceLastTxPayment = ctx.Bool(BuilderBlockValidationForceLastTxPayment.Name) cfg.BuilderRateLimitDuration = ctx.String(BuilderRateLimitDuration.Name) cfg.BuilderRateLimitMaxBurst = ctx.Int(BuilderRateLimitMaxBurst.Name) cfg.BuilderSubmissionOffset = ctx.Duration(BuilderSubmissionOffset.Name) diff --git a/core/blockchain.go b/core/blockchain.go index cdf87c2e56..d4271dd619 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2494,7 +2494,11 @@ func (bc *BlockChain) SetBlockValidatorAndProcessorForTesting(v Validator, p Pro bc.processor = p } -func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Address, expectedProfit *big.Int, registeredGasLimit uint64, vmConfig vm.Config) error { +// ValidatePayload validates the payload of the block. +// It returns nil if the payload is valid, otherwise it returns an error. +// - `forceLastTxPayment` if set to true, 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) +func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Address, expectedProfit *big.Int, registeredGasLimit uint64, vmConfig vm.Config, forceLastTxPayment bool) error { header := block.Header() if err := bc.engine.VerifyHeader(bc, header, true); err != nil { return err @@ -2527,11 +2531,16 @@ func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Ad // and dangling prefetcher, without defering each and holding on live refs. defer statedb.StopPrefetcher() + feeRecipientBalanceBefore := new(big.Int).Set(statedb.GetBalance(feeRecipient)) + receipts, _, usedGas, err := bc.processor.Process(block, statedb, vmConfig) if err != nil { return err } + feeRecipientBalanceDelta := new(big.Int).Set(statedb.GetBalance(feeRecipient)) + feeRecipientBalanceDelta.Sub(feeRecipientBalanceDelta, feeRecipientBalanceBefore) + if bc.Config().IsShanghai(header.Time) { if header.WithdrawalsHash == nil { return fmt.Errorf("withdrawals hash is missing") @@ -2554,6 +2563,18 @@ func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Ad return err } + // Validate proposer payment + + if !forceLastTxPayment { + if feeRecipientBalanceDelta.Cmp(expectedProfit) >= 0 { + if feeRecipientBalanceDelta.Cmp(expectedProfit) > 0 { + log.Warn("builder claimed profit is lower than calculated profit", "expected", expectedProfit, "actual", feeRecipientBalanceDelta) + } + return nil + } + 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") } diff --git a/eth/block-validation/api.go b/eth/block-validation/api.go index d2727ce945..a51b7504b5 100644 --- a/eth/block-validation/api.go +++ b/eth/block-validation/api.go @@ -88,6 +88,8 @@ func NewAccessVerifierFromFile(path string) (*AccessVerifier, error) { type BlockValidationConfig struct { BlacklistSourceFilePath string + // If set to true, proposer payment is assumed to be in the last transaction of the block. + ForceLastTxPayment bool } // Register adds catalyst APIs to the full node. @@ -104,7 +106,7 @@ func Register(stack *node.Node, backend *eth.Ethereum, cfg BlockValidationConfig stack.RegisterAPIs([]rpc.API{ { Namespace: "flashbots", - Service: NewBlockValidationAPI(backend, accessVerifier), + Service: NewBlockValidationAPI(backend, accessVerifier, cfg.ForceLastTxPayment), }, }) return nil @@ -113,14 +115,17 @@ func Register(stack *node.Node, backend *eth.Ethereum, cfg BlockValidationConfig type BlockValidationAPI struct { eth *eth.Ethereum accessVerifier *AccessVerifier + // If set to true, proposer payment is assumed to be in the last transaction of the block. + forceLastTxPayment bool } // NewConsensusAPI creates a new consensus api for the given backend. // The underlying blockchain needs to have a valid terminal total difficulty set. -func NewBlockValidationAPI(eth *eth.Ethereum, accessVerifier *AccessVerifier) *BlockValidationAPI { +func NewBlockValidationAPI(eth *eth.Ethereum, accessVerifier *AccessVerifier, forceLastTxPayment bool) *BlockValidationAPI { return &BlockValidationAPI{ - eth: eth, - accessVerifier: accessVerifier, + eth: eth, + accessVerifier: accessVerifier, + forceLastTxPayment: forceLastTxPayment, } } @@ -180,7 +185,7 @@ func (api *BlockValidationAPI) ValidateBuilderSubmissionV1(params *BuilderBlockV vmconfig = vm.Config{Tracer: tracer, Debug: true} } - err = api.eth.BlockChain().ValidatePayload(block, feeRecipient, expectedProfit, params.RegisteredGasLimit, vmconfig) + err = api.eth.BlockChain().ValidatePayload(block, feeRecipient, expectedProfit, params.RegisteredGasLimit, vmconfig, api.forceLastTxPayment) if err != nil { log.Error("invalid payload", "hash", payload.BlockHash.String(), "number", payload.BlockNumber, "parentHash", payload.ParentHash.String(), "err", err) return err @@ -272,7 +277,7 @@ func (api *BlockValidationAPI) ValidateBuilderSubmissionV2(params *BuilderBlockV vmconfig = vm.Config{Tracer: tracer, Debug: true} } - err = api.eth.BlockChain().ValidatePayload(block, feeRecipient, expectedProfit, params.RegisteredGasLimit, vmconfig) + err = api.eth.BlockChain().ValidatePayload(block, feeRecipient, expectedProfit, params.RegisteredGasLimit, vmconfig, api.forceLastTxPayment) if err != nil { log.Error("invalid payload", "hash", payload.BlockHash.String(), "number", payload.BlockNumber, "parentHash", payload.ParentHash.String(), "err", err) return err diff --git a/eth/block-validation/api_test.go b/eth/block-validation/api_test.go index 412fe3d29e..f7cd3d0b90 100644 --- a/eth/block-validation/api_test.go +++ b/eth/block-validation/api_test.go @@ -3,6 +3,7 @@ package blockvalidation import ( "encoding/json" "errors" + "fmt" "math/big" "os" "testing" @@ -22,6 +23,7 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth/downloader" @@ -50,7 +52,14 @@ var ( testValidatorKey, _ = crypto.HexToECDSA("28c3cd61b687fdd03488e167a5d84f50269df2a4c29a2cfb1390903aa775c5d0") testValidatorAddr = crypto.PubkeyToAddress(testValidatorKey.PublicKey) + testBuilderKeyHex = "0bfbbbc68fefd990e61ba645efb84e0a62e94d5fff02c9b1da8eb45fea32b4e0" + testBuilderKey, _ = crypto.HexToECDSA(testBuilderKeyHex) + testBuilderAddr = crypto.PubkeyToAddress(testBuilderKey.PublicKey) + testBalance = big.NewInt(2e18) + + // This EVM code generates a log when the contract is created. + logCode = common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00") ) func TestValidateBuilderSubmissionV1(t *testing.T) { @@ -60,14 +69,11 @@ func TestValidateBuilderSubmissionV1(t *testing.T) { ethservice.Merger().ReachTTD() defer n.Close() - api := NewBlockValidationAPI(ethservice, nil) + api := NewBlockValidationAPI(ethservice, nil, false) parent := preMergeBlocks[len(preMergeBlocks)-1] api.eth.APIBackend.Miner().SetEtherbase(testValidatorAddr) - // This EVM code generates a log when the contract is created. - logCode := common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00") - statedb, _ := ethservice.BlockChain().StateAt(parent.Root()) nonce := statedb.GetNonce(testAddr) @@ -166,20 +172,17 @@ func TestValidateBuilderSubmissionV1(t *testing.T) { func TestValidateBuilderSubmissionV2(t *testing.T) { genesis, preMergeBlocks := generatePreMergeChain(20) - os.Setenv("BUILDER_TX_SIGNING_KEY", "0x28c3cd61b687fdd03488e167a5d84f50269df2a4c29a2cfb1390903aa775c5d0") + os.Setenv("BUILDER_TX_SIGNING_KEY", testBuilderKeyHex) time := preMergeBlocks[len(preMergeBlocks)-1].Time() + 5 genesis.Config.ShanghaiTime = &time n, ethservice := startEthService(t, genesis, preMergeBlocks) ethservice.Merger().ReachTTD() defer n.Close() - api := NewBlockValidationAPI(ethservice, nil) + api := NewBlockValidationAPI(ethservice, nil, false) parent := preMergeBlocks[len(preMergeBlocks)-1] - api.eth.APIBackend.Miner().SetEtherbase(testValidatorAddr) - - // This EVM code generates a log when the contract is created. - logCode := common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00") + api.eth.APIBackend.Miner().SetEtherbase(testBuilderAddr) statedb, _ := ethservice.BlockChain().StateAt(parent.Root()) nonce := statedb.GetNonce(testAddr) @@ -234,7 +237,8 @@ func TestValidateBuilderSubmissionV2(t *testing.T) { ProposerFeeRecipient: proposerAddr, GasLimit: execData.GasLimit, GasUsed: execData.GasUsed, - Value: uint256.NewInt(0), + // This value is actual profit + 1, validation should fail + Value: uint256.NewInt(149842511727213), }, ExecutionPayload: payload, }, @@ -313,7 +317,7 @@ func generatePreMergeChain(n int) (*core.Genesis, []*types.Block) { config := params.AllEthashProtocolChanges genesis := &core.Genesis{ Config: config, - Alloc: core.GenesisAlloc{testAddr: {Balance: testBalance}}, + Alloc: core.GenesisAlloc{testAddr: {Balance: testBalance}, testValidatorAddr: {Balance: testBalance}, testBuilderAddr: {Balance: testBalance}}, ExtraData: []byte("test genesis"), Timestamp: 9000, BaseFee: big.NewInt(params.InitialBaseFee), @@ -518,3 +522,263 @@ func WithdrawalToBlockRequestWithdrawal(withdrawals types.Withdrawals) []*capell } return withdrawalsData } + +type buildBlockArgs struct { + parentHash common.Hash + parentRoot common.Hash + feeRecipient common.Address + txs types.Transactions + random common.Hash + number uint64 + gasLimit uint64 + timestamp uint64 + extraData []byte + baseFeePerGas *big.Int + withdrawals types.Withdrawals +} + +func buildBlock(args buildBlockArgs, chain *core.BlockChain) (*engine.ExecutableData, error) { + header := &types.Header{ + ParentHash: args.parentHash, + Coinbase: args.feeRecipient, + Number: big.NewInt(int64(args.number)), + GasLimit: args.gasLimit, + Time: args.timestamp, + Extra: args.extraData, + BaseFee: args.baseFeePerGas, + MixDigest: args.random, + } + + err := chain.Engine().Prepare(chain, header) + if err != nil { + return nil, err + } + + statedb, err := chain.StateAt(args.parentRoot) + if err != nil { + return nil, err + } + + receipts := make([]*types.Receipt, 0, len(args.txs)) + gasPool := core.GasPool(header.GasLimit) + vmConfig := vm.Config{} + for i, tx := range args.txs { + statedb.SetTxContext(tx.Hash(), i) + receipt, err := core.ApplyTransaction(chain.Config(), chain, &args.feeRecipient, &gasPool, statedb, header, tx, &header.GasUsed, vmConfig, nil) + if err != nil { + return nil, err + } + receipts = append(receipts, receipt) + } + + block, err := chain.Engine().FinalizeAndAssemble(chain, header, statedb, args.txs, nil, receipts, args.withdrawals) + if err != nil { + return nil, err + } + + execData := engine.BlockToExecutableData(block, common.Big0) + + return execData.ExecutionPayload, nil +} + +func executableDataToBlockValidationRequest(execData *engine.ExecutableData, proposer common.Address, value *big.Int, withdrawalsRoot common.Hash) (*BuilderBlockValidationRequestV2, error) { + payload, err := ExecutableDataToExecutionPayloadV2(execData) + if err != nil { + return nil, err + } + + proposerAddr := bellatrix.ExecutionAddress{} + copy(proposerAddr[:], proposer.Bytes()) + + value256, overflow := uint256.FromBig(value) + if overflow { + return nil, errors.New("could not convert value to uint256") + } + blockRequest := &BuilderBlockValidationRequestV2{ + SubmitBlockRequest: capellaapi.SubmitBlockRequest{ + Signature: phase0.BLSSignature{}, + Message: &apiv1.BidTrace{ + ParentHash: phase0.Hash32(execData.ParentHash), + BlockHash: phase0.Hash32(execData.BlockHash), + ProposerFeeRecipient: proposerAddr, + GasLimit: execData.GasLimit, + GasUsed: execData.GasUsed, + Value: value256, + }, + ExecutionPayload: payload, + }, + RegisteredGasLimit: execData.GasLimit, + WithdrawalsRoot: withdrawalsRoot, + } + return blockRequest, nil +} + +// This tests payment when the proposer fee recipient is the same as the coinbase +func TestValidateBuilderSubmissionV2_CoinbasePaymentDefault(t *testing.T) { + genesis, preMergeBlocks := generatePreMergeChain(20) + lastBlock := preMergeBlocks[len(preMergeBlocks)-1] + time := lastBlock.Time() + 5 + genesis.Config.ShanghaiTime = &time + n, ethservice := startEthService(t, genesis, preMergeBlocks) + ethservice.Merger().ReachTTD() + defer n.Close() + + api := NewBlockValidationAPI(ethservice, nil, false) + + baseFee := misc.CalcBaseFee(ethservice.BlockChain().Config(), lastBlock.Header()) + txs := make(types.Transactions, 0) + + statedb, _ := ethservice.BlockChain().StateAt(lastBlock.Root()) + nonce := statedb.GetNonce(testAddr) + signer := types.LatestSigner(ethservice.BlockChain().Config()) + + expectedProfit := uint64(0) + + tx1, _ := types.SignTx(types.NewTransaction(nonce, common.Address{0x16}, big.NewInt(10), 21000, big.NewInt(2*baseFee.Int64()), nil), signer, testKey) + txs = append(txs, tx1) + expectedProfit += 21000 * baseFee.Uint64() + + // this tx will use 56996 gas + tx2, _ := types.SignTx(types.NewContractCreation(nonce+1, new(big.Int), 1000000, big.NewInt(2*baseFee.Int64()), logCode), signer, testKey) + txs = append(txs, tx2) + expectedProfit += 56996 * baseFee.Uint64() + + tx3, _ := types.SignTx(types.NewTransaction(nonce+2, testAddr, big.NewInt(10), 21000, baseFee, nil), signer, testKey) + txs = append(txs, tx3) + + // this transaction sends 7 wei to the proposer fee recipient, this should count as a profit + tx4, _ := types.SignTx(types.NewTransaction(nonce+3, testValidatorAddr, big.NewInt(7), 21000, baseFee, nil), signer, testKey) + txs = append(txs, tx4) + expectedProfit += 7 + + // transactions from the proposer fee recipient + + // this transaction sends 3 wei from the proposer fee recipient to the proposer fee recipient and pays tip of baseFee + // this should not count as a profit (because balance does not increase) + // Base fee is burned from the balance so it should decrease decreasing the profit. + tx5, _ := types.SignTx(types.NewTransaction(0, testValidatorAddr, big.NewInt(3), 21000, big.NewInt(2*baseFee.Int64()), nil), signer, testValidatorKey) + txs = append(txs, tx5) + expectedProfit -= 21000 * baseFee.Uint64() + + // this tx sends 11 wei from the proposer fee recipient to some other address and burns 21000*baseFee + // this should count as negative profit + tx6, _ := types.SignTx(types.NewTransaction(1, testAddr, big.NewInt(11), 21000, baseFee, nil), signer, testValidatorKey) + txs = append(txs, tx6) + expectedProfit -= 11 + 21000*baseFee.Uint64() + + withdrawals := []*types.Withdrawal{ + { + Index: 0, + Validator: 1, + Amount: 100, + Address: testAddr, + }, + { + Index: 1, + Validator: 1, + Amount: 100, + Address: testAddr, + }, + } + withdrawalsRoot := types.DeriveSha(types.Withdrawals(withdrawals), trie.NewStackTrie(nil)) + + buildBlockArgs := buildBlockArgs{ + parentHash: lastBlock.Hash(), + parentRoot: lastBlock.Root(), + feeRecipient: testValidatorAddr, + txs: txs, + random: common.Hash{}, + number: lastBlock.NumberU64() + 1, + gasLimit: lastBlock.GasLimit(), + timestamp: lastBlock.Time() + 5, + extraData: nil, + baseFeePerGas: baseFee, + withdrawals: withdrawals, + } + + execData, err := buildBlock(buildBlockArgs, ethservice.BlockChain()) + require.NoError(t, err) + + value := big.NewInt(int64(expectedProfit)) + + req, err := executableDataToBlockValidationRequest(execData, testValidatorAddr, value, withdrawalsRoot) + require.NoError(t, err) + require.NoError(t, api.ValidateBuilderSubmissionV2(req)) + + // 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)) + + // 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") +} + +func TestValidateBuilderSubmissionV2_Blocklist(t *testing.T) { + genesis, preMergeBlocks := generatePreMergeChain(20) + lastBlock := preMergeBlocks[len(preMergeBlocks)-1] + time := lastBlock.Time() + 5 + genesis.Config.ShanghaiTime = &time + n, ethservice := startEthService(t, genesis, preMergeBlocks) + ethservice.Merger().ReachTTD() + defer n.Close() + + accessVerifier := &AccessVerifier{ + blacklistedAddresses: map[common.Address]struct{}{ + testAddr: {}, + }, + } + + apiWithBlock := NewBlockValidationAPI(ethservice, accessVerifier, false) + apiNoBlock := NewBlockValidationAPI(ethservice, nil, false) + + baseFee := misc.CalcBaseFee(ethservice.BlockChain().Config(), lastBlock.Header()) + blockedTxs := make(types.Transactions, 0) + + statedb, _ := ethservice.BlockChain().StateAt(lastBlock.Root()) + + signer := types.LatestSigner(ethservice.BlockChain().Config()) + + nonce := statedb.GetNonce(testAddr) + tx, _ := types.SignTx(types.NewTransaction(nonce, common.Address{0x16}, big.NewInt(10), 21000, baseFee, nil), signer, testKey) + blockedTxs = append(blockedTxs, tx) + + nonce = statedb.GetNonce(testBuilderAddr) + tx, _ = types.SignTx(types.NewTransaction(nonce, testAddr, big.NewInt(10), 21000, baseFee, nil), signer, testBuilderKey) + blockedTxs = append(blockedTxs, tx) + + withdrawalsRoot := types.DeriveSha(types.Withdrawals(nil), trie.NewStackTrie(nil)) + + for i, tx := range blockedTxs { + t.Run(fmt.Sprintf("tx %d", i), func(t *testing.T) { + buildBlockArgs := buildBlockArgs{ + parentHash: lastBlock.Hash(), + parentRoot: lastBlock.Root(), + feeRecipient: testValidatorAddr, + txs: types.Transactions{tx}, + random: common.Hash{}, + number: lastBlock.NumberU64() + 1, + gasLimit: lastBlock.GasLimit(), + timestamp: lastBlock.Time() + 5, + extraData: nil, + baseFeePerGas: baseFee, + withdrawals: nil, + } + + execData, err := buildBlock(buildBlockArgs, ethservice.BlockChain()) + require.NoError(t, err) + + 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") + }) + } +}