Skip to content

pallet_revive: Replace adhoc pre-compiles with pre-compile framework#8262

Merged
athei merged 28 commits intomasterfrom
at/precompiles
Apr 28, 2025
Merged

pallet_revive: Replace adhoc pre-compiles with pre-compile framework#8262
athei merged 28 commits intomasterfrom
at/precompiles

Conversation

@athei
Copy link
Member

@athei athei commented Apr 16, 2025

This PRs adds the ability to define custom pre-compiles from outside the pallet. Before, all pre-compiles were hard coded as part of the pallet.

Design

  1. Adding a pre-compile is as easy as implementing the new Precompile trait on any type. It can be added to the pallet by passing it into Config::Precompiles. This config know excepts a tuple of multiple types that each implement Precompile.
  2. Each pre-compile has to implement Solidity ABI. Meaning its inputs and outputs are encoded according to Eth ABI. This makes writing a pre-compile much simpler since it doesn't have to implement its own decoding logic. More importantly: It makes it trivial to consume the API from a Solidity contract.
  3. We constrain the address space of pre-compiles to a safe range so that they cannot accidentally match a wide range creating a collision with real contracts.
  4. We check that pre-compile address ranges do not overlap at compile time.
  5. Pre-compiles behave exactly as a normal contract. They exist as frames on the call stack and the environment they observe is their own (not the one of the calling contract). They can also be delegate called which changes the semantics in the same way as for normal contracts: They observe the environment of the calling contract.
  6. They can also be called by the origin without any other contract in-between.

Check the rustdocs of the precompile module on how to write a pre-compile.

Changes

  1. A new module precompiles is added that contains the framework to write pre-compiles. It also contains the sub module builtin that contains hard coded pre-compiles which exist Ethereum.
  2. The pure_precompiles module was deleted since all its pre-compiles were ported over to the new framework and are now housed in builtin.
  3. The CallSetup type is moved outside of the benchmarking module because it is also needed for testing code now. It is intended to be used for implementors outside of the crate to test the pre-compiles (in addition to benchmarking them).

Follow Ups

Fixes #6716

@athei
Copy link
Member Author

athei commented Apr 16, 2025

/cmd fmt

@athei
Copy link
Member Author

athei commented Apr 16, 2025

/cmd bench --runtime dev --pallet pallet_revive

@github-actions
Copy link
Contributor

Command "bench --runtime dev --pallet pallet_revive" has started 🚀 See logs here

@github-actions
Copy link
Contributor

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

Details

Subweight results:
File Extrinsic Old New Change [%]
substrate/frame/revive/src/weights.rs bn128_add 15.82us 743.36us +4598.85
substrate/frame/revive/src/weights.rs blake2f 27.67us 751.38us +2615.59
substrate/frame/revive/src/weights.rs identity 39.19us 766.60us +1856.16
substrate/frame/revive/src/weights.rs ecdsa_recover 45.17us 772.35us +1609.75
substrate/frame/revive/src/weights.rs sha2_256 304.01us 1.08ms +254.97
substrate/frame/revive/src/weights.rs ripemd_160 990.89us 1.77ms +78.72
substrate/frame/revive/src/weights.rs bn128_mul 1.02ms 1.75ms +71.68
substrate/frame/revive/src/weights.rs seal_copy_to_contract 62.81us 77.86us +23.98
substrate/frame/revive/src/weights.rs seal_return 63.03us 77.64us +23.17
substrate/frame/revive/src/weights.rs seal_to_account_id 31.68us 34.76us +9.73
substrate/frame/revive/src/weights.rs seal_contains_transient_storage 1.95us 1.83us -5.83
substrate/frame/revive/src/weights.rs seal_balance 5.17us 4.81us -6.98
substrate/frame/revive/src/weights.rs seal_weight_to_fee 1.60us 1.47us -8.36
substrate/frame/revive/src/weights.rs rollback_transient_storage 1.21us 1.11us -8.41
substrate/frame/revive/src/weights.rs instr 1.57ms 1.35ms -13.66
substrate/frame/revive/src/weights.rs seal_weight_left 736.00ns 610.00ns -17.12
substrate/frame/revive/src/weights.rs seal_caller_is_origin 347.00ns 232.00ns -33.14
substrate/frame/revive/src/weights.rs seal_gas_limit 482.00ns 306.00ns -36.51
substrate/frame/revive/src/weights.rs seal_gas_price 276.00ns 168.00ns -39.13
substrate/frame/revive/src/weights.rs seal_caller_is_root 299.00ns 174.00ns -41.81
substrate/frame/revive/src/weights.rs seal_own_code_hash 305.00ns 172.00ns -43.61
substrate/frame/revive/src/weights.rs seal_address 346.00ns 189.00ns -45.38
substrate/frame/revive/src/weights.rs seal_base_fee 292.00ns 158.00ns -45.89
substrate/frame/revive/src/weights.rs seal_minimum_balance 282.00ns 151.00ns -46.45
substrate/frame/revive/src/weights.rs seal_call_data_load 293.00ns 153.00ns -47.78
substrate/frame/revive/src/weights.rs seal_now 290.00ns 150.00ns -48.28
substrate/frame/revive/src/weights.rs seal_return_data_size 285.00ns 140.00ns -50.88
substrate/frame/revive/src/weights.rs seal_ref_time_left 276.00ns 135.00ns -51.09
substrate/frame/revive/src/weights.rs seal_value_transferred 292.00ns 140.00ns -52.05
substrate/frame/revive/src/weights.rs seal_call_data_size 320.00ns 141.00ns -55.94
substrate/frame/revive/src/weights.rs seal_block_number 308.00ns 135.00ns -56.17
substrate/frame/revive/src/weights.rs seal_caller 1.13us 217.00ns -80.80
substrate/frame/revive/src/weights.rs seal_origin 1.09us 182.00ns -83.38
substrate/frame/revive/src/weights.rs seal_call_precompile 486.83us Added
Command output:

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

