Skip to content

Commit

Permalink
Allow alternative methods of proposer payments in validation api.
Browse files Browse the repository at this point in the history
  • Loading branch information
dvush committed Jun 20, 2023
1 parent 351b040 commit f3541db
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 3 deletions.
52 changes: 52 additions & 0 deletions core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand All @@ -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")
}
Expand Down
11 changes: 8 additions & 3 deletions eth/block-validation/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)

Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand Down Expand Up @@ -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,
},
Expand Down
87 changes: 87 additions & 0 deletions eth/tracers/logger/balance_diff_tracer.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
59 changes: 59 additions & 0 deletions eth/tracers/logger/balance_diff_tracer_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit f3541db

Please sign in to comment.