Skip to content

Conversation

@TorstenStueber
Copy link
Contributor

@TorstenStueber TorstenStueber commented Oct 30, 2025

This PR implements the general gas tracking spec.

Follow-up PR to address gas scale: #10393

This PR ballooned into something much bigger than I expected. Many of the changes are due to the fact that all tests and a lot of the other logic has some touch points with the resource management logic. Most of the actual changes in logic are just in the folder metering of pallet-revive.

The main changes are that

  • Metering now works differently depending on whether the transaction as a whole defines weight and deposit limits ("Substrate execution mode") or just an Ethereum gas limit ("Ethereum execution mode"). The Ethereum execution mode is used for all eth_transact extrinsics.
  • There is a third resource (in addition to weight and storage deposits): Ethereum gas. In the Ethereum execution mode this is a shared resource (consumable through weight and through storage deposits).

Metering logic

Almost all changes in this PR are confined to the folder metering of pallet-revive. Before this PR there were two meters: a weight meter and a gas meter. They have now been combined into a main meter called ResourceMeter. Outside code only interacts with the ResourceMeter and not individually with the gas or storage meter. The reason is that in Ethereum execution mode gas is a shared resource and interacting with one meter influences the limits of the other meter.

Here are some finer points:

  • The previous code of the gas and deposit meters has been moved to the metering folder
  • Since outside code interacts only with the ResourceMeter, most functions now don't use a separate gas meter and deposit meter anymore but just a ResourceMeter
  • Similar to the two two kinds of deposits meters (Root and Nested), there are two kind of ResourceMeter: the top-level TransactionMeter used at the beginning of a transaction and a FrameMeter used once per frame
  • The limits of a TransactionMeter are specified through the TransactionLimits type, which distinguishes between Substrate and Ethereum execution mode.
  • The limits of a FrameMeter is specified through the type CallResources, which can either be a) no limits (e.g., in the case of contract creation), or b) a weight and deposit, or c) a gas limit.
  • The top level name of functions in the meters has been changed to be a bit more explicit about their purpose.
    • This applied particularly to the methods at the end of the lifecycle:
      • enforce_limit has been renamed to finalize as that describes the semantics better
      • try_into_deposit has been renamed to execute_postponed_deposits
  • For absorbing a frame meter into its parent meter, there are two different absorption functions:
    • absorb_weight_meter_only: when a frame reverts. In this case we ignore all storage deposits from the reverting frame. We still need to absorb the observed maximum deposit so that we determine the correct maximum deposit during dry running.
    • absorb_all_meters: when a frame was successful
  • The weight meter now has an effective_weight_limit, which needs to be recalculated whenever the deposit meter changes and is for optimization purposes.
  • The limits of the gas meter and deposit meters are now an Option<...>. When it is None, then this represents unlimited meters and this is only used for Ethereum style executions (the meters are not really unlimited, there will be a gas limit that effectively limits the resource usages of the weight and deposit meters).
  • In the weight meters, the sync_to_executor and sync_from_executor are a bit simplified and there is no need for engine_fuel_left anymore.

