diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml deleted file mode 100644 index f31ba944f8..0000000000 --- a/.github/workflows/changelog-check.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: "Check changelog update" -on: - pull_request: - # The specific activity types are listed here to include "labeled" and "unlabeled" - # (which are not included by default for the "pull_request" trigger). - # This is needed to allow skipping enforcement of the changelog in PRs with specific labels, - # as defined in the (optional) "skipLabels" property. - types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] - -jobs: - # This implementation handles CHANGELOG.md checks for a monorepo, ensuring - # that changelogs throughout the repo are edited. The advantage of this - # implemenation is that packages or projecs with independent changelogs will - # can still have automation without requiring edits to the root changelog. - # - # IF a Go-related file (filter "nibiru-go") is changed, the - # "dangoslen/changelog-enforcer" action runs, enforcing changes to the - # CHANGELOG.md file at the root of the repo. - # - # ELSE IF no changelog was modified anywhere else, then - # "dangoslen/changelog-enforcer" runs. - # - # ELSE, some changelog file must have been modified that was not the root - # one, so the workflow can skip "dangoslen/changelog-enforcer". - changelog: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: "Check for changelog and Go edits" - id: check_changelog - uses: dorny/paths-filter@v3 - with: - filters: | - changelog: - - "**/CHANGELOG.md" - nibiru-go: - - "app/**/*.go" - - "cmd/**/*.go" - - "eth/**/*.go" - - "gosdk/**/*.go" - - "x/**/*.go" - - "**/*.proto" - - "go.mod" - - "go.sum" - - "contrib/docker-compose/*" - - - uses: dangoslen/changelog-enforcer@v3 - if: steps.check_changelog.outputs.nibiru-go == 'true' || steps.check_changelog.outputs.changelog == 'false' - with: - skipLabels: "skip-changelog" diff --git a/CHANGELOG-UNRELEASED.md b/CHANGELOG-UNRELEASED.md new file mode 100644 index 0000000000..c340b6b1e5 --- /dev/null +++ b/CHANGELOG-UNRELEASED.md @@ -0,0 +1,32 @@ +## Changelog Chunk +* fix: bug with ineffectual assignment +* fix: add suggestion from Cursor bot +* fix(evm-e2e): newer assertion is more lenient +* fix(evm): use geth efficiency improvements and improved BinSearch with lo bias +* fix(evm): add panic safety to eth_estimateGas. Fix usage of incorrect SDB context in successive estimation calls. +* docs(bank): README fixes +* docs(bank): improve documentation and add section on Nibiru changes in [#2421](https://github.com/NibiruChain/nibiru/pull/2421) - ([172c008](https://github.com/NibiruChain/nibiru/commit/172c008068f8436081ee571f2fb7c01f607297f3)) by @Unique-Divine +* feat: add recursive check for nested authz exec messages and enforce … in [#2420](https://github.com/NibiruChain/nibiru/pull/2420) - ([3cdc810](https://github.com/NibiruChain/nibiru/commit/3cdc8107069c149bbfc538a6e3b7b0dd17b9cf2e)) by @expertdicer +* ci: simplify Go caching in CI to prevent file collisions in [#2419](https://github.com/NibiruChain/nibiru/pull/2419) - ([b1d1c22](https://github.com/NibiruChain/nibiru/commit/b1d1c22319eed8af11152b460fca6d9f54ad067a)) by @Unique-Divine +* fix(evmstate/test): stabilize trace tx tests with deterministic ERC20 transfer recipient in [#2418](https://github.com/NibiruChain/nibiru/pull/2418) - ([b47e3bd](https://github.com/NibiruChain/nibiru/commit/b47e3bd8ef89e01134fe3e3e0d60bea3e34c9561)) by @Unique-Divine +* feat: custom ante NewDeductFeeDecorator allowing 0 fee for zero gas actors in [#2415](https://github.com/NibiruChain/nibiru/pull/2415) - ([09e58ab](https://github.com/NibiruChain/nibiru/commit/09e58abd5a636b18303434aa1d5d0a526a7e65cb)) by @onikonychev +* ci: add back coverage reporting using gocovmerge; bring README more up to date in [#2416](https://github.com/NibiruChain/nibiru/pull/2416) - ([63257f0](https://github.com/NibiruChain/nibiru/commit/63257f0c749543478e8ce7be4aebec899d906f2e)) by @Unique-Divine +* refactor(upgrades): simplify upgrade hanlder code to use less abstractions and combine micro-packages in [#2413](https://github.com/NibiruChain/nibiru/pull/2413) - ([74cb33e](https://github.com/NibiruChain/nibiru/commit/74cb33eee6854cf55be26b26f5a85e6542625071)) by @Unique-Divine +* fix(evm-rpc): remove unsafe debug API methods. in [#2412](https://github.com/NibiruChain/nibiru/pull/2412) - ([e2a1ee8](https://github.com/NibiruChain/nibiru/commit/e2a1ee84e651dff9202ba25b0b7b54b64036697a)) by @Unique-Divine +* chore: v2.8.0 upgrade handler in [#2411](https://github.com/NibiruChain/nibiru/pull/2411) - ([9000cf3](https://github.com/NibiruChain/nibiru/commit/9000cf3a16797670769643e4feae4f9d07f38518)) by @onikonychev +* fix(evm-trace-block): handle native tracer errors JSON-RPC errors for "debug_traceBlockByNumber". Fixes [Nibiru#2400 bug](https://github.com/NibiruChain/nibiru/issues/2400) in [#2409](https://github.com/NibiruChain/nibiru/pull/2409) - ([e44cbc5](https://github.com/NibiruChain/nibiru/commit/e44cbc5c4e2f8be3a6756baf6aa5866ef642e63a)) by @Unique-Divine +* docs: merge PR from @yinwenyu6 . Comments only +* feat(evm/grpc-query): Update the "/eth.evm.v1.Query/Balance" query to work with "0x" Ethereum hex and "nibi"-prefixed Bech32 address formats in [#2410](https://github.com/NibiruChain/nibiru/pull/2410) - ([1bfc24d](https://github.com/NibiruChain/nibiru/commit/1bfc24d35401fccf120d1e82e1d184b57ca989d4)) by @Unique-Divine +* feat(proto): REST API doc generation for bank, auth, and txs in [#2394](https://github.com/NibiruChain/nibiru/pull/2394) - ([5dbb398](https://github.com/NibiruChain/nibiru/commit/5dbb3984e86a87c4d91c984c4fcd96d36cde25f5)) by @expertdicer +* feat(sudo-ante): implement zero gas actors for invoking whitelisted contract in [#2407](https://github.com/NibiruChain/nibiru/pull/2407) - ([6316bcd](https://github.com/NibiruChain/nibiru/commit/6316bcd66593dcf2dd370e7a1f0daf5d4fef3676)) by @Unique-Divine +* chore: added monad logo svg in [#2406](https://github.com/NibiruChain/nibiru/pull/2406) - ([90b951c](https://github.com/NibiruChain/nibiru/commit/90b951c43f7ad0c793d6b84bd6d087fc767bb190)) by @onikonychev +* chore: additional coin logos which could be used externally in [#2405](https://github.com/NibiruChain/nibiru/pull/2405) - ([6bb649b](https://github.com/NibiruChain/nibiru/commit/6bb649b7c3d3f73a985914e55772f04bbbbc07c6)) by @onikonychev +* feat(epic-evm): rearchitecture for StateDB safety, fix for consensus failures, performance improvements, consistent simulations, and nonce resolution for pending txs in the mempool in [#2397](https://github.com/NibiruChain/nibiru/pull/2397) - ([a252c9b](https://github.com/NibiruChain/nibiru/commit/a252c9bcdf37faa99e60fc59088e2867bd6c461f)) by @Unique-Divine +* chore: erc20 token registry new token: ynETHx in [#2395](https://github.com/NibiruChain/nibiru/pull/2395) - ([9219a94](https://github.com/NibiruChain/nibiru/commit/9219a94448940c1835106cb3d82dc61f50948234)) by @onikonychev +* feat(proto): impl script for gRPC Gateway REST doc generation in [#2391](https://github.com/NibiruChain/nibiru/pull/2391) - ([eb4b67e](https://github.com/NibiruChain/nibiru/commit/eb4b67ea858f889fb996cf2248c225d03a090b17)) by @Unique-Divine +* chore: erc20 token registry new tokens: cbBTC, uBTC in [#2388](https://github.com/NibiruChain/nibiru/pull/2388) - ([c1229d0](https://github.com/NibiruChain/nibiru/commit/c1229d0547ef743c156295218896acdeaa89c008)) by @onikonychev +* feat(evm): 63/64 gas clamp for ERC20 calls. Improved VM error surfacing. Add composite Chainlink-like oracle in [#2385](https://github.com/NibiruChain/nibiru/pull/2385) - ([2f7dbb5](https://github.com/NibiruChain/nibiru/commit/2f7dbb5060e35dab4128a203b63cae3ed1821105)) by @Unique-Divine +* feat(.github/pr-title-lint): Enable "/", capital letters, and "evm" prefix in pull request titles in [#2387](https://github.com/NibiruChain/nibiru/pull/2387) - ([4d1e13d](https://github.com/NibiruChain/nibiru/commit/4d1e13d2fbae61b1a7f3a6dd18515872c125f8df)) by @Unique-Divine +* feat(ai): start .cursorignore and Gemini code reviews in [#2386](https://github.com/NibiruChain/nibiru/pull/2386) - ([412abe7](https://github.com/NibiruChain/nibiru/commit/412abe7ceb0acf6e68211bf494106d2d8b498aab)) by @Unique-Divine + + diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000000..f719eeb2d7 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,72 @@ +# git-cliff ~ configuration file +# https://git-cliff.org/docs/configuration + +[remote.github] +owner = "NibiruChain" +repo = "nibiru" +# token = "" + +[changelog] +# A Tera template to be rendered for each release in the changelog. +# See https://keats.github.io/tera/docs/#introduction +body = """ +## Changelog Chunk {%- if version %}- {{ version }} {%- endif -%} + +{%- if version %} in {{ version | trim_start_matches(pat="v") }}{%- endif -%} +{% for commit in commits %} + {% if commit.remote.pr_title -%} + {%- set commit_message = commit.remote.pr_title -%} + {%- else -%} + {%- set commit_message = commit.message -%} + {%- endif -%} + * {{ commit_message | split(pat="\n") | first | trim }}\ + + {% if commit.remote.pr_number %} in \ + [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \ + - ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }})) \ + {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%} + {%- endif %} +{%- endfor -%} + +{% if version %} + {% if previous.version %} + **Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }} + {% endif %} +{% else -%} + {% raw %}\n{% endraw %} +{% endif %} + +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} +""" +# Remove leading and trailing whitespaces from the changelog's body. +trim = true +# A Tera template to be rendered as the changelog's footer. +# See https://keats.github.io/tera/docs/#introduction +footer = """ + +""" +# An array of regex based postprocessors to modify the changelog. +# Replace the placeholder `` with a URL. +postprocessors = [] + +[git] +# Parse commits according to the conventional commits specification. +# See https://www.conventionalcommits.org +conventional_commits = false +# Exclude commits that do not match the conventional commits specification. +filter_unconventional = true +# Split commits on newlines, treating each line as an individual commit. +split_commits = false +# An array of regex based parsers to modify commit messages prior to further processing. +commit_preprocessors = [{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }] +# Exclude commits that are not matched by any commit parser. +filter_commits = false +# Order releases topologically instead of chronologically. +topo_order = false +# Order of commits in each group/release within the changelog. +# Allowed values: newest, oldest +sort_commits = "newest" +# tag_pattern: Chooses which tags create their own sections. +tag_pattern = '^v\\d+\\.\\d+\\.\\d+$' # only vX.Y.Z count as releases diff --git a/evm-e2e/test/eth_queries.test.ts b/evm-e2e/test/eth_queries.test.ts index 80197e7176..5a61a74284 100644 --- a/evm-e2e/test/eth_queries.test.ts +++ b/evm-e2e/test/eth_queries.test.ts @@ -28,7 +28,7 @@ describe("eth queries", () => { } const estimatedGas = await provider.estimateGas(tx) expect(estimatedGas).toBeGreaterThan(BigInt(0)) - expect(estimatedGas).toEqual(INTRINSIC_TX_GAS) + expect(estimatedGas - INTRINSIC_TX_GAS).toBeLessThan(INTRINSIC_TX_GAS / BigInt(20)) }) it("eth_feeHistory", async () => { diff --git a/justfile b/justfile index a9313e1ac6..04f59be7ad 100644 --- a/justfile +++ b/justfile @@ -46,6 +46,26 @@ gen-embeds: go run "gen-abi/main.go" log_success "Saved ABI JSON files to $embeds_dir/abi for npm publishing" +# Generates CHANGELOG-UNRELEASED.md based on commits and pull requests. +gen-changelog: + #!/usr/bin/env bash + source contrib/bashlib.sh + which_ok cargo + if ! which_ok git-cliff; then + echo "Installing git-cliff with cargo" + cargo install git-cliff + fi + + which_ok git-cliff + + LAST_VER="v2.7.0" + start_branch="$(git branch --show-current)" + git checkout main + git-cliff "$LAST_VER.." -o CHANGELOG-UNRELEASED.md + git checkout "$start_branch" + log_success "Created CHANGELOG-UNRELEASED.md with changes since $LAST_VER" + git add CHANGELOG-UNRELEASED.md + # Generate the Nibiru Token Registry files gen-token-registry: go run token-registry/main/main.go diff --git a/x/evm/const.go b/x/evm/const.go index fa7ab15745..047b5590b7 100644 --- a/x/evm/const.go +++ b/x/evm/const.go @@ -36,6 +36,13 @@ const ( Erc20GasLimitExecute uint64 = 200_000 ) +type contextKey string + +const ( + CtxKeyEvmSimulation contextKey = "evm_simulation" + CtxKeyGasEstimateZeroTolerance contextKey = "gas_estimate_zero_tolerance" +) + // BASE_FEE_MICRONIBI is the global base fee value for the network. It has a // constant value of 1 unibi (micronibi) == 10^12 wei. var ( @@ -123,15 +130,6 @@ const ( updateParamsName = "evm/MsgUpdateParams" ) -type CallType int - -const ( - // CallTypeRPC call type is used on requests to eth_estimateGas rpc API endpoint - CallTypeRPC CallType = iota + 1 - // CallTypeSmart call type is used in case of smart contract methods calls - CallTypeSmart -) - var ( EVM_MODULE_ADDRESS gethcommon.Address EVM_MODULE_ADDRESS_NIBI sdk.AccAddress diff --git a/x/evm/evm.go b/x/evm/evm.go index de765bbb08..8c00154495 100644 --- a/x/evm/evm.go +++ b/x/evm/evm.go @@ -3,13 +3,11 @@ package evm import ( "fmt" - "strings" "github.com/cometbft/cometbft/crypto/tmhash" sdk "github.com/cosmos/cosmos-sdk/types" bank "github.com/cosmos/cosmos-sdk/x/bank/types" gethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/vm" "github.com/NibiruChain/nibiru/v2/eth" ) @@ -114,38 +112,6 @@ func ValidateFunTokenBankMetadata( return out, nil } -// HandleOutOfGasPanic captures an sdk.ErrorOutOfGas panic and folds it into -// *errp, an error pointer. -// - If *errp is nil: sets *errp = vm.ErrOutOfGas -// - If *errp is non-nil: preserves it (do not overwrite) -// - Always applies `format` wrapping if *errp is non-nil after recovery -// - Re-panics for any non-OutOfGas panic -func HandleOutOfGasPanic(errp *error, format string) func() { - return func() { - if perr := recover(); perr != nil { - _, isOutOfGasPanic := perr.(sdk.ErrorOutOfGas) - switch { - case isOutOfGasPanic: - if errp != nil && *errp == nil { - *errp = vm.ErrOutOfGas - } - // else: preserve existing detailed error - case strings.Contains(fmt.Sprint(perr), vm.ErrOutOfGas.Error()): - if errp == nil { - errp = new(error) - } - *errp = fmt.Errorf("%s: %w", perr, vm.ErrOutOfGas) - default: - // Non-OOG panics are not handled here - panic(perr) - } - } - if errp != nil && *errp != nil && format != "" { - *errp = fmt.Errorf("%s: %w", format, *errp) - } - } -} - // Gracefully handles "out of gas" func SafeConsumeGas(ctx sdk.Context, amount uint64, descriptor string) (err error) { defer func() { diff --git a/x/evm/evmstate/grpc_query.go b/x/evm/evmstate/grpc_query.go index 60cc6bd4bd..a84383967f 100644 --- a/x/evm/evmstate/grpc_query.go +++ b/x/evm/evmstate/grpc_query.go @@ -264,7 +264,7 @@ func (k *Keeper) EthCall( } ctx := sdk.UnwrapSDKContext(goCtx) - ctx = ctx.WithValue(SimulationContextKey, true) + ctx = ctx.WithValue(evm.CtxKeyEvmSimulation, true) var args evm.JsonTxArgs err := json.Unmarshal(req.Args, &args) @@ -295,15 +295,9 @@ func (k *Keeper) EthCall( } // EstimateGas: Implements the gRPC query for "/eth.evm.v1.Query/EstimateGas". -// EstimateGas implements eth_estimateGas rpc api. -func (k Keeper) EstimateGas( - goCtx context.Context, req *evm.EthCallRequest, -) (*evm.EstimateGasResponse, error) { - return k.EstimateGasForEvmCallType(goCtx, req, evm.CallTypeRPC) -} - -// EstimateGasForEvmCallType estimates the gas cost of a transaction. This can be -// called with the "eth_estimateGas" JSON-RPC method or smart contract query. +// This estimates the lowest possible gas limit that allows a transaction to run +// successfully with the provided context options. This can be called with the +// "eth_estimateGas" JSON-RPC method. // // When [EstimateGas] is called from the JSON-RPC client, we need to reset the // gas meter before simulating the transaction (tx) to have an accurate gas @@ -316,16 +310,16 @@ func (k Keeper) EstimateGas( // Returns: // - A response containing the estimated gas cost. // - An error if the gas estimation process encounters any issues. -func (k Keeper) EstimateGasForEvmCallType( - goCtx context.Context, req *evm.EthCallRequest, fromType evm.CallType, +func (k Keeper) EstimateGas( + goCtx context.Context, req *evm.EthCallRequest, ) (*evm.EstimateGasResponse, error) { if err := req.Validate(); err != nil { return nil, err } - ctx := sdk.UnwrapSDKContext(goCtx) - ctx = ctx.WithValue(SimulationContextKey, true) - evmCfg := k.GetEVMConfig(ctx) + rootCtx := sdk.UnwrapSDKContext(goCtx). + WithValue(evm.CtxKeyEvmSimulation, true) + evmCfg := k.GetEVMConfig(rootCtx) if req.GasCap < gethparams.TxGas { return nil, grpcstatus.Errorf(grpccodes.InvalidArgument, "gas cap cannot be lower than %d", gethparams.TxGas) @@ -338,54 +332,90 @@ func (k Keeper) EstimateGasForEvmCallType( } // ApplyMessageWithConfig expect correct nonce set in msg - nonce := k.GetAccNonce(ctx, args.GetFrom()) + nonce := k.GetAccNonce(rootCtx, args.GetFrom()) args.Nonce = (*hexutil.Uint64)(&nonce) // Binary search the gas requirement, as it may be higher than the amount used var ( - lo = gethparams.TxGas - 1 - hi uint64 - gasCap uint64 + // Set smart lower bound based on the gas used in the first execution + // (base case). + lo uint64 + hi uint64 + + // executable runs one probe at a specific gas limit. + // - Rewrites evmMsg.GasLimit to the probed value. + // - Constructs a fresh SDB on a context with an infinite gas meter and zero + // KV/transient KV gas costs, isolating the probe from store-gas panics. + // - Defers a panic classifier, where SDK/go-ethereum "out of gas" + // panics result in { vmError=true, err=nil }. Any other panic gets + // bubbled up through the call stack. + // - Returns (vmError, resp, err) where vmError signals VM-level failure + // (incl. OOG/revert), and err signals consensus/unexpected failure. + executable func(gas uint64) (vmError bool, rsp *evm.MsgEthereumTxResponse, err error) ) // Determine the highest gas limit can be used during the estimation. + // Start with block gas limit + params := rootCtx.ConsensusParams() + if params != nil && params.Block != nil && params.Block.MaxGas > 0 { + hi = uint64(params.Block.MaxGas) + } else { + // Fallback to gasCap if block params not available + hi = req.GasCap + } + + // Override with user-provided gas limit if it's valid if args.Gas != nil && uint64(*args.Gas) >= gethparams.TxGas { hi = uint64(*args.Gas) - } else { - // Query block gas limit - params := ctx.ConsensusParams() - if params != nil && params.Block != nil && params.Block.MaxGas > 0 { - hi = uint64(params.Block.MaxGas) - } else { - hi = req.GasCap - } } - // TODO: Recap the highest gas limit with account's available balance. // Recap the highest gas allowance with specified gascap. if req.GasCap != 0 && hi > req.GasCap { hi = req.GasCap } - gasCap = hi - // convert the tx args to an ethereum message evmMsg, err := args.ToMessage(req.GasCap, evmCfg.BaseFeeWei) if err != nil { return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) } - // NOTE: the errors from the executable below should be consistent with - // go-ethereum, so we don't wrap them with the gRPC status code Create a - // helper to check if a gas allowance results in an executable transaction. - executable := func(gas uint64) (vmError bool, rsp *evm.MsgEthereumTxResponse, err error) { - // update the message with the new gas value - evmMsg = core.Message{ + executable = func(gas uint64) (vmError bool, rsp *evm.MsgEthereumTxResponse, err error) { + defer func() { + // Recover OOG panics as a normal VM failure so the binary search can + // increase gas. Any non-OOG panic aborts the search with a + // contextual error for diagnostics. + var ( + oog bool + perr error + ) + + if panicInfo := recover(); panicInfo != nil { + if _, isOutOfGasPanic := panicInfo.(sdk.ErrorOutOfGas); isOutOfGasPanic { + oog, perr = true, vm.ErrOutOfGas + } else if strings.Contains(fmt.Sprint(panicInfo), "out of gas") { + oog, perr = true, vm.ErrOutOfGas + } else { + // Non-OOG panics are not handled here + oog, perr = false, fmt.Errorf( + `unexpected panic in eth_estimateGas { gas: %d }: %v`, gas, panicInfo) + } + } + + if oog { + vmError, rsp, err = true, nil, nil + return + } else if perr != nil { + err = perr // Unexpected panic -> Abort the search + return + } + }() + evmMsg = core.Message{ // update the message with the new gas value To: evmMsg.To, From: evmMsg.From, Nonce: evmMsg.Nonce, Value: evmMsg.Value, - GasLimit: gas, // <---- This one changed + GasLimit: gas, // <---- This one changes GasPrice: evmMsg.GasPrice, GasFeeCap: evmMsg.GasFeeCap, GasTipCap: evmMsg.GasTipCap, @@ -397,37 +427,35 @@ func (k Keeper) EstimateGasForEvmCallType( SkipFromEOACheck: evmMsg.SkipFromEOACheck, } - tmpCtx := ctx - if fromType == evm.CallTypeRPC { - tmpCtx, _ = ctx.CacheContext() + // Initialize SDB + sdb := k.NewSDB( + rootCtx, + k.TxConfig(rootCtx, rootCtx.EvmTxHash()), + ) + sdb.SetCtx( + sdb.Ctx(). + WithGasMeter(eth.NewInfiniteGasMeterWithLimit(evmMsg.GasLimit)). + WithKVGasConfig(storetypes.GasConfig{}). + WithTransientKVGasConfig(storetypes.GasConfig{}), + ) - acct := k.GetAccount(tmpCtx, evmMsg.From) + acct := k.GetAccount(sdb.Ctx(), evmMsg.From) - from := evmMsg.From - if acct == nil { - acc := k.accountKeeper.NewAccountWithAddress(tmpCtx, from[:]) - k.accountKeeper.SetAccount(tmpCtx, acc) - acct = NewEmptyAccount() - } - // When submitting a transaction, the `EthIncrementSenderSequence` ante handler increases the account nonce - acct.Nonce = nonce + 1 - err = k.SetAccount(tmpCtx, from, *acct) - if err != nil { - return true, nil, err - } - // resetting the gasMeter after increasing the sequence to have an accurate gas estimation on EVM extensions transactions - gasMeter := eth.NewInfiniteGasMeterWithLimit(evmMsg.GasLimit) - tmpCtx = tmpCtx.WithGasMeter(gasMeter). - WithKVGasConfig(storetypes.GasConfig{}). - WithTransientKVGasConfig(storetypes.GasConfig{}) + from := evmMsg.From + if acct == nil { + acc := k.accountKeeper.NewAccountWithAddress(sdb.Ctx(), from[:]) + k.accountKeeper.SetAccount(sdb.Ctx(), acc) + acct = NewEmptyAccount() + } + // When submitting a transaction, the `EthIncrementSenderSequence` ante handler increases the account nonce + acct.Nonce = nonce + 1 + err = k.SetAccount(sdb.Ctx(), from, *acct) + if err != nil { + return true, nil, err } + // pass false to not commit StateDB - sdb := NewSDB( - ctx, - &k, - NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())), - ) - evmObj := k.NewEVM(tmpCtx, evmMsg, evmCfg, nil /*tracer*/, sdb) + evmObj := k.NewEVM(sdb.Ctx(), evmMsg, evmCfg, nil /*tracer*/, sdb) rsp, err = k.ApplyEvmMsg(evmMsg, evmObj, false /*commit*/) if err != nil { if strings.Contains(err.Error(), core.ErrIntrinsicGas.Error()) { @@ -438,31 +466,39 @@ func (k Keeper) EstimateGasForEvmCallType( return len(rsp.VmError) > 0, rsp, nil } - // Execute the binary search and hone in on an executable gas limit - hi, err = evm.BinSearch(lo, hi, executable) + // BASE CASE: Jumping straight into binary search is extremely inefficient. + // Instead, execute at the highest allowable gas limit first to validate and + // set a smarter lower bound. + failed, result, err := executable(hi) if err != nil { - return nil, err + return nil, fmt.Errorf("eth call exec error: %w", err) } - - // The gas limit is now the highest gas limit that results in an executable transaction - // Reject the transaction as invalid if it still fails at the highest allowance - if hi == gasCap { - failed, result, err := executable(hi) - if err != nil { - return nil, fmt.Errorf("eth call exec error: %w", err) - } - - if failed && result != nil { + // If the base case fails for non-gas reasons, return the error immediately + if failed { + if result != nil && result.VmError != "" && result.VmError != vm.ErrOutOfGas.Error() { if result.VmError == vm.ErrExecutionReverted.Error() { return nil, fmt.Errorf("estimate gas VMError: %w", evm.NewRevertError(result.Ret)) } - - if result.VmError == vm.ErrOutOfGas.Error() { - return nil, fmt.Errorf("gas required gas limit (%d)", gasCap) - } - return nil, fmt.Errorf("estimate gas VMError: %s", result.VmError) } + return nil, fmt.Errorf("gas required exceeds allowance (%d)", hi) + } + + // Set smart lower bound based on actual gas used + if result.GasUsed > 0 { + lo = result.GasUsed - 1 + } else { + lo = 0 + } + + // Execute the binary search and hone in on an executable gas limit + estimateTolerance := evm.GasEstimateErrorRatioTolerance + if rootCtx.Value(evm.CtxKeyGasEstimateZeroTolerance) == true { + estimateTolerance = 0.00 + } + hi, err = evm.BinSearch(lo, hi, executable, estimateTolerance) + if err != nil { + return nil, err } return &evm.EstimateGasResponse{Gas: hi}, nil @@ -483,7 +519,7 @@ func (k Keeper) TraceTx( contextHeight := max(req.BlockNumber, 1) ctx := sdk.UnwrapSDKContext(goCtx) - ctx = ctx.WithValue(SimulationContextKey, true) + ctx = ctx.WithValue(evm.CtxKeyEvmSimulation, true) ctx = ctx.WithBlockHeight(contextHeight) ctx = ctx.WithBlockTime(req.BlockTime) ctx = ctx.WithHeaderHash(gethcommon.Hex2Bytes(req.BlockHash)) @@ -580,7 +616,7 @@ func (k Keeper) TraceCall( contextHeight := max(req.BlockNumber, 1) ctx := sdk.UnwrapSDKContext(goCtx) - ctx = ctx.WithValue(SimulationContextKey, true) + ctx = ctx.WithValue(evm.CtxKeyEvmSimulation, true) ctx = ctx.WithBlockHeight(contextHeight) ctx = ctx.WithBlockTime(req.BlockTime) ctx = ctx.WithHeaderHash(gethcommon.Hex2Bytes(req.BlockHash)) @@ -670,7 +706,7 @@ func (k Keeper) TraceBlock( WithConsensusParams(&cmtproto.ConsensusParams{ Block: &cmtproto.BlockParams{MaxGas: req.BlockMaxGas}, }) - ctx = ctx.WithValue(SimulationContextKey, true) + ctx = ctx.WithValue(evm.CtxKeyEvmSimulation, true) evmCfg := k.GetEVMConfig(ctx) diff --git a/x/evm/evmstate/keeper.go b/x/evm/evmstate/keeper.go index 8eb0655704..c87661d5bc 100644 --- a/x/evm/evmstate/keeper.go +++ b/x/evm/evmstate/keeper.go @@ -24,10 +24,6 @@ import ( "github.com/NibiruChain/nibiru/v2/x/evm" ) -type contextKey string - -const SimulationContextKey contextKey = "evm_simulation" - type Keeper struct { cdc codec.BinaryCodec // storeKey: For persistent storage of EVM state. @@ -132,7 +128,7 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger { // IsSimulation checks if the context is a simulation context. func IsSimulation(ctx sdk.Context) bool { - if val := ctx.Value(SimulationContextKey); val != nil { + if val := ctx.Value(evm.CtxKeyEvmSimulation); val != nil { if simulation, ok := val.(bool); ok && simulation { return true } diff --git a/x/evm/evmstate/keeper_test.go b/x/evm/evmstate/keeper_test.go index fc27802543..4993872b30 100644 --- a/x/evm/evmstate/keeper_test.go +++ b/x/evm/evmstate/keeper_test.go @@ -7,6 +7,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/NibiruChain/nibiru/v2/x/evm" evmstate "github.com/NibiruChain/nibiru/v2/x/evm/evmstate" "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" "github.com/NibiruChain/nibiru/v2/x/nutil/testutil" @@ -57,14 +58,14 @@ func (s *Suite) TestIsSimulation() { { name: "Context with simulation=true", setup: func(ctx sdk.Context) sdk.Context { - return ctx.WithValue(evmstate.SimulationContextKey, true) + return ctx.WithValue(evm.CtxKeyEvmSimulation, true) }, expected: true, }, { name: "Context with simulation=false", setup: func(ctx sdk.Context) sdk.Context { - return ctx.WithValue(evmstate.SimulationContextKey, false) + return ctx.WithValue(evm.CtxKeyEvmSimulation, false) }, expected: false, }, @@ -72,14 +73,14 @@ func (s *Suite) TestIsSimulation() { name: "Context with wrong type for simulation key", setup: func(ctx sdk.Context) sdk.Context { // Set a string instead of bool - return ctx.WithValue(evmstate.SimulationContextKey, "true") + return ctx.WithValue(evm.CtxKeyEvmSimulation, "true") }, expected: false, }, { name: "Context with nil value", setup: func(ctx sdk.Context) sdk.Context { - return ctx.WithValue(evmstate.SimulationContextKey, nil) + return ctx.WithValue(evm.CtxKeyEvmSimulation, nil) }, expected: false, }, @@ -135,14 +136,14 @@ func (s *Suite) TestIsDeliverTx() { { name: "Simulation context", setup: func(ctx sdk.Context) sdk.Context { - return ctx.WithValue(evmstate.SimulationContextKey, true) + return ctx.WithValue(evm.CtxKeyEvmSimulation, true) }, expected: false, }, { name: "CheckTx with simulation flag", setup: func(ctx sdk.Context) sdk.Context { - return ctx.WithIsCheckTx(true).WithValue(evmstate.SimulationContextKey, true) + return ctx.WithIsCheckTx(true).WithValue(evm.CtxKeyEvmSimulation, true) }, expected: false, }, @@ -150,7 +151,7 @@ func (s *Suite) TestIsDeliverTx() { name: "Simulation context with false value", setup: func(ctx sdk.Context) sdk.Context { // Setting simulation to false should be treated as DeliverTx - return ctx.WithValue(evmstate.SimulationContextKey, false) + return ctx.WithValue(evm.CtxKeyEvmSimulation, false) }, expected: true, }, diff --git a/x/evm/evmtest/tx.go b/x/evm/evmtest/tx.go index 2a33844f05..f67c83bf0d 100644 --- a/x/evm/evmtest/tx.go +++ b/x/evm/evmtest/tx.go @@ -138,6 +138,9 @@ func DeployAndExecuteERC20Transfer( // per zero byte and 16 per non-zero byte, which is why changing the address // bytes moves intrinsic gas in steps of 12. fixedRecipient = gethcommon.BigToAddress(big.NewInt(69_420)) + + // Deployed ERC20 testing contract + deployResp *DeployContractResult ) // TX 1: Deploy ERC-20 contract @@ -198,7 +201,10 @@ func GenerateEthTxMsgAndSigner( return } res, err := deps.App.EvmKeeper.EstimateGas( - sdk.WrapSDKContext(deps.Ctx()), + sdk.WrapSDKContext( + deps.Ctx(). + WithValue(evm.CtxKeyGasEstimateZeroTolerance, true), + ), &evm.EthCallRequest{ Args: estimateArgs, GasCap: srvconfig.DefaultEthCallGasLimit, diff --git a/x/evm/msg.go b/x/evm/msg.go index ca05c09fec..75cfb1388f 100644 --- a/x/evm/msg.go +++ b/x/evm/msg.go @@ -513,12 +513,33 @@ func (m MsgUpdateParams) GetSignBytes() []byte { return sdk.MustSortJSON(AminoCdc.MustMarshalJSON(&m)) } +// GasEstimateErrorRatioTolerance is the amount of overestimation eth_estimateGas is +// allowed to produce in order to speed up calculations. +const GasEstimateErrorRatioTolerance = 0.015 + // BinSearch executes the binary search and hone in on an executable gas limit func BinSearch( - lo, hi uint64, executable func(uint64) (bool, *MsgEthereumTxResponse, error), + lo uint64, + hi uint64, + executable func(uint64) (bool, *MsgEthereumTxResponse, error), + estimateTolernace float64, ) (uint64, error) { for lo+1 < hi { + // 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) < estimateTolernace { + break + } + mid := (hi + lo) / 2 + if mid > lo*2 { + // Skew the binary search toward lower gas values. Since most transactions + // only need slightly more gas than they actually consume (lo ≈ gasUsed), + // testing values closer to lo converges faster than standard bisection. + mid = lo * 2 + } failed, _, err := executable(mid) // If this errors, there was a consensus error, and the provided message // call or tx will never be accepted, regardless of how high we set the