Skip to content

Commit

Permalink
mev_callBundle support
Browse files Browse the repository at this point in the history
  • Loading branch information
roshanrags committed Jun 30, 2023
1 parent a280d6d commit c4220d2
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 7 deletions.
10 changes: 10 additions & 0 deletions consensus/bor/bor.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,16 @@ func (c *Bor) snapshot(chain consensus.ChainHeaderReader, number uint64, hash co
return snap, err
}

// CallBundle API requires the proposer at the specified block
func (c *Bor) GetProposer(chain consensus.ChainHeaderReader, parent *types.Header) (common.Address, error) {
snap, err := c.snapshot(chain, parent.Number.Uint64(), parent.Hash(), nil)
if err != nil {
return common.Address{}, err
}

return snap.ValidatorSet.GetProposer().Address, nil
}

// VerifyUncles implements consensus.Engine, always returning an error for any
// uncles as this consensus mechanism doesn't permit uncles.
func (c *Bor) VerifyUncles(_ consensus.ChainReader, block *types.Block) error {
Expand Down
56 changes: 56 additions & 0 deletions core/state_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,59 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo

return applyTransaction(msg, config, bc, author, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv, interruptCtx)
}

func applyTransactionWithResult(msg types.Message, config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, evm *vm.EVM, interruptCtx context.Context) (*types.Receipt, *ExecutionResult, error) {
// Create a new context to be used in the EVM environment.
txContext := NewEVMTxContext(msg)
evm.Reset(txContext, statedb)

// Apply the transaction to the current state (included in the env).
result, err := ApplyMessage(evm, msg, gp, interruptCtx)
if err != nil {
return nil, nil, err
}

// Update the state with pending changes.
var root []byte
if config.IsByzantium(header.Number) {
statedb.Finalise(true)
} else {
root = statedb.IntermediateRoot(config.IsEIP158(header.Number)).Bytes()
}
*usedGas += result.UsedGas

// Create a new receipt for the transaction, storing the intermediate root and gas used
// by the tx.
receipt := &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: *usedGas}
if result.Failed() {
receipt.Status = types.ReceiptStatusFailed
} else {
receipt.Status = types.ReceiptStatusSuccessful
}
receipt.TxHash = tx.Hash()
receipt.GasUsed = result.UsedGas

// If the transaction created a contract, store the creation address in the receipt.
if msg.To() == nil {
receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, tx.Nonce())
}

// Set the receipt logs and create the bloom filter.
receipt.Logs = statedb.GetLogs(tx.Hash(), header.Hash())
receipt.Bloom = types.CreateBloom(types.Receipts{receipt})
receipt.BlockHash = header.Hash()
receipt.BlockNumber = header.Number
receipt.TransactionIndex = uint(statedb.TxIndex())
return receipt, result, err
}

