From f3541db9ea20a542217e8a00318603c48233d1ae Mon Sep 17 00:00:00 2001 From: Vitaly Drogan Date: Tue, 20 Jun 2023 16:44:53 +0300 Subject: [PATCH] Allow alternative methods of proposer payments in validation api. --- core/blockchain.go | 52 +++++++++++ eth/block-validation/api_test.go | 11 ++- eth/tracers/logger/balance_diff_tracer.go | 87 +++++++++++++++++++ .../logger/balance_diff_tracer_test.go | 59 +++++++++++++ 4 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 eth/tracers/logger/balance_diff_tracer.go create mode 100644 eth/tracers/logger/balance_diff_tracer_test.go diff --git a/core/blockchain.go b/core/blockchain.go index cdf87c2e56..a65974b2ac 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" @@ -2527,11 +2528,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 +2564,48 @@ func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Ad return err } + // Validate proposer payment + // + // 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 len(receipts) >= i { + log.Error("receipts length is less than balance changes length") + break + } + if receipts[i].EffectiveGasPrice != nil { + log.Error("effective gas price is nil") + break + } + tip := new(big.Int).Sub(receipts[i].EffectiveGasPrice, block.BaseFee()) + profit := tip.Mul(tip, new(big.Int).SetUint64(receipts[i].GasUsed)) + if profit.Sign() < 0 { + log.Error("profit is negative") + break + } + 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_test.go b/eth/block-validation/api_test.go index 412fe3d29e..09669981a0 100644 --- a/eth/block-validation/api_test.go +++ b/eth/block-validation/api_test.go @@ -50,6 +50,10 @@ 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) ) @@ -166,7 +170,7 @@ 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) @@ -176,7 +180,7 @@ func TestValidateBuilderSubmissionV2(t *testing.T) { api := NewBlockValidationAPI(ethservice, nil) parent := preMergeBlocks[len(preMergeBlocks)-1] - api.eth.APIBackend.Miner().SetEtherbase(testValidatorAddr) + api.eth.APIBackend.Miner().SetEtherbase(testBuilderAddr) // This EVM code generates a log when the contract is created. logCode := common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00") @@ -234,7 +238,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, }, 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) +}