@athei
Copy link
Member Author

athei commented Apr 16, 2025

/cmd bench --runtime dev --pallet pallet_revive

@github-actions
Copy link
Contributor

Command "bench --runtime dev --pallet pallet_revive" has started 🚀 See logs here

@github-actions
Copy link
Contributor

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

Details

Subweight results:
File Extrinsic Old New Change [%]
substrate/frame/revive/src/weights.rs seal_return 63.03us 78.04us +23.81
substrate/frame/revive/src/weights.rs seal_copy_to_contract 62.81us 77.67us +23.67
substrate/frame/revive/src/weights.rs sha2_256 304.01us 341.36us +12.29
substrate/frame/revive/src/weights.rs seal_to_account_id 31.68us 34.67us +9.46
substrate/frame/revive/src/weights.rs bn128_add 15.82us 16.89us +6.73
substrate/frame/revive/src/weights.rs get_transient_storage_empty 1.48us 1.39us -6.41
substrate/frame/revive/src/weights.rs get_transient_storage_full 1.64us 1.53us -6.42
substrate/frame/revive/src/weights.rs seal_balance 5.17us 4.81us -6.98
substrate/frame/revive/src/weights.rs seal_contains_transient_storage 1.95us 1.80us -7.36
substrate/frame/revive/src/weights.rs seal_weight_to_fee 1.60us 1.48us -7.80
substrate/frame/revive/src/weights.rs rollback_transient_storage 1.21us 1.08us -10.72
substrate/frame/revive/src/weights.rs seal_weight_left 736.00ns 637.00ns -13.45
substrate/frame/revive/src/weights.rs instr 1.57ms 1.30ms -17.18
substrate/frame/revive/src/weights.rs seal_gas_limit 482.00ns 370.00ns -23.24
substrate/frame/revive/src/weights.rs seal_caller_is_root 299.00ns 203.00ns -32.11
substrate/frame/revive/src/weights.rs seal_caller_is_origin 347.00ns 228.00ns -34.29
substrate/frame/revive/src/weights.rs seal_ref_time_left 276.00ns 168.00ns -39.13
substrate/frame/revive/src/weights.rs seal_gas_price 276.00ns 163.00ns -40.94
substrate/frame/revive/src/weights.rs seal_address 346.00ns 204.00ns -41.04
substrate/frame/revive/src/weights.rs seal_value_transferred 292.00ns 170.00ns -41.78
substrate/frame/revive/src/weights.rs seal_own_code_hash 305.00ns 176.00ns -42.30
substrate/frame/revive/src/weights.rs seal_block_number 308.00ns 167.00ns -45.78
substrate/frame/revive/src/weights.rs seal_now 290.00ns 156.00ns -46.21
substrate/frame/revive/src/weights.rs seal_minimum_balance 282.00ns 151.00ns -46.45
substrate/frame/revive/src/weights.rs seal_base_fee 292.00ns 154.00ns -47.26
substrate/frame/revive/src/weights.rs seal_return_data_size 285.00ns 149.00ns -47.72
substrate/frame/revive/src/weights.rs seal_call_data_load 293.00ns 142.00ns -51.54
substrate/frame/revive/src/weights.rs seal_call_data_size 320.00ns 154.00ns -51.88
substrate/frame/revive/src/weights.rs seal_caller 1.13us 233.00ns -79.38
substrate/frame/revive/src/weights.rs seal_origin 1.09us 206.00ns -81.19
substrate/frame/revive/src/weights.rs seal_call_precompile 486.20us Added
Command output:

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