func ApplyTransactionWithResult(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config, interruptCtx context.Context) (*types.Receipt, *ExecutionResult, error) {
msg, err := tx.AsMessage(types.MakeSigner(config, header.Number), header.BaseFee)
if err != nil {
return nil, nil, err
}
// Create a new context to be used in the EVM environment
blockContext := NewEVMBlockContext(header, bc, author)
vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, config, cfg)
return applyTransactionWithResult(msg, config, bc, author, gp, statedb, header, tx, usedGas, vmenv, interruptCtx)
}
2 changes: 1 addition & 1 deletion eth/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ func makeExtraData(extra []byte) []byte {
// APIs return the collection of RPC services the ethereum package offers.
// NOTE, some of these services probably need to be moved to somewhere else.
func (s *Ethereum) APIs() []rpc.API {
apis := ethapi.GetAPIs(s.APIBackend)
apis := ethapi.GetAPIs(s.APIBackend, s.BlockChain())

// Append any APIs exposed explicitly by the consensus engine
apis = append(apis, s.engine.APIs(s.BlockChain())...)
Expand Down
215 changes: 212 additions & 3 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package ethapi

import (
"context"
"encoding/hex"
"errors"
"fmt"
"math/big"
Expand All @@ -35,6 +36,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/clique"
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/consensus/misc"
Expand All @@ -51,6 +53,7 @@ import (
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/rpc"
"golang.org/x/crypto/sha3"
)

// PublicEthereumAPI provides an API to access Ethereum related information.
Expand Down Expand Up @@ -2343,12 +2346,13 @@ func toHexSlice(b [][]byte) []string {

// PrivateTxBundleAPI offers an API for accepting bundled transactions
type PrivateTxBundleAPI struct {
b Backend
b Backend
chain *core.BlockChain
}

// NewPrivateTxBundleAPI creates a new Tx Bundle API instance.
func NewPrivateTxBundleAPI(b Backend) *PrivateTxBundleAPI {
return &PrivateTxBundleAPI{b}
func NewPrivateTxBundleAPI(b Backend, chain *core.BlockChain) *PrivateTxBundleAPI {
return &PrivateTxBundleAPI{b, chain}
}

// SendBundleArgs represents the arguments for a call.
Expand Down Expand Up @@ -2389,3 +2393,208 @@ func (s *PrivateTxBundleAPI) SendBundle(ctx context.Context, args SendBundleArgs

return s.b.SendBundle(ctx, txs, args.BlockNumber, minTimestamp, maxTimestamp, args.RevertingTxHashes)
}

// CallBundleArgs represents the arguments for a call.
type CallBundleArgs struct {
Txs []hexutil.Bytes `json:"txs"`
BlockNumber rpc.BlockNumber `json:"blockNumber"`
StateBlockNumberOrHash rpc.BlockNumberOrHash `json:"stateBlockNumber"`
Coinbase *string `json:"coinbase"`
Timestamp *uint64 `json:"timestamp"`
Timeout *int64 `json:"timeout"`
GasLimit *uint64 `json:"gasLimit"`
Difficulty *big.Int `json:"difficulty"`
BaseFee *big.Int `json:"baseFee"`
}

// CallBundle will simulate a bundle of transactions at the top of a given block
// number with the state of another (or the same) block. This can be used to
// simulate future blocks with the current state, or it can be used to simulate
// a past block.
// The sender is responsible for signing the transactions and using the correct
// nonce and ensuring validity
func (s *PrivateTxBundleAPI) CallBundle(ctx context.Context, args CallBundleArgs) (map[string]interface{}, error) {
if len(args.Txs) == 0 {
return nil, errors.New("bundle missing txs")
}
if args.BlockNumber == 0 {
return nil, errors.New("bundle missing blockNumber")
}

var txs types.Transactions

for _, encodedTx := range args.Txs {
tx := new(types.Transaction)
if err := tx.UnmarshalBinary(encodedTx); err != nil {
return nil, err
}
txs = append(txs, tx)
}
defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now())

timeoutMilliSeconds := int64(5000)
if args.Timeout != nil {
timeoutMilliSeconds = *args.Timeout
}
timeout := time.Millisecond * time.Duration(timeoutMilliSeconds)
state, parent, err := s.b.StateAndHeaderByNumberOrHash(ctx, args.StateBlockNumberOrHash)
if state == nil || err != nil {
return nil, err
}
blockNumber := big.NewInt(int64(args.BlockNumber))

// Reject mev-bor requests with invalid block numbers
// TODO: Add API to engine interface
type BorInfo interface {
GetProposer(chain consensus.ChainHeaderReader, parent *types.Header) (common.Address, error)
}

bor, isBorEngine := s.b.Engine().(BorInfo)
if isBorEngine && parent.Number.Uint64() != s.chain.CurrentHeader().Number.Uint64() {
return nil, errors.New("Please simulate on top of the latest block!")
}

if isBorEngine && blockNumber.Uint64() != parent.Number.Uint64()+1 {
return nil, errors.New("Please target the in-progress block for simulation!")
}

timestamp := parent.Time + 1
if args.Timestamp != nil {
timestamp = *args.Timestamp
}
var coinbase common.Address
if args.Coinbase != nil {
coinbase = common.HexToAddress(*args.Coinbase)
} else {
if isBorEngine {
coinbase, err = bor.GetProposer(s.chain, parent)
if err != nil {
return nil, err
}
} else {
coinbase = parent.Coinbase
}
}
difficulty := parent.Difficulty
if args.Difficulty != nil {
difficulty = args.Difficulty
}
gasLimit := parent.GasLimit
if args.GasLimit != nil {
gasLimit = *args.GasLimit
}
var baseFee *big.Int
if args.BaseFee != nil {
baseFee = args.BaseFee
} else if s.b.ChainConfig().IsLondon(big.NewInt(args.BlockNumber.Int64())) {
baseFee = misc.CalcBaseFee(s.b.ChainConfig(), parent)
}
header := &types.Header{
ParentHash: parent.Hash(),
Number: blockNumber,
GasLimit: gasLimit,
Time: timestamp,
Difficulty: difficulty,
Coinbase: coinbase,
BaseFee: baseFee,
}

// Setup context so it may be cancelled the call has completed
// or, in case of unmetered gas, setup a context with a timeout.
var cancel context.CancelFunc
if timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, timeout)
} else {
ctx, cancel = context.WithCancel(ctx)
}
// Make sure the context is cancelled when the call has completed
// this makes sure resources are cleaned up.
defer cancel()

vmconfig := vm.Config{}

// Setup the gas pool (also for unmetered requests)
// and apply the message.
gp := new(core.GasPool).AddGas(math.MaxUint64)

results := []map[string]interface{}{}
coinbaseBalanceBefore := state.GetBalance(coinbase)

bundleHash := sha3.NewLegacyKeccak256()
signer := types.MakeSigner(s.b.ChainConfig(), blockNumber)
var totalGasUsed uint64
gasFees := new(big.Int)
for i, tx := range txs {
coinbaseBalanceBeforeTx := state.GetBalance(coinbase)
state.Prepare(tx.Hash(), i)

receipt, result, err := core.ApplyTransactionWithResult(s.b.ChainConfig(), s.chain, &coinbase, gp, state, header, tx, &header.GasUsed, vmconfig, ctx)
if err != nil {
return nil, fmt.Errorf("err: %w; txhash %s", err, tx.Hash())
}

txHash := tx.Hash().String()
from, err := types.Sender(signer, tx)
if err != nil {
return nil, fmt.Errorf("err: %w; txhash %s", err, tx.Hash())
}
to := "0x"
if tx.To() != nil {
to = tx.To().String()
}
jsonResult := map[string]interface{}{
"txHash": txHash,
"gasUsed": receipt.GasUsed,
"fromAddress": from.String(),
"toAddress": to,
}
totalGasUsed += receipt.GasUsed
gasPrice, err := tx.EffectiveGasTip(header.BaseFee)
if err != nil {
return nil, fmt.Errorf("err: %w; txhash %s", err, tx.Hash())
}
gasFeesTx := new(big.Int).Mul(big.NewInt(int64(receipt.GasUsed)), gasPrice)
gasFees.Add(gasFees, gasFeesTx)
bundleHash.Write(tx.Hash().Bytes())
if result.Err != nil {
jsonResult["error"] = result.Err.Error()
revert := result.Revert()
if len(revert) > 0 {
jsonResult["revert"] = string(revert)
}
} else {
dst := make([]byte, hex.EncodedLen(len(result.Return())))
hex.Encode(dst, result.Return())
jsonResult["value"] = "0x" + string(dst)
}
coinbaseDiffTx := new(big.Int).Sub(state.GetBalance(coinbase), coinbaseBalanceBeforeTx)
jsonResult["coinbaseDiff"] = coinbaseDiffTx.String()
jsonResult["gasFees"] = gasFeesTx.String()
jsonResult["ethSentToCoinbase"] = new(big.Int).Sub(coinbaseDiffTx, gasFeesTx).String()
jsonResult["gasPrice"] = new(big.Int).Div(coinbaseDiffTx, big.NewInt(int64(receipt.GasUsed))).String()
jsonResult["gasUsed"] = receipt.GasUsed
results = append(results, jsonResult)
}

ret := map[string]interface{}{}
ret["results"] = results
coinbaseDiff := new(big.Int).Sub(state.GetBalance(coinbase), coinbaseBalanceBefore)
ret["coinbaseDiff"] = coinbaseDiff.String()
ret["gasFees"] = gasFees.String()
ret["ethSentToCoinbase"] = new(big.Int).Sub(coinbaseDiff, gasFees).String()
ret["bundleGasPrice"] = new(big.Int).Div(coinbaseDiff, big.NewInt(int64(totalGasUsed))).String()
ret["totalGasUsed"] = totalGasUsed
ret["stateBlockNumber"] = parent.Number.Int64()

ret["bundleHash"] = "0x" + common.Bytes2Hex(bundleHash.Sum(nil))

// Retrieve block producer in bor when available
if isBorEngine {
proposer, err := bor.GetProposer(s.chain, parent)
if err == nil {
ret["proposer"] = proposer.Hex()
}
}

return ret, nil
}
4 changes: 2 additions & 2 deletions internal/ethapi/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ type Backend interface {
Engine() consensus.Engine
}

func GetAPIs(apiBackend Backend) []rpc.API {
func GetAPIs(apiBackend Backend, chain *core.BlockChain) []rpc.API {
nonceLock := new(AddrLocker)
return []rpc.API{
{
Expand Down Expand Up @@ -155,7 +155,7 @@ func GetAPIs(apiBackend Backend) []rpc.API {
}, {
Namespace: "mev",
Version: "1.0",
Service: NewPrivateTxBundleAPI(apiBackend),
Service: NewPrivateTxBundleAPI(apiBackend, chain),
Public: false,
},
}
Expand Down
2 changes: 1 addition & 1 deletion les/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ func (s *LightDummyAPI) Mining() bool {
// APIs returns the collection of RPC services the ethereum package offers.
// NOTE, some of these services probably need to be moved to somewhere else.
func (s *LightEthereum) APIs() []rpc.API {
apis := ethapi.GetAPIs(s.ApiBackend)
apis := ethapi.GetAPIs(s.ApiBackend, nil)
apis = append(apis, s.engine.APIs(s.BlockChain().HeaderChain())...)
return append(apis, []rpc.API{
{
Expand Down

0 comments on commit c4220d2

Please sign in to comment.