diff --git a/circleciconfig.toml b/circleciconfig.toml index 5fe40629c8..edc119fa42 100644 --- a/circleciconfig.toml +++ b/circleciconfig.toml @@ -1,5 +1,5 @@ [Eth] Rip7560MaxBundleSize = 0 Rip7560MaxBundleGas = 0 -Rip7560PullUrls = ["http://localhost:7560/rpc"] +Rip7560PullUrls = ["http://localhost:3001/rpc"] Rip7560AcceptPush = false diff --git a/core/state_processor_rip7560.go b/core/state_processor_rip7560.go index feca04cca4..c8fdf39d02 100644 --- a/core/state_processor_rip7560.go +++ b/core/state_processor_rip7560.go @@ -44,7 +44,7 @@ type ValidationPhaseResult struct { PmValidUntil uint64 } -func (vpr *ValidationPhaseResult) validationPhaseUsedGas() (uint64, error) { +func (vpr *ValidationPhaseResult) ValidationPhaseUsedGas() (uint64, error) { return types.SumGas( vpr.PreTransactionGasCost, vpr.NonceManagerUsedGas, @@ -216,7 +216,7 @@ func handleRip7560Transactions( // TODO: this will miss all validation phase events - pass in 'vpr' // statedb.SetTxContext(vpr.Tx.Hash(), i) - receipt, err := ApplyRip7560ExecutionPhase(chainConfig, vpr, bc, coinbase, gp, statedb, header, cfg, usedGas) + receipt, _, _, err := ApplyRip7560ExecutionPhase(chainConfig, vpr, bc, coinbase, gp, statedb, header, cfg, usedGas) if err != nil { return nil, nil, nil, nil, err @@ -229,7 +229,7 @@ func handleRip7560Transactions( return validatedTransactions, receipts, validationFailureInfos, allLogs, nil } -func calculateRollupCost( +func CalculateRollupCost( chainConfig *params.ChainConfig, header *types.Header, tx *types.Transaction, @@ -352,7 +352,13 @@ func ApplyRip7560ValidationPhases( header *types.Header, tx *types.Transaction, cfg vm.Config, + allowSigFailFlag ...bool, ) (*ValidationPhaseResult, error) { + var allowSigFail = false + if len(allowSigFailFlag) > 0 && allowSigFailFlag[0] { + allowSigFail = allowSigFailFlag[0] + } + aatx := tx.Rip7560TransactionData() err := performStaticValidation(aatx, statedb) if err != nil { @@ -363,7 +369,7 @@ func ApplyRip7560ValidationPhases( effectiveGasPrice := uint256.MustFromBig(gasPrice) // calculate rollup cost - rollupCost, err := calculateRollupCost(chainConfig, header, tx, statedb) + rollupCost, err := CalculateRollupCost(chainConfig, header, tx, statedb) if err != nil { return nil, err } @@ -461,7 +467,7 @@ func ApplyRip7560ValidationPhases( true, ) } - aad, err := validateAccountEntryPointCall(epc, aatx.Sender) + aad, err := validateAccountEntryPointCall(epc, aatx.Sender, allowSigFail) if err != nil { return nil, wrapError(err) } @@ -476,7 +482,7 @@ func ApplyRip7560ValidationPhases( return nil, wrapError(err) } - paymasterContext, pmValidationUsedGas, pmValidAfter, pmValidUntil, err := applyPaymasterValidationFrame(st, epc, tx, signingHash, header) + paymasterContext, pmValidationUsedGas, pmValidAfter, pmValidUntil, err := applyPaymasterValidationFrame(st, epc, tx, signingHash, header, allowSigFail) if err != nil { return nil, err } @@ -595,7 +601,7 @@ func performStaticValidation( return nil } -func applyPaymasterValidationFrame(st *StateTransition, epc *EntryPointCall, tx *types.Transaction, signingHash common.Hash, header *types.Header) ([]byte, uint64, uint64, uint64, error) { +func applyPaymasterValidationFrame(st *StateTransition, epc *EntryPointCall, tx *types.Transaction, signingHash common.Hash, header *types.Header, estimate bool) ([]byte, uint64, uint64, uint64, error) { /*** Paymaster Validation Frame ***/ aatx := tx.Rip7560TransactionData() var pmValidationUsedGas uint64 @@ -617,7 +623,7 @@ func applyPaymasterValidationFrame(st *StateTransition, epc *EntryPointCall, tx ) } pmValidationUsedGas = resultPm.UsedGas - apd, err := validatePaymasterEntryPointCall(epc, aatx.Paymaster) + apd, err := validatePaymasterEntryPointCall(epc, aatx.Paymaster, estimate) if err != nil { return nil, 0, 0, 0, wrapError(err) } @@ -661,7 +667,7 @@ func ApplyRip7560ExecutionPhase( header *types.Header, cfg vm.Config, usedGas *uint64, -) (*types.Receipt, error) { +) (*types.Receipt, *ExecutionResult, *ExecutionResult, error) { blockContext := NewEVMBlockContext(header, bc, author, config, statedb) aatx := vpr.Tx.Rip7560TransactionData() @@ -688,7 +694,7 @@ func ApplyRip7560ExecutionPhase( } executionGasPenalty := (aatx.Gas - executionResult.UsedGas) * AA_GAS_PENALTY_PCT / 100 - validationPhaseUsedGas, _ := vpr.validationPhaseUsedGas() + validationPhaseUsedGas, _ := vpr.ValidationPhaseUsedGas() gasUsed := validationPhaseUsedGas + executionResult.UsedGas + executionGasPenalty @@ -729,24 +735,24 @@ func ApplyRip7560ExecutionPhase( err := injectRIP7560TransactionEvent(aatx, executionStatus, header, statedb) if err != nil { - return nil, err + return nil, nil, nil, err } if aatx.Deployer != nil { err = injectRIP7560AccountDeployedEvent(aatx, header, statedb) if err != nil { - return nil, err + return nil, nil, nil, err } } if executionResult.Failed() { err = injectRIP7560TransactionRevertReasonEvent(aatx, executionResult.ReturnData, header, statedb) if err != nil { - return nil, err + return nil, nil, nil, err } } if paymasterPostOpResult != nil && paymasterPostOpResult.Failed() { err = injectRIP7560TransactionPostOpRevertReasonEvent(aatx, paymasterPostOpResult.ReturnData, header, statedb) if err != nil { - return nil, err + return nil, nil, nil, err } } @@ -763,7 +769,7 @@ func ApplyRip7560ExecutionPhase( receipt.Bloom = types.CreateBloom(types.Receipts{receipt}) receipt.TransactionIndex = uint(vpr.TxIndex) // other fields are filled in DeriveFields (all tx, block fields, and updating CumulativeGasUsed - return receipt, nil + return receipt, executionResult, paymasterPostOpResult, nil } func injectRIP7560TransactionEvent( @@ -892,7 +898,7 @@ func preparePostOpMessage(vpr *ValidationPhaseResult, success bool, gasUsed uint return abiEncodePostPaymasterTransaction(success, gasUsed, vpr.PaymasterContext) } -func validateAccountEntryPointCall(epc *EntryPointCall, sender *common.Address) (*AcceptAccountData, error) { +func validateAccountEntryPointCall(epc *EntryPointCall, sender *common.Address, allowSigFail bool) (*AcceptAccountData, error) { if epc.err != nil { return nil, epc.err } @@ -902,10 +908,10 @@ func validateAccountEntryPointCall(epc *EntryPointCall, sender *common.Address) if epc.From.Cmp(*sender) != 0 { return nil, errors.New("invalid call to EntryPoint contract from a wrong account address") } - return abiDecodeAcceptAccount(epc.Input, false) + return abiDecodeAcceptAccount(epc.Input, allowSigFail) } -func validatePaymasterEntryPointCall(epc *EntryPointCall, paymaster *common.Address) (*AcceptPaymasterData, error) { +func validatePaymasterEntryPointCall(epc *EntryPointCall, paymaster *common.Address, allowSigFail bool) (*AcceptPaymasterData, error) { if epc.err != nil { return nil, epc.err } @@ -916,7 +922,7 @@ func validatePaymasterEntryPointCall(epc *EntryPointCall, paymaster *common.Addr if epc.From.Cmp(*paymaster) != 0 { return nil, errors.New("invalid call to EntryPoint contract from a wrong paymaster address") } - apd, err := abiDecodeAcceptPaymaster(epc.Input, false) + apd, err := abiDecodeAcceptPaymaster(epc.Input, allowSigFail) if err != nil { return nil, err } diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go index fbcdbc8b35..97904abbd4 100644 --- a/eth/gasestimator/gasestimator.go +++ b/eth/gasestimator/gasestimator.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "github.com/holiman/uint256" "math" "math/big" @@ -44,6 +45,11 @@ type Options struct { State *state.StateDB // Pre-state on top of which to estimate the gas ErrorRatio float64 // Allowed overestimation ratio for faster estimation termination + + // RIP-7560 specific fields + Payment *common.Address + PrepaidGas *uint256.Int + ValidationPhaseResult *core.ValidationPhaseResult } // Estimate returns the lowest possible gas limit that allows the transaction to diff --git a/eth/gasestimator/gasestimator_rip7560.go b/eth/gasestimator/gasestimator_rip7560.go new file mode 100644 index 0000000000..426f812c52 --- /dev/null +++ b/eth/gasestimator/gasestimator_rip7560.go @@ -0,0 +1,344 @@ +package gasestimator + +import ( + "context" + "errors" + "fmt" + "math" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" +) + +func executeRip7560Validation(ctx context.Context, tx *types.Transaction, opts *Options, gasLimit uint64) (*core.ValidationPhaseResult, *state.StateDB, error) { + st := tx.Rip7560TransactionData() + // Configure the call for this specific execution (and revert the change after) + defer func(gas uint64) { st.ValidationGasLimit = gas }(st.ValidationGasLimit) + st.ValidationGasLimit = gasLimit + + // Execute the call and separate execution faults caused by a lack of gas or + // other non-fixable conditions + var ( + blockContext = core.NewEVMBlockContext(opts.Header, opts.Chain, nil, opts.Config, opts.State) + txContext = vm.TxContext{ + Origin: *tx.Rip7560TransactionData().Sender, + GasPrice: tx.GasFeeCap(), + } + + dirtyState = opts.State.Copy() + evm = vm.NewEVM(blockContext, txContext, dirtyState, opts.Config, vm.Config{NoBaseFee: true}) + ) + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + go func() { + <-ctx.Done() + evm.Cancel() + }() + + // Gas Pool is set to half of the maximum possible gas to prevent overflow + vpr, err := core.ApplyRip7560ValidationPhases(opts.Config, opts.Chain, &opts.Header.Coinbase, new(core.GasPool).AddGas(math.MaxUint64/2), dirtyState, opts.Header, tx, evm.Config, true) + if err != nil { + if errors.Is(err, vm.ErrOutOfGas) { + return nil, nil, nil // Special case, raise gas limit + } + return nil, nil, err // Bail out + } + return vpr, dirtyState, nil +} + +func EstimateRip7560Validation(ctx context.Context, tx *types.Transaction, opts *Options, gasCap uint64) (uint64, error) { + // Binary search the gas limit, as it may need to be higher than the amount used + st := tx.Rip7560TransactionData() + gasLimit, err := st.TotalGasLimit() + if err != nil { + return 0, err + } + var ( + lo uint64 // lowest-known gas limit where tx execution fails + hi uint64 // lowest-known gas limit where tx execution succeeds + ) + // Determine the highest gas limit can be used during the estimation. + hi = opts.Header.GasLimit + if gasLimit >= params.TxGas { + hi = gasLimit + } + // Normalize the max fee per gas the call is willing to spend. + var feeCap *big.Int + if st.GasFeeCap != nil { + feeCap = st.GasFeeCap + } else { + feeCap = common.Big0 + } + // Recap the highest gas limit with account's available balance. + if feeCap.BitLen() != 0 { + var payment common.Address + if st.Paymaster == nil { + payment = *st.Sender + } else { + payment = *st.Paymaster + } + balance := opts.State.GetBalance(payment).ToBig() + + allowance := new(big.Int).Div(balance, feeCap) + + // If the allowance is larger than maximum uint64, skip checking + if allowance.IsUint64() && hi > allowance.Uint64() { + log.Debug("Gas estimation capped by limited funds", "original", hi, "balance", balance, + "maxFeePerGas", feeCap, "fundable", allowance) + hi = allowance.Uint64() + } + } + // Recap the highest gas allowance with specified gascap. + if gasCap != 0 && hi > gasCap { + log.Debug("Caller gas above allowance, capping", "requested", hi, "cap", gasCap) + hi = gasCap + } + + // We first execute the transaction at the highest allowable gas limit, since if this fails we + // can return error immediately. + vpr, statedb, err := executeRip7560Validation(ctx, tx, opts, hi) + if err != nil { + return 0, err + } else if vpr == nil && err == nil { + return 0, fmt.Errorf("gas required exceeds allowance (%d)", hi) + } + // For almost any transaction, the gas consumed by the unconstrained execution + // above lower-bounds the gas limit required for it to succeed. One exception + // is those that explicitly check gas remaining in order to execute within a + // given limit, but we probably don't want to return the lowest possible gas + // limit for these cases anyway. + vpUsedGas, _ := vpr.ValidationPhaseUsedGas() + lo = vpUsedGas - 1 + + // There's a fairly high chance for the transaction to execute successfully + // with gasLimit set to the first execution's usedGas + gasRefund. Explicitly + // check that gas amount and use as a limit for the binary search. + optimisticGasLimit := (vpUsedGas + params.CallStipend) * 64 / 63 + if optimisticGasLimit < hi { + vpr, statedb, err = executeRip7560Validation(ctx, tx, opts, optimisticGasLimit) + if err != nil { + // This should not happen under normal conditions since if we make it this far the + // transaction had run without error at least once before. + log.Error("Execution error in estimate gas", "err", err) + return 0, err + } + if vpr == nil { + lo = optimisticGasLimit + } else { + hi = optimisticGasLimit + } + } + // Binary search for the smallest gas limit that allows the tx to execute successfully. + for lo+1 < hi { + if opts.ErrorRatio > 0 { + // It is a bit pointless to return a perfect estimation, as changing + // network conditions require the caller to bump it up anyway. Since + // wallets tend to use 20-25% bump, allowing a small approximation + // error is fine (as long as it's upwards). + if float64(hi-lo)/float64(hi) < opts.ErrorRatio { + break + } + } + mid := (hi + lo) / 2 + if mid > lo*2 { + // Most txs don't need much higher gas limit than their gas used, and most txs don't + // require near the full block limit of gas, so the selection of where to bisect the + // range here is skewed to favor the low side. + mid = lo * 2 + } + vpr, statedb, err = executeRip7560Validation(ctx, tx, opts, mid) + if err != nil { + // This should not happen under normal conditions since if we make it this far the + // transaction had run without error at least once before. + log.Error("Execution error in estimate gas", "err", err) + return 0, err + } + if vpr == nil { + lo = mid + } else { + hi = mid + } + } + + opts.ValidationPhaseResult = vpr + opts.State = statedb + return hi, nil +} + +func executeRip7560Execution(ctx context.Context, tx *types.Transaction, opts *Options, gasLimit uint64) (bool, *core.ExecutionResult, *core.ExecutionResult, error) { + st := tx.Rip7560TransactionData() + // Configure the call for this specific execution (and revert the change after) + defer func(gas uint64) { st.Gas = gas }(st.Gas) + st.Gas = gasLimit + + // Execute the call and separate execution faults caused by a lack of gas or + // other non-fixable conditions + var ( + blockContext = core.NewEVMBlockContext(opts.Header, opts.Chain, nil, opts.Config, opts.State) + txContext = vm.TxContext{ + Origin: *tx.Rip7560TransactionData().Sender, + GasPrice: tx.GasFeeCap(), + } + + dirtyState = opts.State.Copy() + evm = vm.NewEVM(blockContext, txContext, dirtyState, opts.Config, vm.Config{NoBaseFee: true}) + ) + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + go func() { + <-ctx.Done() + evm.Cancel() + }() + + // Gas Pool is set to half of the maximum possible gas to prevent overflow. + // Unused gas penalty is not taken into account, since it does not affect the estimation. + _, exr, ppr, err := core.ApplyRip7560ExecutionPhase(opts.Config, opts.ValidationPhaseResult, opts.Chain, &opts.Header.Coinbase, new(core.GasPool).AddGas(math.MaxUint64/2), dirtyState, opts.Header, vm.Config{NoBaseFee: true}, new(uint64)) + //exr, ppr, _, err := core.ApplyRip7560ExecutionPhase(opts.Config, opts.ValidationPhaseResult, opts.Chain, &opts.Header.Coinbase, new(core.GasPool).AddGas(math.MaxUint64/2), dirtyState, opts.Header, vm.Config{NoBaseFee: true}) + if err != nil { + if errors.Is(err, core.ErrIntrinsicGas) { + return true, nil, nil, nil // Special case, raise gas limit + } + return true, nil, nil, err // Bail out + } + return false, exr, ppr, nil +} + +func EstimateRip7560Execution(ctx context.Context, opts *Options, gasCap uint64) (uint64, []byte, error) { + // Binary search the gas limit, as it may need to be higher than the amount used + tx := opts.ValidationPhaseResult.Tx + st := tx.Rip7560TransactionData() + gasLimit := st.Gas + st.PostOpGas + var ( + lo uint64 // lowest-known gas limit where tx execution fails + hi uint64 // lowest-known gas limit where tx execution succeeds + ) + // Determine the highest gas limit can be used during the estimation. + hi = opts.Header.GasLimit + if gasLimit >= params.TxGas { + hi = gasLimit + } + // Normalize the max fee per gas the call is willing to spend. + var feeCap *big.Int + if st.GasFeeCap != nil { + feeCap = st.GasFeeCap + } else { + feeCap = common.Big0 + } + // Recap the highest gas limit with account's available balance. + if feeCap.BitLen() != 0 { + var payment common.Address + if st.Paymaster == nil { + payment = *st.Sender + } else { + payment = *st.Paymaster + } + balance := opts.State.GetBalance(payment).ToBig() + + allowance := new(big.Int).Div(balance, feeCap) + + // If the allowance is larger than maximum uint64, skip checking + if allowance.IsUint64() && hi > allowance.Uint64() { + log.Debug("Gas estimation capped by limited funds", "original", hi, "balance", balance, + "maxFeePerGas", feeCap, "fundable", allowance) + hi = allowance.Uint64() + } + } + // Recap the highest gas allowance with specified gascap. + if gasCap != 0 && hi > gasCap { + log.Debug("Caller gas above allowance, capping", "requested", hi, "cap", gasCap) + hi = gasCap + } + + // We first execute the transaction at the highest allowable gas limit, since if this fails we + // can return error immediately. + failed, exr, ppr, err := executeRip7560Execution(ctx, tx, opts, hi) + if err != nil { + return 0, nil, err + } + if failed { + if exr != nil && ppr != nil { + if !errors.Is(exr.Err, vm.ErrOutOfGas) { + return 0, exr.Revert(), exr.Err + } else if !errors.Is(ppr.Err, vm.ErrOutOfGas) { + return 0, ppr.Revert(), ppr.Err + } + } + return 0, nil, fmt.Errorf("gas required exceeds allowance (%d)", hi) + } + // For almost any transaction, the gas consumed by the unconstrained execution + // above lower-bounds the gas limit required for it to succeed. One exception + // is those that explicitly check gas remaining in order to execute within a + // given limit, but we probably don't want to return the lowest possible gas + // limit for these cases anyway. + if ppr == nil { + lo = exr.UsedGas - 1 + } else { + lo = exr.UsedGas + ppr.UsedGas - 1 + } + + // There's a fairly high chance for the transaction to execute successfully + // with gasLimit set to the first execution's usedGas + gasRefund. Explicitly + // check that gas amount and use as a limit for the binary search. + var optimisticGasLimit uint64 + if ppr == nil { + optimisticGasLimit = (exr.UsedGas + exr.RefundedGas + params.CallStipend) * 64 / 63 + } else { + optimisticGasLimit = (exr.UsedGas + exr.RefundedGas + ppr.UsedGas + ppr.RefundedGas + params.CallStipend) * 64 / 63 + } + if optimisticGasLimit < hi { + failed, _, _, err = executeRip7560Execution(ctx, tx, opts, optimisticGasLimit) + if err != nil { + // This should not happen under normal conditions since if we make it this far the + // transaction had run without error at least once before. + log.Error("Execution error in estimate gas", "err", err) + return 0, nil, err + } + if failed { + lo = optimisticGasLimit + } else { + hi = optimisticGasLimit + } + } + // Binary search for the smallest gas limit that allows the tx to execute successfully. + for lo+1 < hi { + if opts.ErrorRatio > 0 { + // It is a bit pointless to return a perfect estimation, as changing + // network conditions require the caller to bump it up anyway. Since + // wallets tend to use 20-25% bump, allowing a small approximation + // error is fine (as long as it's upwards). + if float64(hi-lo)/float64(hi) < opts.ErrorRatio { + break + } + } + mid := (hi + lo) / 2 + if mid > lo*2 { + // Most txs don't need much higher gas limit than their gas used, and most txs don't + // require near the full block limit of gas, so the selection of where to bisect the + // range here is skewed to favor the low side. + mid = lo * 2 + } + failed, _, _, err = executeRip7560Execution(ctx, tx, opts, mid) + if err != nil { + // This should not happen under normal conditions since if we make it this far the + // transaction had run without error at least once before. + log.Error("Execution error in estimate gas", "err", err) + return 0, nil, err + } + if failed { + lo = mid + } else { + hi = mid + } + } + return hi, nil, nil +} diff --git a/internal/ethapi/rip7560api.go b/internal/ethapi/rip7560api.go index 5adb14ab4d..f1a8ec499e 100644 --- a/internal/ethapi/rip7560api.go +++ b/internal/ethapi/rip7560api.go @@ -3,13 +3,29 @@ package ethapi import ( "context" "errors" + "fmt" "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/core" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/gasestimator" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" + "github.com/holiman/uint256" "golang.org/x/crypto/sha3" "math/big" + "time" ) +type Rip7560UsedGas struct { + ValidationGas hexutil.Uint64 `json:"verificationGasLimit"` + ExecutionGas hexutil.Uint64 `json:"callGasLimit"` +} + func (s *TransactionAPI) SendRip7560TransactionsBundle(ctx context.Context, args []TransactionArgs, creationBlock *big.Int, bundlerId string) (common.Hash, error) { if len(args) == 0 { return common.Hash{}, errors.New("submitted bundle has zero length") @@ -41,6 +57,189 @@ func (s *TransactionAPI) GetRip7560TransactionDebugInfo(hash common.Hash) (map[s return s.b.GetRip7560TransactionDebugInfo(hash) } +func (s *TransactionAPI) CallRip7560Validation(ctx context.Context, args TransactionArgs, blockNrOrHash *rpc.BlockNumberOrHash, overrides *StateOverride, blockOverrides *BlockOverrides) (*core.ValidationPhaseResult, error) { + if blockNrOrHash == nil { + latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) + blockNrOrHash = &latest + } + + // TODO(sm-stack): Configure RIP-7560 enabled devnet option + //header, err := headerByNumberOrHash(ctx, s.b, *blockNrOrHash) + //if err != nil { + // return nil, err + //} + + //if s.b.ChainConfig().IsRIP7560(header.Number) { + // return nil, fmt.Errorf("cannot call RIP-7560 validation on pre-rip7560 block %v", header.Number) + //} + + result, err := DoCallRip7560Validation(ctx, s.b, args, *blockNrOrHash, overrides, blockOverrides, s.b.RPCEVMTimeout(), s.b.RPCGasCap()) + if err != nil { + return nil, err + } + // just return the result and err + return result, nil +} + +func doCallRip7560Validation(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *types.Header, overrides *StateOverride, blockOverrides *BlockOverrides, timeout time.Duration, globalGasCap uint64) (*core.ValidationPhaseResult, error) { + if err := overrides.Apply(state); err != nil { + return nil, err + } + // 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() + + // Get a new instance of the EVM. + tx := args.ToTransaction() + + chainConfig := b.ChainConfig() + bc := NewChainContext(ctx, b) + blockContext := core.NewEVMBlockContext(header, bc, &header.Coinbase, chainConfig, state) + if blockOverrides != nil { + blockOverrides.Apply(&blockContext) + } + txContext := vm.TxContext{ + Origin: *tx.Rip7560TransactionData().Sender, + GasPrice: tx.GasPrice(), + } + evm := vm.NewEVM(blockContext, txContext, state, chainConfig, vm.Config{NoBaseFee: true}) + + // Wait for the context to be done and cancel the evm. Even if the + // EVM has finished, cancelling may be done (repeatedly) + go func() { + <-ctx.Done() + evm.Cancel() + }() + + gasPrice := new(big.Int).Add(header.BaseFee, tx.GasTipCap()) + if gasPrice.Cmp(tx.GasFeeCap()) > 0 { + gasPrice = tx.GasFeeCap() + } + gasPriceUint256, _ := uint256.FromBig(gasPrice) + + // Execute the validation phase. + gp := new(core.GasPool).AddGas(math.MaxUint64) + aatx := tx.Rip7560TransactionData() + rollupCost, err := core.CalculateRollupCost(chainConfig, header, tx, state) + if err != nil { + return nil, err + } + _, _, err = core.BuyGasRip7560Transaction(aatx, state, gasPriceUint256, gp, rollupCost) + if err != nil { + return nil, err + } + + result, err := core.ApplyRip7560ValidationPhases(chainConfig, bc, &header.Coinbase, gp, state, header, tx, evm.Config) + if err := state.Error(); err != nil { + return nil, err + } + + // If the timer caused an abort, return an appropriate error message + if evm.Cancelled() { + return nil, fmt.Errorf("validation aborted (timeout = %v)", timeout) + } + if err != nil { + return result, fmt.Errorf("err: %w (supplied gas %d)", err, tx.Rip7560TransactionData().ValidationGasLimit) + } + return result, nil +} + +func DoCallRip7560Validation(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, blockOverrides *BlockOverrides, timeout time.Duration, globalGasCap uint64) (*core.ValidationPhaseResult, error) { + defer func(start time.Time) { + log.Debug("Executing RIP-7560 validation finished", "runtime", time.Since(start)) + }(time.Now()) + + state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + if state == nil || err != nil { + return nil, err + } + + return doCallRip7560Validation(ctx, b, args, state, header, overrides, blockOverrides, timeout, globalGasCap) +} + +func DoEstimateRip7560TransactionGas(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, gasCap uint64) (*Rip7560UsedGas, error) { + state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + if state == nil || err != nil { + return nil, err + } + if err = overrides.Apply(state); err != nil { + return nil, err + } + // Construct the gas estimator option from the user input + chainConfig := b.ChainConfig() + bc := NewChainContext(ctx, b) + if err := args.Call7560Defaults(gasCap, header.BaseFee, chainConfig.ChainID); err != nil { + return nil, err + } + tx := args.ToTransaction() + + gasPrice := new(big.Int).Add(header.BaseFee, tx.GasTipCap()) + if gasPrice.Cmp(tx.GasFeeCap()) > 0 { + gasPrice = tx.GasFeeCap() + } + gasPriceUint256, _ := uint256.FromBig(gasPrice) + + // Execute the validation phase. + gp := new(core.GasPool).AddGas(math.MaxUint64) + aatx := tx.Rip7560TransactionData() + rollupCost, err := core.CalculateRollupCost(chainConfig, header, tx, state) + if err != nil { + return nil, err + } + _, _, err = core.BuyGasRip7560Transaction(aatx, state, gasPriceUint256, gp, rollupCost) + if err != nil { + return nil, err + } + opts := &gasestimator.Options{ + Config: chainConfig, + Chain: bc, + Header: header, + State: state, + ErrorRatio: estimateGasErrorRatio, + } + + vg, err := gasestimator.EstimateRip7560Validation(ctx, tx, opts, gasCap) + if err != nil { + return nil, err + } + + eg, _, err := gasestimator.EstimateRip7560Execution(ctx, opts, gasCap) + if err != nil { + return nil, err + } + + return &Rip7560UsedGas{ + ValidationGas: hexutil.Uint64(vg), + ExecutionGas: hexutil.Uint64(eg), + }, nil +} + +func (s *BlockChainAPI) EstimateRip7560TransactionGas(ctx context.Context, args TransactionArgs, blockNrOrHash *rpc.BlockNumberOrHash, overrides *StateOverride) (*Rip7560UsedGas, error) { + bNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) + if blockNrOrHash != nil { + bNrOrHash = *blockNrOrHash + } + + header, err := headerByNumberOrHash(ctx, s.b, bNrOrHash) + if err != nil { + return nil, err + } + + if !s.b.ChainConfig().IsRIP7560(header.Number) { + return nil, fmt.Errorf("cannot estimate gas for RIP-7560 tx on pre-bedrock block %v", header.Number) + } + + return DoEstimateRip7560TransactionGas(ctx, s.b, args, bNrOrHash, overrides, s.b.RPCGasCap()) +} + // CalculateBundleHash // TODO: If this code is indeed necessary, keep it in utils; better - remove altogether. func CalculateBundleHash(txs []*types.Transaction) common.Hash { diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go index 8ea5ebe634..3a19decb06 100644 --- a/internal/ethapi/transaction_args.go +++ b/internal/ethapi/transaction_args.go @@ -455,6 +455,44 @@ func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, return nil } +func (args *TransactionArgs) Call7560Defaults(globalGasCap uint64, baseFee *big.Int, chainID *big.Int) error { + if args.Sender == nil { + return errors.New(`missing "Sender" in transaction`) + } + if args.BuilderFee == nil { + args.BuilderFee = new(hexutil.Big) + } + if args.Paymaster == nil { + args.Paymaster = &common.Address{} + args.PaymasterData = &hexutil.Bytes{} + args.PaymasterGas = new(hexutil.Uint64) + args.PostOpGas = new(hexutil.Uint64) + } + if args.Deployer == nil { + args.Deployer = &common.Address{} + args.DeployerData = &hexutil.Bytes{} + } + if args.NonceKey == nil { + args.NonceKey = new(hexutil.Big) + } + if args.ValidationGas == nil || *args.ValidationGas == hexutil.Uint64(0) { + gas := globalGasCap + if gas == 0 { + gas = uint64(math.MaxUint64 / 2) + } + args.ValidationGas = (*hexutil.Uint64)(&gas) + } else { + if globalGasCap > 0 && globalGasCap < uint64(*args.ValidationGas) { + log.Warn("Caller ValidationGas above allowance, capping", "requested", args.Gas, "cap", globalGasCap) + args.ValidationGas = (*hexutil.Uint64)(&globalGasCap) + } + } + if err := args.CallDefaults(globalGasCap, baseFee, chainID); err != nil { + return err + } + return nil +} + // ToMessage converts the transaction arguments to the Message type used by the // core evm. This method is used in calls and traces that do not require a real // live transaction. @@ -512,6 +550,13 @@ func toUint64(b *hexutil.Uint64) uint64 { return uint64(*b) } +func toByte(b *hexutil.Bytes) []byte { + if b == nil { + return []byte{} + } + return *b +} + // ToTransaction converts the arguments to a transaction. // This assumes that setDefaults has been called. func (args *TransactionArgs) ToTransaction() *types.Transaction { @@ -525,7 +570,7 @@ func (args *TransactionArgs) ToTransaction() *types.Transaction { aatx := types.Rip7560AccountAbstractionTx{ //To: &common.Address{}, ChainID: (*big.Int)(args.ChainID), - Gas: uint64(*args.Gas), + Gas: toUint64(args.Gas), NonceKey: (*big.Int)(args.NonceKey), Nonce: uint64(*args.Nonce), GasFeeCap: (*big.Int)(args.MaxFeePerGas), @@ -537,9 +582,9 @@ func (args *TransactionArgs) ToTransaction() *types.Transaction { Sender: args.Sender, AuthorizationData: *args.AuthorizationData, Paymaster: args.Paymaster, - PaymasterData: *args.PaymasterData, + PaymasterData: toByte(args.PaymasterData), Deployer: args.Deployer, - DeployerData: *args.DeployerData, + DeployerData: toByte(args.DeployerData), BuilderFee: (*big.Int)(args.BuilderFee), ValidationGasLimit: toUint64(args.ValidationGas), PaymasterValidationGasLimit: toUint64(args.PaymasterGas),