diff --git a/README.md b/README.md index 17a179964f..d7686a17f8 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,10 @@ $ geth --help --builder.validation_blacklist value Path to file containing blacklisted addresses, json-encoded list of strings + + --builder.validation_force_last_tx_payment (default: false) + 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 c1fef3ae32..0c293c9247 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"` @@ -48,6 +49,7 @@ var DefaultConfig = Config{ RemoteRelayEndpoint: "", SecondaryRemoteRelayEndpoints: nil, ValidationBlocklist: "", + ValidationForceLastTxPayment: false, BuilderRateLimitDuration: RateLimitIntervalDefault.String(), BuilderRateLimitMaxBurst: RateLimitBurstDefault, EnableCancellations: false, diff --git a/builder/service.go b/builder/service.go index cb8685a559..ac6e5b13ea 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 cd4a004680..c8e45adf65 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -161,6 +161,7 @@ var ( utils.BuilderEnabled, 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 248c20ff61..f4b2c5d0ff 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -720,6 +720,12 @@ var ( Value: "", 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: false, + Category: flags.BuilderCategory, + } BuilderEnableLocalRelay = &cli.BoolFlag{ Name: "builder.local_relay", Usage: "Enable the local relay", @@ -1676,6 +1682,7 @@ func SetBuilderConfig(ctx *cli.Context, cfg *builder.Config) { cfg.RemoteRelayEndpoint = ctx.String(BuilderRemoteRelayEndpoint.Name) cfg.SecondaryRemoteRelayEndpoints = strings.Split(ctx.String(BuilderSecondaryRemoteRelayEndpoints.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..b4275ba2be 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -40,6 +40,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/utils" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers/logger" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/internal/syncx" @@ -2494,7 +2495,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 each transaction 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 +2532,20 @@ func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Ad // and dangling prefetcher, without defering each and holding on live refs. defer statedb.StopPrefetcher() + // Inject balance change tracer + // This will allow us to check balance changes of the fee recipient without modifying `Process` method + balanceTracer := logger.NewBalanceChangeTracer(feeRecipient, vmConfig.Tracer, statedb) + vmConfig.Tracer = balanceTracer + vmConfig.Debug = true + receipts, _, usedGas, err := bc.processor.Process(block, statedb, vmConfig) if err != nil { return err } + // Get fee recipient balance changes during each transaction execution + balanceChanges := balanceTracer.GetBalanceChanges() + if bc.Config().IsShanghai(header.Time) { if header.WithdrawalsHash == nil { return fmt.Errorf("withdrawals hash is missing") @@ -2554,6 +2568,44 @@ func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Ad return err } + // Validate proposer payment + + if !forceLastTxPayment { + // We calculate the proposer payment by counting balance increases of the fee recipient account after each transaction. + // If the balance decreases for the fee recipient for some transaction, we ignore it, + // but we still count profit from the tip of this transaction if the fee recipient is also a coinbase. + // If this method of profit calculation fails for some reason, we fall back to the old method of calculating proposer payment + // where we look at the last transaction in the block. + feeRecipientProfit := big.NewInt(0) + for i, balanceChange := range balanceChanges { + if balanceChange.Sign() > 0 { + feeRecipientProfit.Add(feeRecipientProfit, balanceChange) + } else { + // If the fee recipient balance decreases, it means that the fee recipient sent eth out of the account + // or paid for the gas of the transaction. + // In this case, we ignore the balance change, but we still count fee profit as a positive balance change if we can. + if block.Coinbase() == feeRecipient { + if i >= len(block.Transactions()) { + log.Error("transactions length is less than balance changes length") + break + } + tip, err := block.Transactions()[i].EffectiveGasTip(block.BaseFee()) + if err != nil { + log.Error("failed to calculate tip", "err", err) + break + } + profit := tip.Mul(tip, new(big.Int).SetUint64(receipts[i].GasUsed)) + feeRecipientProfit.Add(feeRecipientProfit, profit) + } + } + } + if feeRecipientProfit.Cmp(expectedProfit) >= 0 { + return nil + } + + log.Warn("proposer payment not enough, trying last tx payment validation", "expected", expectedProfit, "actual", feeRecipientProfit) + } + 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..e9d26e6b11 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}}, ExtraData: []byte("test genesis"), Timestamp: 9000, BaseFee: big.NewInt(params.InitialBaseFee), @@ -518,3 +522,260 @@ 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, this should not count as a profit + // but its tip of baseFee should count as a 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 the proposer fee recipient, this should not affect profit + tx6, _ := types.SignTx(types.NewTransaction(1, testAddr, big.NewInt(11), 21000, baseFee, nil), signer, testValidatorKey) + txs = append(txs, tx6) + + 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(testValidatorAddr) + tx, _ = types.SignTx(types.NewTransaction(nonce, testAddr, big.NewInt(10), 21000, baseFee, nil), signer, testValidatorKey) + 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") + }) + } +} diff --git a/eth/tracers/logger/balance_diff_tracer.go b/eth/tracers/logger/balance_diff_tracer.go new file mode 100644 index 0000000000..b64c8c912e --- /dev/null +++ b/eth/tracers/logger/balance_diff_tracer.go @@ -0,0 +1,87 @@ +package logger + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" +) + +// BalanceChangeTracer is a tracer that captures the balance changes of an address before and after each transaction +type BalanceChangeTracer struct { + address common.Address + outerLogger vm.EVMLogger + stateDB vm.StateDB + + balanceChanges []*big.Int + tempBalance *big.Int +} + +func NewBalanceChangeTracer(address common.Address, outerLogger vm.EVMLogger, stateDB vm.StateDB) *BalanceChangeTracer { + return &BalanceChangeTracer{ + address: address, + outerLogger: outerLogger, + stateDB: stateDB, + + balanceChanges: nil, + tempBalance: new(big.Int), + } +} + +// GetBalanceChanges returns the balance changes of the address during the execution of the transaction +// It should be called after all transactions were executed with this tracer +func (b *BalanceChangeTracer) GetBalanceChanges() []*big.Int { + return b.balanceChanges +} + +func (b *BalanceChangeTracer) CaptureTxStart(gasLimit uint64) { + b.tempBalance.Set(b.stateDB.GetBalance(b.address)) + if b.outerLogger != nil { + b.outerLogger.CaptureTxStart(gasLimit) + } +} + +func (b *BalanceChangeTracer) CaptureTxEnd(restGas uint64) { + balanceChange := new(big.Int).Sub(b.stateDB.GetBalance(b.address), b.tempBalance) + b.balanceChanges = append(b.balanceChanges, balanceChange) + + if b.outerLogger != nil { + b.outerLogger.CaptureTxEnd(restGas) + } +} + +func (b *BalanceChangeTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { + if b.outerLogger != nil { + b.outerLogger.CaptureStart(env, from, to, create, input, gas, value) + } +} + +func (b *BalanceChangeTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { + if b.outerLogger != nil { + b.outerLogger.CaptureEnd(output, gasUsed, err) + } +} + +func (b *BalanceChangeTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + if b.outerLogger != nil { + b.outerLogger.CaptureEnter(typ, from, to, input, gas, value) + } +} + +func (b *BalanceChangeTracer) CaptureExit(output []byte, gasUsed uint64, err error) { + if b.outerLogger != nil { + b.outerLogger.CaptureExit(output, gasUsed, err) + } +} + +func (b *BalanceChangeTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + if b.outerLogger != nil { + b.outerLogger.CaptureState(pc, op, gas, cost, scope, rData, depth, err) + } +} + +func (b *BalanceChangeTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { + if b.outerLogger != nil { + b.outerLogger.CaptureFault(pc, op, gas, cost, scope, depth, err) + } +} diff --git a/eth/tracers/logger/balance_diff_tracer_test.go b/eth/tracers/logger/balance_diff_tracer_test.go new file mode 100644 index 0000000000..c2fc327c99 --- /dev/null +++ b/eth/tracers/logger/balance_diff_tracer_test.go @@ -0,0 +1,59 @@ +package logger + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/stretchr/testify/require" +) + +type dummyStateDB struct { + state.StateDB + address common.Address + balance *big.Int +} + +func (db *dummyStateDB) GetBalance(address common.Address) *big.Int { + if address == db.address { + return db.balance + } else { + return big.NewInt(0) + } +} + +func Test_balanceChangeTracer(t *testing.T) { + address := common.HexToAddress("0x123") + stateDB := &dummyStateDB{address: address} + + tracer := NewBalanceChangeTracer(address, nil, stateDB) + + // before the block balance is 100 + stateDB.balance = big.NewInt(100) + + // 1-st tx, gain of 7 + tracer.CaptureTxStart(0) + stateDB.balance = big.NewInt(107) + tracer.CaptureTxEnd(0) + + // 2-end tx, gain of 17 + tracer.CaptureTxStart(0) + stateDB.balance = big.NewInt(124) + tracer.CaptureTxEnd(0) + + // 3-rd tx, loss of 5 + tracer.CaptureTxStart(0) + stateDB.balance = big.NewInt(119) + tracer.CaptureTxEnd(0) + + // 4-rd tx, gain of 3 + tracer.CaptureTxStart(0) + stateDB.balance = big.NewInt(122) + tracer.CaptureTxEnd(0) + + result := tracer.GetBalanceChanges() + expectedResult := []*big.Int{big.NewInt(7), big.NewInt(17), big.NewInt(-5), big.NewInt(3)} + + require.Equal(t, expectedResult, result) +}