diff --git a/core/types/bundle_gasless.go b/core/types/bundle_gasless.go new file mode 100644 index 0000000000..2ffcefe7ef --- /dev/null +++ b/core/types/bundle_gasless.go @@ -0,0 +1,21 @@ +package types + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +type SimulateGaslessBundleArgs struct { + Txs []hexutil.Bytes `json:"txs"` +} + +type GaslessTxSimResult struct { + Hash common.Hash + GasUsed uint64 + Valid bool +} + +type SimulateGaslessBundleResp struct { + Results []GaslessTxSimResult + BasedBlockNumber int64 +} diff --git a/eth/api_backend.go b/eth/api_backend.go index 1679ac79bf..afbf154054 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -304,6 +304,10 @@ func (b *EthAPIBackend) SendBundle(ctx context.Context, bundle *types.Bundle) er return b.eth.txPool.AddBundle(bundle) } +func (b *EthAPIBackend) SimulateGaslessBundle(bundle *types.Bundle) (*types.SimulateGaslessBundleResp, error) { + return b.Miner().SimulateGaslessBundle(bundle) +} + func (b *EthAPIBackend) BundlePrice() *big.Int { bundles := b.eth.txPool.AllBundles() gasFloor := big.NewInt(b.eth.config.Miner.MevGasPriceFloor) diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index d52354c806..a66598dc5c 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -779,6 +779,16 @@ func (ec *Client) BestBidGasFee(ctx context.Context, parentHash common.Hash) (*b return fee, nil } +// SimulateGaslessBundle simulates a gasless bundle +func (ec *Client) SimulateGaslessBundle(ctx context.Context, args types.SimulateGaslessBundleArgs) (*types.SimulateGaslessBundleResp, error) { + var bundle types.SimulateGaslessBundleResp + err := ec.c.CallContext(ctx, &bundle, "eth_simulateGaslessBundle", args) + if err != nil { + return nil, err + } + return &bundle, nil +} + // SendBundle sends a bundle func (ec *Client) SendBundle(ctx context.Context, args types.SendBundleArgs) (common.Hash, error) { var hash common.Hash diff --git a/internal/ethapi/api_bundle.go b/internal/ethapi/api_bundle.go index cf9e5034e9..f5aae60d6c 100644 --- a/internal/ethapi/api_bundle.go +++ b/internal/ethapi/api_bundle.go @@ -25,6 +25,29 @@ func (s *PrivateTxBundleAPI) BundlePrice(ctx context.Context) *big.Int { return s.b.BundlePrice() } +// SimulateGaslessBundle simulates the execution of a list of transactions with order +func (s *PrivateTxBundleAPI) SimulateGaslessBundle(_ context.Context, args types.SimulateGaslessBundleArgs) (*types.SimulateGaslessBundleResp, error) { + if len(args.Txs) == 0 { + return nil, newBundleError(errors.New("bundle missing txs")) + } + + 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) + } + + bundle := &types.Bundle{ + Txs: txs, + } + + return s.b.SimulateGaslessBundle(bundle) +} + // SendBundle will add the signed transaction to the transaction pool. // The sender is responsible for signing the transaction and using the correct nonce and ensuring validity func (s *PrivateTxBundleAPI) SendBundle(ctx context.Context, args types.SendBundleArgs) (common.Hash, error) { diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 9d687b0d28..a819f26cba 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -615,6 +615,10 @@ func (b testBackend) SendTx(ctx context.Context, signedTx *types.Transaction) er func (b testBackend) SendBundle(ctx context.Context, bundle *types.Bundle) error { panic("implement me") } +func (b *testBackend) SimulateGaslessBundle(bundle *types.Bundle) (*types.SimulateGaslessBundleResp, error) { + //TODO implement me + panic("implement me") +} func (b testBackend) BundlePrice() *big.Int { panic("implement me") } diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index c69b33cce5..f9cf795128 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -79,6 +79,7 @@ type Backend interface { // Transaction pool API SendTx(ctx context.Context, signedTx *types.Transaction) error SendBundle(ctx context.Context, bundle *types.Bundle) error + SimulateGaslessBundle(bundle *types.Bundle) (*types.SimulateGaslessBundleResp, error) BundlePrice() *big.Int GetTransaction(ctx context.Context, txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64, error) GetPoolTransactions() (types.Transactions, error) diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index 60d36b7fd7..fb06e37993 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -389,7 +389,11 @@ func (b *backendMock) SubscribeNewVoteEvent(ch chan<- core.NewVoteEvent) event.S } func (b *backendMock) SendTx(ctx context.Context, signedTx *types.Transaction) error { return nil } func (b *backendMock) SendBundle(ctx context.Context, bundle *types.Bundle) error { return nil } -func (b *backendMock) BundlePrice() *big.Int { return nil } +func (b *backendMock) SimulateGaslessBundle(bundle *types.Bundle) (*types.SimulateGaslessBundleResp, error) { + //TODO implement me + panic("implement me") +} +func (b *backendMock) BundlePrice() *big.Int { return nil } func (b *backendMock) GetTransaction(ctx context.Context, txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64, error) { return false, nil, [32]byte{}, 0, 0, nil } diff --git a/miner/miner.go b/miner/miner.go index 25dab62171..d4a21b6ae3 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -18,7 +18,6 @@ package miner import ( - "errors" "fmt" "math/big" "sync" @@ -310,6 +309,47 @@ func (miner *Miner) GasCeil() uint64 { func (miner *Miner) SimulateBundle(bundle *types.Bundle) (*big.Int, error) { parent := miner.eth.BlockChain().CurrentBlock() + + parentState, err := miner.eth.BlockChain().StateAt(parent.Root) + if err != nil { + return nil, err + } + + env, err := miner.prepareSimulationEnv(parent, parentState) + if err != nil { + return nil, err + } + + s, err := miner.worker.simulateBundle(env, bundle, parentState, env.gasPool, 0, true, true) + if err != nil { + return nil, err + } + + return s.BundleGasPrice, nil +} + +func (miner *Miner) SimulateGaslessBundle(bundle *types.Bundle) (*types.SimulateGaslessBundleResp, error) { + parent := miner.eth.BlockChain().CurrentBlock() + + parentState, err := miner.eth.BlockChain().StateAt(parent.Root) + if err != nil { + return nil, err + } + + env, err := miner.prepareSimulationEnv(parent, parentState) + if err != nil { + return nil, err + } + + resp, err := miner.worker.simulateGaslessBundle(env, bundle) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (miner *Miner) prepareSimulationEnv(parent *types.Header, state *state.StateDB) (*environment, error) { timestamp := time.Now().Unix() if parent.Time >= uint64(timestamp) { timestamp = int64(parent.Time + 1) @@ -352,15 +392,11 @@ func (miner *Miner) SimulateBundle(bundle *types.Bundle) (*big.Int, error) { // } } - state, err := miner.eth.BlockChain().StateAt(parent.Root) - if err != nil { - return nil, err - } - env := &environment{ - header: header, - state: state.Copy(), - signer: types.MakeSigner(miner.worker.chainConfig, header.Number, header.Time), + header: header, + state: state.Copy(), + signer: types.MakeSigner(miner.worker.chainConfig, header.Number, header.Time), + gasPool: prepareGasPool(header.GasLimit), } if !miner.worker.chainConfig.IsFeynman(header.Number, header.Time) { @@ -368,14 +404,5 @@ func (miner *Miner) SimulateBundle(bundle *types.Bundle) (*big.Int, error) { systemcontracts.UpgradeBuildInSystemContract(miner.worker.chainConfig, header.Number, parent.Time, header.Time, env.state) } - s, err := miner.worker.simulateBundles(env, []*types.Bundle{bundle}) - if err != nil { - return nil, err - } - - if len(s) == 0 { - return nil, errors.New("no valid sim result") - } - - return s[0].BundleGasPrice, nil + return env, nil } diff --git a/miner/worker_builder.go b/miner/worker_builder.go index b264180303..8aa1641b33 100644 --- a/miner/worker_builder.go +++ b/miner/worker_builder.go @@ -438,39 +438,52 @@ func (w *worker) simulateBundle( return nil, err } - bundleGasUsed += receipt.GasUsed + if !w.eth.TxPool().Has(tx.Hash()) { + bundleGasUsed += receipt.GasUsed - txGasUsed := new(big.Int).SetUint64(receipt.GasUsed) - effectiveTip, err := tx.EffectiveGasTip(env.header.BaseFee) - if err != nil { - return nil, err - } - txGasFees := new(big.Int).Mul(txGasUsed, effectiveTip) + txGasUsed := new(big.Int).SetUint64(receipt.GasUsed) + effectiveTip, er := tx.EffectiveGasTip(env.header.BaseFee) + if er != nil { + return nil, er + } + + if env.header.BaseFee != nil { + log.Info("simulate bundle: header base fee", "value", env.header.BaseFee.String()) + effectiveTip.Add(effectiveTip, env.header.BaseFee) + } + + txGasFees := new(big.Int).Mul(txGasUsed, effectiveTip) - if tx.Type() == types.BlobTxType { - blobFee := new(big.Int).SetUint64(receipt.BlobGasUsed) - blobFee.Mul(blobFee, receipt.BlobGasPrice) - txGasFees.Add(txGasFees, blobFee) + if tx.Type() == types.BlobTxType { + blobFee := new(big.Int).SetUint64(receipt.BlobGasUsed) + blobFee.Mul(blobFee, receipt.BlobGasPrice) + txGasFees.Add(txGasFees, blobFee) + } + bundleGasFees.Add(bundleGasFees, txGasFees) + sysBalanceAfter := state.GetBalance(consensus.SystemAddress) + sysDelta := new(uint256.Int).Sub(sysBalanceAfter, sysBalanceBefore) + sysDelta.Sub(sysDelta, uint256.MustFromBig(txGasFees)) + ethSentToSystem.Add(ethSentToSystem, sysDelta.ToBig()) } - bundleGasFees.Add(bundleGasFees, txGasFees) - sysBalanceAfter := state.GetBalance(consensus.SystemAddress) - sysDelta := new(uint256.Int).Sub(sysBalanceAfter, sysBalanceBefore) - sysDelta.Sub(sysDelta, uint256.MustFromBig(txGasFees)) - ethSentToSystem.Add(ethSentToSystem, sysDelta.ToBig()) } - bundleGasPrice := new(big.Int).Div(bundleGasFees, new(big.Int).SetUint64(bundleGasUsed)) + // if all txs in the bundle are from mempool, we accept the bundle without checking gas price + bundleGasPrice := big.NewInt(0) - if bundleGasPrice.Cmp(big.NewInt(w.config.MevGasPriceFloor)) < 0 { - err := errBundlePriceTooLow - log.Warn("fail to simulate bundle", "hash", bundle.Hash().String(), "err", err) + if bundleGasUsed != 0 { + bundleGasPrice = new(big.Int).Div(bundleGasFees, new(big.Int).SetUint64(bundleGasUsed)) - if prune { - log.Warn("prune bundle", "hash", bundle.Hash().String()) - w.eth.TxPool().PruneBundle(bundle.Hash()) - } + if bundleGasPrice.Cmp(big.NewInt(w.config.MevGasPriceFloor)) < 0 { + err := errBundlePriceTooLow + log.Warn("fail to simulate bundle", "hash", bundle.Hash().String(), "err", err) + + if prune { + log.Warn("prune bundle", "hash", bundle.Hash().String()) + w.eth.TxPool().PruneBundle(bundle.Hash()) + } - return nil, err + return nil, err + } } return &types.SimulatedBundle{ @@ -482,6 +495,43 @@ func (w *worker) simulateBundle( }, nil } +func (w *worker) simulateGaslessBundle(env *environment, bundle *types.Bundle) (*types.SimulateGaslessBundleResp, error) { + result := make([]types.GaslessTxSimResult, 0) + + txIdx := 0 + for _, tx := range bundle.Txs { + env.state.SetTxContext(tx.Hash(), txIdx) + + var ( + snap = env.state.Snapshot() + gp = env.gasPool.Gas() + valid = true + ) + + receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, &w.coinbase, env.gasPool, env.state, env.header, tx, + &env.header.GasUsed, *w.chain.GetVMConfig()) + if err != nil { + env.state.RevertToSnapshot(snap) + env.gasPool.SetGas(gp) + valid = false + log.Warn("fail to simulate gasless bundle, skipped", "txHash", tx.Hash(), "err", err) + } else { + txIdx++ + } + + result = append(result, types.GaslessTxSimResult{ + Hash: tx.Hash(), + GasUsed: receipt.GasUsed, + Valid: valid, + }) + } + + return &types.SimulateGaslessBundleResp{ + Results: result, + BasedBlockNumber: env.header.Number.Int64(), + }, nil +} + func containsHash(arr []common.Hash, match common.Hash) bool { for _, elem := range arr { if elem == match {