Other Changes

  • The old name gas for weights has been consistently replaced by weight
  • eth_call and eth_instantiate_with_code now take a weight_limit (used to ensure that weight does not exceed the max extrinsic weight) and an eth_gas_limit (the new externally defined limit)
  • The numeric calculation in compute_max_integer_quotient and compute_max_integer_pair_quotient (defined in substrate/frame/revive/src/evm/frees.rs) are meant to divide a number by the next fee multiplier
  • The call tracer does not take a GasMapper anymore as it will now be fed directly with the Ethereum gas values instead of weights
  • Re-entrancy protection now has three modes: no protection, Strict protection and AllowNext
    • AllowNext allows to re-enter the same contract but only for the next frame. This is required to implement reentrancy protection for simple transfers with call stipends
    • For Strict protection we set allows_reentry of the caller to false before the creation of the new frame, for AllowNext we to it after the creation
  • We define the max block gas as u64::MAX (as discussed with @pgherveou)
  • I now calculate the maximal required storage deposits during dry running (called max_storage_deposit in the deposit meter). For example, if a transaction encounters a storage deposit that is later refunded, then the total storage deposit is zero. However, the caller needs to provide enough resources so that temporarily the execution does not run out of gas and terminates the call prematurely.
  • The function try_upload_code now always takes a meter and records the storage deposit charge there
  • In this PR I added logic to correctly handle call stipends (this fixes [v2-periphery] OutOfGas due to eth transfer gas limit contract-issues#215)

Fixes

This fixes a couple of issues

TODOs

  • Ignore deposit refunds for dry running
  • Properly enforce weight limits
  • Fix gas mapping in the tracer
  • Fix (?) gas mapping in block storage (with_ethereum_context)
  • Check dry running logic again, and create_call, also in ExecConfig
  • Introduce SignedGas
  • use effective_gas_price instead of next fee multiplier
  • ensure that deducted amount is effective_gas_price * used gas
  • check logic of ensure_not_overdrawn
  • Optimize calculations
  • Check whether rounding is done correctly
  • add debug logging
  • Scale gas amounts charged in revm

Other TODOs

  • fix tests and benchmarks
  • add new tests
  • add code docs
  • resolve merge conflicts
  • run benchmarks
  • add PR description

@TorstenStueber TorstenStueber added the T7-smart_contracts This PR/Issue is related to smart contracts. label Oct 30, 2025
@TorstenStueber TorstenStueber requested review from a team as code owners October 30, 2025 11:42
@TorstenStueber TorstenStueber marked this pull request as draft October 30, 2025 11:43
athei and others added 3 commits October 30, 2025 09:49
Fix maxPriorityFee RPC

Change the EVM call opcodes to use proper gas for subcalls

Update tests-evm.yml

Update from github-actions[bot] running command 'prdoc --audience runtime_dev'

fix

fix

[pallet-revive] fix subxt submit & add debug statments (#10016)

- Fix subxt submit by default it's using
`author_submitAndWatchExtrinsic` even though we just want to fire and
forget
- Add debug instructions to log the signer & nonce of new eth
transactions when the node validate the transaction

---------

Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com>

Version bumps and prdocs reordering from stable2509 (#9974)

This PR backports regular version bumps and prdocs reordering from the
stable2509 branch back to master

---------

Co-authored-by: ParityReleases <[email protected]>

Update .github/workflows/tests-evm.yml

[Release|CI/CD] Fix polkadot prod docker image (#9975)

This PR introduces a workaround to fix failing polkadot production image
flow.
The initial issue is that, for some reason, our key that used to sign
the deb `InRelease` repo noted as expired on the first `apt update` run.
But reimport of the same key fixes is it. Until the reason for this
issue is fixed, this work around helps to keep the flow working

Introduce `/cmd label` for labelling pull requests (#9915)

This allows external contributors to set label for their pull request.

Closes: #9873

Use parity-large-persistent-test for merge queue (#10025)

Investigating issue with removing persistent runners from merge queue

cc paritytech/devops#3875

pallet_revive: Lower the deposit costs for child trie items (#10027)

Fixes #9246

---------

Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com>

pallet_revive: Fix incorrect `block.gaslimit` (#10026)

Fixes paritytech/contract-issues#112

---------

Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com>

update tests-evm
@TorstenStueber TorstenStueber marked this pull request as ready for review November 5, 2025 15:05
@TorstenStueber TorstenStueber requested a review from a team as a code owner November 5, 2025 15:05
@sekisamu
Copy link
Contributor

sekisamu commented Nov 6, 2025

@TorstenStueber Hello, thanks for the quick fix. I tested this by running the Uniswap v2 periphery test suite with the latest code from the torsten/gas-fixes branch. The result was 22 failing test cases.

For comparison, the same test suite has only 9 failing cases when run against the latest master branch.

It's worth noting that although the error messages in Hardhat might differ between the failing tests, I've observed on the node that the root cause for all these failed transactions is an OutOfGas error.

@TorstenStueber
Copy link
Contributor Author

@sekisamu thanks for the message. Can you add instructions how to run the tests in that repository? E.g.,

  • Did you just compile the revive-dev-node and the pallet-revive-eth-rpc from my branch?
  • Any setup required for Hardhat? Environment variables?
  • What command do you use to run the tests?

@sekisamu
Copy link
Contributor

@TorstenStueber Hello, I've updated the issue description and include the info you require.

  • yes, I've used the branch torsten/gas-fixes, the commit is: 947a492
  • No extra setup for hardhat, I've already included the account setup in hardhat config.
  • included in the updated issue.

And I've also tried again on the latest commit of torsten/gas-fixes, you can find the result here:paritytech/contract-issues#215 (comment)

@TorstenStueber
Copy link
Contributor Author

@sekisamu it is fixed on the latest commit of this PR and both test suites (https://github.com/papermoonio/v2-periphery-polkadot/tree/revm and https://github.com/papermoonio/eth-transfer-test) are 100% successful.

@github-actions
Copy link
Contributor

Command "bench --runtime dev --pallet pallet_revive" has finished ✅ See logs here

Subweight results:
File Extrinsic Old New Change [%]
substrate/frame/revive/src/weights.rs evm_instantiate 1.23ms 1.43ms +16.51
substrate/frame/revive/src/weights.rs seal_origin 343.00ns 395.00ns +15.16
substrate/frame/revive/src/weights.rs seal_call_data_load 310.00ns 343.00ns +10.65
substrate/frame/revive/src/weights.rs seal_balance 13.41us 14.24us +6.17
substrate/frame/revive/src/weights.rs rollback_transient_storage 1.26us 1.34us +6.02
substrate/frame/revive/src/weights.rs seal_caller 382.00ns 403.00ns +5.50
substrate/frame/revive/src/weights.rs seal_contains_transient_storage 3.63us 3.82us +5.40
substrate/frame/revive/src/weights.rs seal_call_data_size 314.00ns 330.00ns +5.10
substrate/frame/revive/src/weights.rs get_storage_empty 34.63us 32.86us -5.11
substrate/frame/revive/src/weights.rs caller_is_root 1.25us 1.19us -5.19
substrate/frame/revive/src/weights.rs seal_get_immutable_data 35.83us 33.90us -5.41
substrate/frame/revive/src/weights.rs seal_terminate 1.14us 1.07us -5.54
substrate/frame/revive/src/weights.rs seal_get_storage 37.44us 35.23us -5.89
substrate/frame/revive/src/weights.rs seal_block_hash 33.21us 31.20us -6.05
substrate/frame/revive/src/weights.rs seal_base_fee 1.24us 1.06us -14.41
substrate/frame/revive/src/weights.rs seal_ref_time_left 6.24us 1.93us -69.06
substrate/frame/revive/src/weights.rs seal_gas_limit 1.89us 339.00ns -82.06
Command output:

✅ Successful benchmarks of runtimes/pallets:
-- dev: ['pallet_revive']

Copy link
Contributor

@pgherveou pgherveou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests in substrate/frame/revive/src/metering/tests.rs are a bit hard to decipher
but otherwise looks good to me, approving now to unblock, will make a few more passes to make sure I am not missing anything.

}

#[test]
fn substrate_metering_charges_works() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these tests and values are a bit hard to decrypt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things:

  • The meaning of numbers in these tuples are unclear: It is just long tuples of numbers and I can add more structure to it but the test cases will then consume more space and the test file will become large.
  • The numbers themselves are unclear: I can add another validation function that will make it more obvious how these numbers have been calculated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this AI refactoring helps make things more readable / manageable by moving values to a JSON file like we do for precompile vector test

pg/gas-fix-comment...pg/gas-fix-comment-2

I am bit worried that all these tests seems to depend on exact gas value that might change with benchmark updates.

If there are no ways around that, let's use this approach, we can them find way to automate the test fixture updates

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now worries. The tests are completely independent of concrete weights and benchmarks. I was aware that this would have been a nightmare to maintain.

The logic in the test are all directly fed values to the meters, no weight tokens that depend on weights.

I will have a look at the AI generated code.

@TorstenStueber TorstenStueber changed the title Implement general gas tracking [Revive] Implement general gas tracking Nov 26, 2025
@alindima
Copy link
Contributor

@TorstenStueber is this in a state where we could use it in anvil to run our integration tests to ensure compatibility? (after rebasing on top of latest master and fixing the conflicts)

@TorstenStueber
Copy link
Contributor Author

@TorstenStueber is this in a state where we could use it in anvil to run our integration tests to ensure compatibility? (after rebasing on top of latest master and fixing the conflicts)

@alindima Yes, it is now!

@paritytech-workflow-stopper
Copy link

All GitHub workflows were cancelled due to failure one of the required jobs.
Failed workflow url: https://github.com/paritytech/polkadot-sdk/actions/runs/19728822752
Failed job name: test-linux-stable

Copy link
Member

@athei athei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really like the refactorings here, too. Couldn't find anything obviously wrong. Most of my comments are nits (we should really put a lot of error into explaining the fields of the ResourceMeter). Except for some questions around the gas stipend.

Comment on lines +86 to +90
max_total_gas: SignedGas<T>,
total_consumed_weight_before: Weight,
total_consumed_deposit_before: DepositOf<T>,

transaction_limits: TransactionLimits<T>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move the docs from above to the individual items as rust doc? Easier to understand what each thing is. Having good docs on data structurs is much more important than on functions. I can understand what it does if I understand the data structures.

For example, I don't understand what the difference is between max_total_gas and eth_gas_limit. I think having good docs for each field here would go a long way of explaining what is happening. i.e explaining why we need each field.

math::substrate_execution::new_nested_meter(self, limit),
}?;

new_meter.adjust_effective_weight_limit()?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be called by the new_* functions?

Comment on lines +104 to +106
// if this is provided, we will additionally ensure that execution will not exhaust this
// weight limit
maybe_weight_limit: Option<Weight>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is is provided and when it isn't?

Comment on lines +18 to +21
pub mod gas;
pub mod math;
pub mod storage;
pub mod weight;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are they pub on purpose? I would assume that most types in those modules are only used in this file. Maybe consider selectively pub use what you need?

impl Sealed for super::Nested {}
}

#[cfg(test)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move tests to their own file (sub module) since you are touching it anyways?

}
}

#[cfg(test)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move to its own file since you are touching this anyways?

Comment on lines -111 to +125
storage_bytes: u32,
pub storage_bytes: u32,
/// How many items of storage are accumulated in this contract's child trie.
storage_items: u32,
pub storage_items: u32,
/// This records to how much deposit the accumulated `storage_bytes` amount to.
pub storage_byte_deposit: BalanceOf<T>,
/// This records to how much deposit the accumulated `storage_items` amount to.
storage_item_deposit: BalanceOf<T>,
pub storage_item_deposit: BalanceOf<T>,
/// This records how much deposit is put down in order to pay for the contract itself.
///
/// We need to store this information separately so it is not used when calculating any refunds
/// since the base deposit can only ever be refunded on contract termination.
storage_base_deposit: BalanceOf<T>,
pub storage_base_deposit: BalanceOf<T>,
/// The size of the immutable data of this contract.
immutable_data_len: u32,
pub immutable_data_len: u32,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping we could down on the public fields here and not increase them :D Where do we need to access them?

// We use ALL_STIPEND to detect the typical gas limit solc defines as a call stipend
// This is just a heuristic
let add_stipend =
!value.is_zero() || gas_limit.try_into().is_ok_and(|limit: u64| limit == CALL_STIPEND);
Copy link
Member

@athei athei Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think those should be two conditionals:

  1. If value !=0: Set gas_limit to at least the stipend (no heuristic just EVM semantics). Not all value transfers are plain transfers. Could call into more complicated logic. Don't think we can mess with disabling reentrancy here.
  2. If limit == CALL_STIPEND: Only this part is a heuristic. Transfer detected. Disable reentrancy on top of setting the limit to at least the stipend.

Comment on lines +205 to +206
if add_stipend {
ReentrancyProtection::AllowNext
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should disable reentrancy for ever CALL with value != 0. They are not necessarily plain transfers.

Comment on lines +140 to +148
let stipend = if *add_stipend {
let weight_stipend = determine_call_stipend::<T>();
if weight_left.any_lt(weight_stipend) {
return Err(<Error<T>>::OutOfGas.into())
}

weight_limit.saturating_accrue(weight_stipend);

Some(weight_stipend)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK the stipend is not added to the limit. I think its weight_limit = max(weight_limit, stipend).

@pgherveou
Copy link
Contributor

@TorstenStueber is this in a state where we could use it in anvil to run our integration tests to ensure compatibility? (after rebasing on top of latest master and fixing the conflicts)

@alindima Yes, it is now!

fyi @mokita-j created a branch in foundry polkadot that compile against this branch

Copy link
Member

@xermicus xermicus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks very good. Thanks!

I ran the revive compiler integration test suite against this branch and saw no regressions introduced vs. master.

Comment on lines +18 to +19
clearStorageSlot(0);
clearStorageSlot(1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
clearStorageSlot(0);
clearStorageSlot(1);
uint slot;
assembly {
slot := a.slot
}
clearStorageSlot(slot);
assembly {
slot := b.slot
}
clearStorageSlot(slot);

Nit: This always gets the actual storage slot (makes it easier to refactor the test)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T7-smart_contracts This PR/Issue is related to smart contracts.

Projects

Status: In progress

8 participants