Skip to content

Commit 4ff4c10

Browse files
authored
exclude CL withdrawals from profit calculation (ethereum#144)
1 parent 18be100 commit 4ff4c10

File tree

8 files changed

+136
-11
lines changed

8 files changed

+136
-11
lines changed

builder/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Config struct {
2222
SecondaryRemoteRelayEndpoints []string `toml:",omitempty"`
2323
ValidationBlocklist string `toml:",omitempty"`
2424
ValidationUseCoinbaseDiff bool `toml:",omitempty"`
25+
ValidationExcludeWithdrawals bool `toml:",omitempty"`
2526
BuilderRateLimitDuration string `toml:",omitempty"`
2627
BuilderRateLimitMaxBurst int `toml:",omitempty"`
2728
BuilderRateLimitResubmitInterval string `toml:",omitempty"`
@@ -52,6 +53,7 @@ var DefaultConfig = Config{
5253
SecondaryRemoteRelayEndpoints: nil,
5354
ValidationBlocklist: "",
5455
ValidationUseCoinbaseDiff: false,
56+
ValidationExcludeWithdrawals: false,
5557
BuilderRateLimitDuration: RateLimitIntervalDefault.String(),
5658
BuilderRateLimitMaxBurst: RateLimitBurstDefault,
5759
DiscardRevertibleTxOnErr: false,

builder/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ func Register(stack *node.Node, backend *eth.Ethereum, cfg *Config) error {
214214
return fmt.Errorf("failed to load validation blocklist %w", err)
215215
}
216216
}
217-
validator = blockvalidation.NewBlockValidationAPI(backend, accessVerifier, cfg.ValidationUseCoinbaseDiff)
217+
validator = blockvalidation.NewBlockValidationAPI(backend, accessVerifier, cfg.ValidationUseCoinbaseDiff, cfg.ValidationExcludeWithdrawals)
218218
}
219219

220220
// Set up builder rate limiter based on environment variables or CLI flags.

cmd/geth/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) {
180180
if ctx.IsSet(utils.BuilderBlockValidationUseBalanceDiff.Name) {
181181
bvConfig.UseBalanceDiffProfit = ctx.Bool(utils.BuilderBlockValidationUseBalanceDiff.Name)
182182
}
183+
if ctx.IsSet(utils.BuilderBlockValidationExcludeWithdrawals.Name) {
184+
bvConfig.ExcludeWithdrawals = ctx.Bool(utils.BuilderBlockValidationExcludeWithdrawals.Name)
185+
}
183186

184187
if err := blockvalidationapi.Register(stack, eth, bvConfig); err != nil {
185188
utils.Fatalf("Failed to register the Block Validation API: %v", err)

cmd/geth/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ var (
163163
utils.BuilderEnableValidatorChecks,
164164
utils.BuilderBlockValidationBlacklistSourceFilePath,
165165
utils.BuilderBlockValidationUseBalanceDiff,
166+
utils.BuilderBlockValidationExcludeWithdrawals,
166167
utils.BuilderEnableLocalRelay,
167168
utils.BuilderSecondsInSlot,
168169
utils.BuilderSlotsInEpoch,

cmd/utils/flags.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,12 @@ var (
741741
Value: false,
742742
Category: flags.BuilderCategory,
743743
}
744+
BuilderBlockValidationExcludeWithdrawals = &cli.BoolFlag{
745+
Name: "builder.validation_exclude_withdrawals",
746+
Usage: "Block validation API will exclude CL withdrawals to the fee recipient from the balance delta.",
747+
Value: false,
748+
Category: flags.BuilderCategory,
749+
}
744750
BuilderEnableLocalRelay = &cli.BoolFlag{
745751
Name: "builder.local_relay",
746752
Usage: "Enable the local relay",
@@ -1724,6 +1730,7 @@ func SetBuilderConfig(ctx *cli.Context, cfg *builder.Config) {
17241730
cfg.ValidationBlocklist = ctx.String(BuilderBlockValidationBlacklistSourceFilePath.Name)
17251731
}
17261732
cfg.ValidationUseCoinbaseDiff = ctx.Bool(BuilderBlockValidationUseBalanceDiff.Name)
1733+
cfg.ValidationExcludeWithdrawals = ctx.Bool(BuilderBlockValidationExcludeWithdrawals.Name)
17271734
cfg.BuilderRateLimitDuration = ctx.String(BuilderRateLimitDuration.Name)
17281735
cfg.BuilderRateLimitMaxBurst = ctx.Int(BuilderRateLimitMaxBurst.Name)
17291736
cfg.BuilderSubmissionOffset = ctx.Duration(BuilderSubmissionOffset.Name)

core/blockchain.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2498,7 +2498,8 @@ func (bc *BlockChain) SetBlockValidatorAndProcessorForTesting(v Validator, p Pro
24982498
// It returns nil if the payload is valid, otherwise it returns an error.
24992499
// - `useBalanceDiffProfit` if set to false, proposer payment is assumed to be in the last transaction of the block
25002500
// otherwise we use proposer balance changes after the block to calculate proposer payment (see details in the code)
2501-
func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Address, expectedProfit *big.Int, registeredGasLimit uint64, vmConfig vm.Config, useBalanceDiffProfit bool) error {
2501+
// - `excludeWithdrawals` if set to true, withdrawals to the fee recipient are excluded from the balance change
2502+
func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Address, expectedProfit *big.Int, registeredGasLimit uint64, vmConfig vm.Config, useBalanceDiffProfit, excludeWithdrawals bool) error {
25022503
header := block.Header()
25032504
if err := bc.engine.VerifyHeader(bc, header, true); err != nil {
25042505
return err
@@ -2540,6 +2541,14 @@ func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Ad
25402541

25412542
feeRecipientBalanceDelta := new(big.Int).Set(statedb.GetBalance(feeRecipient))
25422543
feeRecipientBalanceDelta.Sub(feeRecipientBalanceDelta, feeRecipientBalanceBefore)
2544+
if excludeWithdrawals {
2545+
for _, w := range block.Withdrawals() {
2546+
if w.Address == feeRecipient {
2547+
amount := new(big.Int).Mul(new(big.Int).SetUint64(w.Amount), big.NewInt(params.GWei))
2548+
feeRecipientBalanceDelta.Sub(feeRecipientBalanceDelta, amount)
2549+
}
2550+
}
2551+
}
25432552

25442553
if bc.Config().IsShanghai(header.Time) {
25452554
if header.WithdrawalsHash == nil {

eth/block-validation/api.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ type BlockValidationConfig struct {
9090
BlacklistSourceFilePath string
9191
// If set to true, proposer payment is calculated as a balance difference of the fee recipient.
9292
UseBalanceDiffProfit bool
93+
// If set to true, withdrawals to the fee recipient are excluded from the balance difference.
94+
ExcludeWithdrawals bool
9395
}
9496

9597
// Register adds catalyst APIs to the full node.
@@ -106,7 +108,7 @@ func Register(stack *node.Node, backend *eth.Ethereum, cfg BlockValidationConfig
106108
stack.RegisterAPIs([]rpc.API{
107109
{
108110
Namespace: "flashbots",
109-
Service: NewBlockValidationAPI(backend, accessVerifier, cfg.UseBalanceDiffProfit),
111+
Service: NewBlockValidationAPI(backend, accessVerifier, cfg.UseBalanceDiffProfit, cfg.ExcludeWithdrawals),
110112
},
111113
})
112114
return nil
@@ -117,15 +119,18 @@ type BlockValidationAPI struct {
117119
accessVerifier *AccessVerifier
118120
// If set to true, proposer payment is calculated as a balance difference of the fee recipient.
119121
useBalanceDiffProfit bool
122+
// If set to true, withdrawals to the fee recipient are excluded from the balance delta.
123+
excludeWithdrawals bool
120124
}
121125

122126
// NewConsensusAPI creates a new consensus api for the given backend.
123127
// The underlying blockchain needs to have a valid terminal total difficulty set.
124-
func NewBlockValidationAPI(eth *eth.Ethereum, accessVerifier *AccessVerifier, useBalanceDiffProfit bool) *BlockValidationAPI {
128+
func NewBlockValidationAPI(eth *eth.Ethereum, accessVerifier *AccessVerifier, useBalanceDiffProfit, excludeWithdrawals bool) *BlockValidationAPI {
125129
return &BlockValidationAPI{
126130
eth: eth,
127131
accessVerifier: accessVerifier,
128132
useBalanceDiffProfit: useBalanceDiffProfit,
133+
excludeWithdrawals: excludeWithdrawals,
129134
}
130135
}
131136

@@ -185,7 +190,7 @@ func (api *BlockValidationAPI) ValidateBuilderSubmissionV1(params *BuilderBlockV
185190
vmconfig = vm.Config{Tracer: tracer, Debug: true}
186191
}
187192

188-
err = api.eth.BlockChain().ValidatePayload(block, feeRecipient, expectedProfit, params.RegisteredGasLimit, vmconfig, api.useBalanceDiffProfit)
193+
err = api.eth.BlockChain().ValidatePayload(block, feeRecipient, expectedProfit, params.RegisteredGasLimit, vmconfig, api.useBalanceDiffProfit, api.excludeWithdrawals)
189194
if err != nil {
190195
log.Error("invalid payload", "hash", payload.BlockHash.String(), "number", payload.BlockNumber, "parentHash", payload.ParentHash.String(), "err", err)
191196
return err
@@ -277,7 +282,7 @@ func (api *BlockValidationAPI) ValidateBuilderSubmissionV2(params *BuilderBlockV
277282
vmconfig = vm.Config{Tracer: tracer, Debug: true}
278283
}
279284

280-
err = api.eth.BlockChain().ValidatePayload(block, feeRecipient, expectedProfit, params.RegisteredGasLimit, vmconfig, api.useBalanceDiffProfit)
285+
err = api.eth.BlockChain().ValidatePayload(block, feeRecipient, expectedProfit, params.RegisteredGasLimit, vmconfig, api.useBalanceDiffProfit, api.excludeWithdrawals)
281286
if err != nil {
282287
log.Error("invalid payload", "hash", payload.BlockHash.String(), "number", payload.BlockNumber, "parentHash", payload.ParentHash.String(), "err", err)
283288
return err

eth/block-validation/api_test.go

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func TestValidateBuilderSubmissionV1(t *testing.T) {
6969
ethservice.Merger().ReachTTD()
7070
defer n.Close()
7171

72-
api := NewBlockValidationAPI(ethservice, nil, true)
72+
api := NewBlockValidationAPI(ethservice, nil, true, true)
7373
parent := preMergeBlocks[len(preMergeBlocks)-1]
7474

7575
api.eth.APIBackend.Miner().SetEtherbase(testValidatorAddr)
@@ -179,7 +179,7 @@ func TestValidateBuilderSubmissionV2(t *testing.T) {
179179
ethservice.Merger().ReachTTD()
180180
defer n.Close()
181181

182-
api := NewBlockValidationAPI(ethservice, nil, true)
182+
api := NewBlockValidationAPI(ethservice, nil, true, true)
183183
parent := preMergeBlocks[len(preMergeBlocks)-1]
184184

185185
api.eth.APIBackend.Miner().SetEtherbase(testBuilderAddr)
@@ -623,7 +623,7 @@ func TestValidateBuilderSubmissionV2_CoinbasePaymentDefault(t *testing.T) {
623623
ethservice.Merger().ReachTTD()
624624
defer n.Close()
625625

626-
api := NewBlockValidationAPI(ethservice, nil, true)
626+
api := NewBlockValidationAPI(ethservice, nil, true, true)
627627

628628
baseFee := misc.CalcBaseFee(ethservice.BlockChain().Config(), lastBlock.Header())
629629
txs := make(types.Transactions, 0)
@@ -735,8 +735,8 @@ func TestValidateBuilderSubmissionV2_Blocklist(t *testing.T) {
735735
},
736736
}
737737

738-
apiWithBlock := NewBlockValidationAPI(ethservice, accessVerifier, true)
739-
apiNoBlock := NewBlockValidationAPI(ethservice, nil, true)
738+
apiWithBlock := NewBlockValidationAPI(ethservice, accessVerifier, true, true)
739+
apiNoBlock := NewBlockValidationAPI(ethservice, nil, true, true)
740740

741741
baseFee := misc.CalcBaseFee(ethservice.BlockChain().Config(), lastBlock.Header())
742742
blockedTxs := make(types.Transactions, 0)
@@ -782,3 +782,101 @@ func TestValidateBuilderSubmissionV2_Blocklist(t *testing.T) {
782782
})
783783
}
784784
}
785+
786+
// This tests payment when the proposer fee recipient receives CL withdrawal.
787+
func TestValidateBuilderSubmissionV2_ExcludeWithdrawals(t *testing.T) {
788+
genesis, preMergeBlocks := generatePreMergeChain(20)
789+
lastBlock := preMergeBlocks[len(preMergeBlocks)-1]
790+
time := lastBlock.Time() + 5
791+
genesis.Config.ShanghaiTime = &time
792+
n, ethservice := startEthService(t, genesis, preMergeBlocks)
793+
ethservice.Merger().ReachTTD()
794+
defer n.Close()
795+
796+
api := NewBlockValidationAPI(ethservice, nil, true, true)
797+
798+
baseFee := misc.CalcBaseFee(ethservice.BlockChain().Config(), lastBlock.Header())
799+
txs := make(types.Transactions, 0)
800+
801+
statedb, _ := ethservice.BlockChain().StateAt(lastBlock.Root())
802+
nonce := statedb.GetNonce(testAddr)
803+
signer := types.LatestSigner(ethservice.BlockChain().Config())
804+
805+
expectedProfit := uint64(0)
806+
807+
tx1, _ := types.SignTx(types.NewTransaction(nonce, common.Address{0x16}, big.NewInt(10), 21000, big.NewInt(2*baseFee.Int64()), nil), signer, testKey)
808+
txs = append(txs, tx1)
809+
expectedProfit += 21000 * baseFee.Uint64()
810+
811+
// this tx will use 56996 gas
812+
tx2, _ := types.SignTx(types.NewContractCreation(nonce+1, new(big.Int), 1000000, big.NewInt(2*baseFee.Int64()), logCode), signer, testKey)
813+
txs = append(txs, tx2)
814+
expectedProfit += 56996 * baseFee.Uint64()
815+
816+
tx3, _ := types.SignTx(types.NewTransaction(nonce+2, testAddr, big.NewInt(10), 21000, baseFee, nil), signer, testKey)
817+
txs = append(txs, tx3)
818+
819+
// this transaction sends 7 wei to the proposer fee recipient, this should count as a profit
820+
tx4, _ := types.SignTx(types.NewTransaction(nonce+3, testValidatorAddr, big.NewInt(7), 21000, baseFee, nil), signer, testKey)
821+
txs = append(txs, tx4)
822+
expectedProfit += 7
823+
824+
withdrawals := []*types.Withdrawal{
825+
{
826+
Index: 0,
827+
Validator: 1,
828+
Amount: 100,
829+
Address: testAddr,
830+
},
831+
{
832+
Index: 1,
833+
Validator: 1,
834+
Amount: 17,
835+
Address: testValidatorAddr,
836+
},
837+
{
838+
Index: 1,
839+
Validator: 1,
840+
Amount: 21,
841+
Address: testValidatorAddr,
842+
},
843+
}
844+
withdrawalsRoot := types.DeriveSha(types.Withdrawals(withdrawals), trie.NewStackTrie(nil))
845+
846+
buildBlockArgs := buildBlockArgs{
847+
parentHash: lastBlock.Hash(),
848+
parentRoot: lastBlock.Root(),
849+
feeRecipient: testValidatorAddr,
850+
txs: txs,
851+
random: common.Hash{},
852+
number: lastBlock.NumberU64() + 1,
853+
gasLimit: lastBlock.GasLimit(),
854+
timestamp: lastBlock.Time() + 5,
855+
extraData: nil,
856+
baseFeePerGas: baseFee,
857+
withdrawals: withdrawals,
858+
}
859+
860+
execData, err := buildBlock(buildBlockArgs, ethservice.BlockChain())
861+
require.NoError(t, err)
862+
863+
value := big.NewInt(int64(expectedProfit))
864+
865+
req, err := executableDataToBlockValidationRequest(execData, testValidatorAddr, value, withdrawalsRoot)
866+
require.NoError(t, err)
867+
require.NoError(t, api.ValidateBuilderSubmissionV2(req))
868+
869+
// try to claim less profit than expected, should work
870+
value.SetUint64(expectedProfit - 1)
871+
872+
req, err = executableDataToBlockValidationRequest(execData, testValidatorAddr, value, withdrawalsRoot)
873+
require.NoError(t, err)
874+
require.NoError(t, api.ValidateBuilderSubmissionV2(req))
875+
876+
// try to claim more profit than expected, should fail
877+
value.SetUint64(expectedProfit + 1)
878+
879+
req, err = executableDataToBlockValidationRequest(execData, testValidatorAddr, value, withdrawalsRoot)
880+
require.NoError(t, err)
881+
require.ErrorContains(t, api.ValidateBuilderSubmissionV2(req), "payment")
882+
}

0 commit comments

Comments
 (0)