@pgherveou
Copy link
Contributor

pgherveou commented Apr 24, 2025

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.

Looks really good!

Just one thing: I tried the revive integration tests against this branch and there is a failing case passing against master: Delegate calls to unknown (non-existing) contract address revert with an out of gas error. I could not pin down the root cause quickly but to me this looks like a small regression?

athei and others added 2 commits April 25, 2025 18:34
@athei
Copy link
Member Author

athei commented Apr 25, 2025

Looks really good!

Just one thing: I tried the revive integration tests against this branch and there is a failing case passing against master: Delegate calls to unknown (non-existing) contract address revert with an out of gas error. I could not pin down the root cause quickly but to me this looks like a small regression?

Can you send me a link to the test code in question?

@xermicus
Copy link
Member

https://github.com/paritytech/revive/blob/f6a412eef4d5aa9779b8499713e53c2b77a91ad3/crates/integration/contracts/DelegateCaller.sol#L31

I see the out of gas revert not only for the zero address but any arbitrary address I set there.

@athei
Copy link
Member Author

athei commented Apr 26, 2025

I made a change that the gas for loading the code from storage is charged from the nested meter and not the parent meter. But in your test all remaining gas is passed. So it it should not affect this.

I see the out of gas revert not only for the zero address but any arbitrary address I set there.

Weird. As if delegate calls don't work at all. However, we have tests for delegate calls which continued to work unchanged. I have to investigate further. I will try to run this test against my dev node and add some logs.

@xermicus
Copy link
Member

The arguments stay the same so I assume there must be something changed in the pallet.

Trace log from the PR branch:

2025-04-26T12:40:18.460783Z TRACE runtime::revive::strace: delegate_call(flags_and_callee: 196624, ref_time_limit: 18446744073709551615, proof_size_limit: 18446744073709551615, deposit_ptr: 4294836128, input_data: 17180000432, output_data: 18446180574000447504) = Err(TrapReason::SupervisorError(Module(ModuleError { index: 3, error: [3, 0, 0, 0], message: Some("OutOfGas") }))) gas_consumed: Weight { ref_time: 100000000000000, proof_size: 3221225472 }

On master:

2025-04-26T12:57:16.755145Z TRACE runtime::revive::strace: delegate_call(flags_and_callee: 196624, ref_time_limit: 18446744073709551615, proof_size_limit: 18446744073709551615, deposit_ptr: 4294836128, input_data: 17180000432, output_data: 18446180574000447504) = Ok(Success) gas_consumed: Weight { ref_time: 121866192, proof_size: 7557 }    

The weight in that log corresponds to the sandbox runners default limit.. Or is the runners config wrong? To test against this branch I just change the ChainExtension type to Precompiles.

@athei
Copy link
Member Author

athei commented Apr 26, 2025

The arguments stay the same so I assume there must be something changed in the pallet.

Agreed. It is a regression. Didn't had time to track it down, yet.

@athei
Copy link
Member Author

athei commented Apr 28, 2025

So I think I found the bug. When we error out early when creating a new frame we never refund the nested gas into the parent meter. The bug was introduced earlier but the changes here added one more case where this can happen: This is because I moved the check if the delegated to contract exists after the spawning of the nested meter.

I did a bit of research: What we should do is that everything except the execution costs should be charged from the parent meter as this is what EVM does.

I will add a regression test and fix this.

@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/14712012332
Failed job name: test-linux-stable

@athei athei enabled auto-merge April 28, 2025 17:15
@athei athei added this pull request to the merge queue Apr 28, 2025
Merged via the queue into master with commit 398bbfc Apr 28, 2025
239 of 249 checks passed
@athei athei deleted the at/precompiles branch April 28, 2025 18:01
@xermicus
Copy link
Member

xermicus commented May 5, 2025

So I think I found the bug. When we error out early when creating a new frame we never refund the nested gas into the parent meter. The bug was introduced earlier but the changes here added one more case where this can happen: This is because I moved the check if the delegated to contract exists after the spawning of the nested meter.

I did a bit of research: What we should do is that everything except the execution costs should be charged from the parent meter as this is what EVM does.

I will add a regression test and fix this.

Awesome thanks for digging this up!

castillax pushed a commit that referenced this pull request May 12, 2025
…8262)

This PRs adds the ability to define custom pre-compiles from outside the
pallet. Before, all pre-compiles were hard coded as part of the pallet.

1. Adding a pre-compile is as easy as implementing the new `Precompile`
trait on any type. It can be added to the pallet by passing it into
`Config::Precompiles`. This config know excepts a tuple of multiple
types that each implement `Precompile`.
2. Each pre-compile has to implement Solidity ABI. Meaning its inputs
and outputs are encoded according to Eth ABI. This makes writing a
pre-compile much simpler since it doesn't have to implement its own
decoding logic. More importantly: It makes it trivial to consume the API
from a Solidity contract.
3. We constrain the address space of pre-compiles to a safe range so
that they cannot accidentally match a wide range creating a collision
with real contracts.
4. We check that pre-compile address ranges do not overlap at compile
time.
5. Pre-compiles behave exactly as a normal contract. They exist as
frames on the call stack and the environment they observe is their own
(not the one of the calling contract). They can also be delegate called
which changes the semantics in the same way as for normal contracts:
They observe the environment of the calling contract.
6. They can also be called by the origin without any other contract
in-between.

Check the rustdocs of the `precompile` module on how to write a
pre-compile.

1. A new module `precompiles` is added that contains the framework to
write pre-compiles. It also contains the sub module `builtin` that
contains hard coded pre-compiles which exist Ethereum.
2. The `pure_precompiles` module was deleted since all its pre-compiles
were ported over to the new framework and are now housed in `builtin`.
4. The `CallSetup` type is moved outside of the `benchmarking` module
because it is also needed for testing code now. It is intended to be
used for implementors outside of the crate to test the pre-compiles (in
addition to benchmarking them).

- #8363
- #8364
- #8362

Fixes [#6716](#6716)

---------

Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: PG Herveou <pgherveou@gmail.com>
Co-authored-by: xermicus <cyrill@parity.io>
fellowship-merge-bot bot pushed a commit to polkadot-fellows/runtimes that referenced this pull request Aug 7, 2025
This brings in `stable2506` Polkadot SDK, and integrates many new
features.

Integrated breaking changes to be verified by the original authors:

- [x] ~paritytech/polkadot-sdk#8127 @kianenigma
@Ank4n~
     This will come in with AHM, and not before.
- [x] paritytech/polkadot-sdk#7597 @gui1117 
- [x] paritytech/polkadot-sdk#8254 @bkchr 
- [x] paritytech/polkadot-sdk#7592 @bkontur 
- [x] paritytech/polkadot-sdk#8382
@UtkarshBhardwaj007
- [x] paritytech/polkadot-sdk#8021 @serban300 
- [x] paritytech/polkadot-sdk#8344 @serban300 
- [x] paritytech/polkadot-sdk#8262 @athei 
- [x] paritytech/polkadot-sdk#8584 @athei 
- [x] paritytech/polkadot-sdk#8299 @skunert
- [x] paritytech/polkadot-sdk#8652 @pgherveou 
- [x] paritytech/polkadot-sdk#8554 @pgherveou 
- [x] paritytech/polkadot-sdk#8281 @mrshiposha 
- [x] paritytech/polkadot-sdk#7730
@franciscoaguirre
- [x] paritytech/polkadot-sdk#8599 @yrong
@claravanstaden
- [x] paritytech/polkadot-sdk#8531 @bkontur 
- [x] paritytech/polkadot-sdk#8409 @kianenigma 
- [x] paritytech/polkadot-sdk#9137
@franciscoaguirre
- [x] paritytech/polkadot-sdk#7944 @bkontur 
- [x] paritytech/polkadot-sdk#8179 @bkontur 
- [x] paritytech/polkadot-sdk#8037 @yrong

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: claravanstaden <claravanstaden64@gmail.com>
Co-authored-by: Branislav Kontur <bkontur@gmail.com>
Co-authored-by: Bastian Köcher <git@kchr.de>
Co-authored-by: Alain Brenzikofer <alain@integritee.network>
Co-authored-by: kianenigma <kian@parity.io>
Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com>
Co-authored-by: ron <yrong1997@gmail.com>
Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>
Co-authored-by: Overkillus <maciej.zyszkiewicz@parity.io>
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: Done

Development

Successfully merging this pull request may close these issues.

Replace chain extensions with pre-compile framework

3 participants