Skip to content

Commit

Permalink
Moves precompile-utils from moonbeam into frontier. (#1150)
Browse files Browse the repository at this point in the history
* Moves precompile-utils into frontier

* Remove redundant affix dependency

* Fix formatting

* Fix toml formatting

* Fix clippy issues

* Fix formatting

* Update Cargo.toml

Co-authored-by: Qinxuan Chen <koushiro.cqx@gmail.com>

* Update Cargo.toml

Co-authored-by: Qinxuan Chen <koushiro.cqx@gmail.com>

* Remove paste from dependencies

* Move the precompile-utils crates from utils folder to the root folder

* Replace sha3 crate with sp-core-hashing

* Replace sha3 crate with sp-core-hashing

* Replaces Moonbeam license statement with Parity license statement

* Remove the pre-commit file

---------

Co-authored-by: Qinxuan Chen <koushiro.cqx@gmail.com>
  • Loading branch information
rimbi and koushiro authored Aug 29, 2023
1 parent ba35e15 commit 8b0ff04
Show file tree
Hide file tree
Showing 99 changed files with 11,758 additions and 749 deletions.
1,779 changes: 1,030 additions & 749 deletions Cargo.lock

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ members = [
"primitives/self-contained",
"template/node",
"template/runtime",
"precompiles",
"precompiles/macro",
"precompiles/tests-external",
]
resolver = "2"

Expand All @@ -43,6 +46,7 @@ repository = "https://github.com/paritytech/frontier/"
async-trait = "0.1"
bn = { package = "substrate-bn", version = "0.6", default-features = false }
clap = { version = "4.3", features = ["derive", "deprecated"] }
derive_more = "0.99"
environmental = { version = "1.1.4", default-features = false }
ethereum = { version = "0.14.0", default-features = false }
ethereum-types = { version = "0.14.1", default-features = false }
Expand All @@ -51,17 +55,20 @@ futures = "0.3.28"
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
hex-literal = "0.4.1"
impl-serde = { version = "0.4.0", default-features = false }
impl-trait-for-tuples = "0.2.1"
jsonrpsee = "0.16.2"
kvdb-rocksdb = "0.19.0"
libsecp256k1 = { version = "0.7.1", default-features = false }
log = { version = "0.4.20", default-features = false }
num_enum = { version = "0.6.1", default-features = false }
parity-db = "0.4.10"
parking_lot = "0.12.1"
rlp = { version = "0.5.2", default-features = false }
scale-codec = { package = "parity-scale-codec", version = "3.6.4", default-features = false, features = ["derive"] }
scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] }
serde_json = "1.0"
similar-asserts = "1.1.0"
sqlx = { version = "0.7.1", default-features = false, features = ["macros"] }
thiserror = "1.0"
tokio = "1.32.0"
Expand Down Expand Up @@ -96,6 +103,8 @@ sp-consensus = { version = "0.10.0-dev", git = "https://github.com/paritytech/su
sp-consensus-aura = { version = "0.10.0-dev", git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
sp-consensus-grandpa = { version = "4.0.0-dev", git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
sp-core = { version = "21.0.0", git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
sp-core-hashing = { version = "9.0.0", git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
sp-core-hashing-proc-macro = { version = "9.0.0", git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
sp-database = { version = "4.0.0-dev", git = "https://github.com/paritytech/substrate", branch = "master" }
sp-inherents = { version = "4.0.0-dev", git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
sp-io = { version = "23.0.0", git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
Expand Down Expand Up @@ -165,6 +174,10 @@ pallet-evm-test-vector-support = { version = "1.0.0-dev", path = "frame/evm/test
pallet-hotfix-sufficients = { version = "1.0.0", path = "frame/hotfix-sufficients", default-features = false }
# Frontier Template
frontier-template-runtime = { path = "template/runtime", default-features = false }

# Frontier utils
precompile-utils = { path = "precompiles", default-features = false }

# Arkworks
ark-bls12-377 = { version = "0.4.0", default-features = false, features = ["curve"] }
ark-bw6-761 = { version = "0.4.0", default-features = false }
Expand Down
6 changes: 6 additions & 0 deletions frame/evm/src/runner/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,8 @@ mod tests {
&config,
&MockPrecompileSet,
false,
None,
None,
|_| {
let res = Runner::<Test>::execute(
H160::default(),
Expand All @@ -1252,6 +1254,8 @@ mod tests {
&config,
&MockPrecompileSet,
false,
None,
None,
|_| (ExitReason::Succeed(ExitSucceed::Stopped), ()),
);
assert_matches!(
Expand Down Expand Up @@ -1282,6 +1286,8 @@ mod tests {
&config,
&MockPrecompileSet,
false,
None,
None,
|_| (ExitReason::Succeed(ExitSucceed::Stopped), ()),
);
assert!(res.is_ok());
Expand Down
53 changes: 53 additions & 0 deletions precompiles/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[package]
name = "precompile-utils"
authors = { workspace = true }
description = "Utils to write EVM precompiles."
edition = "2021"
version = "0.1.0"

[dependencies]
derive_more = { workspace = true, optional = true }
environmental = { workspace = true }
hex = { workspace = true }
hex-literal = { workspace = true, optional = true }
impl-trait-for-tuples = { workspace = true }
log = { workspace = true }
num_enum = { workspace = true }
scale-info = { workspace = true, optional = true, features = ["derive"] }
serde = { workspace = true, optional = true }
similar-asserts = { workspace = true, optional = true }

# Moonbeam
precompile-utils-macro = { path = "macro" }

# Substrate
frame-support = { workspace = true }
frame-system = { workspace = true }
scale-codec = { package = "parity-scale-codec", workspace = true }
sp-core = { workspace = true }
sp-io = { workspace = true }
sp-runtime = { workspace = true }
sp-std = { workspace = true }

# Frontier
evm = { workspace = true, features = ["with-codec"] }
fp-evm = { workspace = true }
pallet-evm = { workspace = true, features = ["forbid-evm-reentrancy"] }

[dev-dependencies]
hex-literal = { workspace = true }

[features]
default = ["std"]
std = [
"environmental/std",
"fp-evm/std",
"frame-support/std",
"frame-system/std",
"pallet-evm/std",
"scale-codec/std",
"sp-core/std",
"sp-io/std",
"sp-std/std",
]
testing = ["derive_more", "hex-literal", "scale-info", "serde", "similar-asserts", "std"]
33 changes: 33 additions & 0 deletions precompiles/macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[package]
name = "precompile-utils-macro"
authors = { workspace = true }
description = ""
edition = "2021"
version = "0.1.0"

[lib]
proc-macro = true

[[test]]
name = "tests"
path = "tests/tests.rs"

[dependencies]
case = "1.0"
num_enum = { workspace = true }
prettyplease = "0.2.12"
proc-macro2 = "1.0"
quote = "1.0"
sp-core-hashing = { workspace = true }
syn = { version = "1.0", features = ["extra-traits", "fold", "full", "visit"] }

[dev-dependencies]
macrotest = "1.0.9"
trybuild = "1.0"

precompile-utils = { path = "../", features = ["testing"] }

fp-evm = { workspace = true }
frame-support = { workspace = true }
sp-core-hashing = { workspace = true }
sp-std = { workspace = true }
199 changes: 199 additions & 0 deletions precompiles/macro/docs/precompile_macro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# `#[precompile]` procedural macro.

This procedural macro allows to simplify the implementation of an EVM precompile or precompile set
using an `impl` block with annotations to automatically generate:

- the implementation of the trait `Precompile` or `PrecompileSet` (exposed by the `fp_evm` crate)
- parsing of the method parameters from Solidity encoding into Rust type, based on the `solidity::Codec`
trait (exposed by the `precompile-utils` crate)
- a test to ensure the types expressed in the Solidity signature match the Rust types in the
implementation.

## How to use

Define your precompile type and write an `impl` block that will contain the precompile methods
implementation. This `impl` block can have type parameters and a `where` clause, which will be
reused to generate the `Precompile`/`PrecompileSet` trait implementation and the enum representing
each public function of precompile with its parsed arguments.

```rust,ignore
pub struct ExemplePrecompile<R, I>(PhantomData<(R,I)>);
#[precomile_utils::precompile]
impl<R, I> ExemplePrecompile<R, I>
where
R: pallet_evm::Config
{
#[precompile::public("example(uint32)")]
fn example(handle: &mut impl PrecompileHandle, arg: u32) -> EvmResult<u32> {
Ok(arg * 2)
}
}
```

The example code above will automatically generate an enum like

```rust,ignore
#[allow(non_camel_case_types)]
pub enum ExemplePrecompileCall<R, I>
where
R: pallet_evm::Config
{
example {
arg: u32
},
// + an non constrible variant with a PhantomData<(R,I)>
}
```

This enum have the function `parse_call_data` that can parse the calldata, recognize the Solidity
4-bytes selector and parse the appropriate enum variant.

It will also generate automatically an implementation of `Precompile`/`PrecompileSet` that calls
this function and the content of the variant to its associated function of the `impl` block.

## Function attributes

`#[precompile::public("signature")]` allows to declare a function as a public method of the
precompile with the provided Solidity signature. A function can have multiple `public` attributes to
support renamed functions with backward compatibility, however the arguments must have the same
type. It is not allowed to use the exact same signature multiple times.

The function must take a `&mut impl PrecompileHandle` as parameter, followed by all the parameters
of the Solidity function in the same order. Those parameters types must implement `solidity::Codec`, and
their name should match the one used in the Solidity interface (.sol) while being in `snake_case`,
which will automatically be converted to `camelCase` in revert messages. The function must return an
`EvmResult<T>`, which is an alias of `Result<T, PrecompileFailure>`. This `T` must implement the
`solidity::Codec` trait and must match the return type in the Solidity interface. The macro will
automatically encode it to Solidity format.

By default those functions are considered non-payable and non-view (can cause state changes). This
can be changed using either `#[precompile::payable]` or `#[precompile::view]`. Only one can be used.

It is also possible to declare a fallback function using `#[precompile::fallback]`. This function
will be called if the selector is unknown or if the input is less than 4-bytes long (no selector).
This function cannot have any parameter outside of the `PrecompileHandle`. A function can be both
`public` and `fallback`.

In case some check must be performed before parsing the input, such as forbidding being called from
some address, a function can be annotated with `#[precompile::pre_check]`:

```rust,ignore
#[precompile::pre_check]
fn pre_check(handle: &mut impl PrecompileHandle) -> EvmResult {
todo!("Perform your check here")
}
```

This function cannot have other attributes.

## PrecompileSet

By default the macro considers the `impl` block to represent a precompile and this will implement
the `Precompile` trait. If you want to instead implement a precompile set, you must add the
`#[precompile::precompile_set]` to the `impl` block.

Then, it is necessary to have a function annotated with the `#[precompile::discriminant]` attribute.
This function is called with the **code address**, the address of the precompile. It must return
`None` if this address is not part of the precompile set, or `Some` if it is. The `Some` variants
contains a value of a type of your choice that represents which member of the set this address
corresponds to. For example for our XC20 precompile sets this function returns the asset id
corresponding to this address if it exists.

Finally, every other function annotated with a `precompile::_` attribute must now take this
discriminant as first parameter, before the `PrecompileHandle`.

```rust,ignore
pub struct ExemplePrecompileSet<R>(PhantomData<R>);
#[precompile_utils::precompile]
#[precompile::precompile_set]
impl<R> ExamplePrecompileSet<R>
where
R: pallet_evm::Config
{
#[precompile::discriminant]
fn discriminant(address: H160) -> Option<u8> {
// Replace with your discriminant logic.
Some(match address {
a if a == H160::from(42) => 1
a if a == H160::from(43) => 2,
_ => return None,
})
}
#[precompile::public("example(uint32)")]
fn example(discriminant: u8, handle: &mut impl PrecompileHandle, arg: u32) -> EvmResult {
// Discriminant can be used here.
Ok(arg * discriminant)
}
}
```

## Solidity signatures test

The macro will automatically generate a unit test to ensure that the types expressed in a `public`
attribute matches the Rust parameters of the function, thanks to the `solidity::Codec` trait having the
`solidity_type() -> String` function.

If any **parsed** argument (discriminant is not concerned) depends on the type parameters of the
`impl` block, the macro will not be able to produce valid code and output an error like:

```text
error[E0412]: cannot find type `R` in this scope
--> tests/precompile/compile-fail/test/generic-arg.rs:25:63
|
23 | impl<R: Get<u32>> Precompile<R> {
| - help: you might be missing a type parameter: `<R>`
24 | #[precompile::public("foo(bytes)")]
25 | fn foo(handle: &mut impl PrecompileHandle, arg: BoundedBytes<R>) -> EvmResult {
| ^ not found in this scope
```

In this case you need to annotate the `impl` block with the `#[precompile::test_concrete_types(...)]`
attributes. The `...` should be replaced with concrete types for each type parameter, like a mock
runtime. Those types are only used to generate the test and only one set of types can be used.

```rust,ignore
pub struct ExamplePrecompile<R, I>(PhantomData<(R, I)>);
pub struct GetMaxSize<R, I>(PhantomData<(R, I)>);
impl<R: SomeConfig, I> Get<u32> for GetMaxSize<R, I> {
fn get() -> u32 {
<R as SomeConfig<I>>::SomeConstant::get()
}
}
#[precompile_utils::precompile]
#[precompile::test_concrete_types(mock::Runtime, Instance1)]
impl<R, I> ExamplePrecompile<R, I>
where
R: pallet_evm::Config + SomeConfig<I>
{
#[precompile::public("example(bytes)")]
fn example(
handle: &mut impl PrecompileHandle,
data: BoundedBytes<GetMaxSize<R>>,
) -> EvmResult {
todo!("Method implementation")
}
}
```

## Enum functions

The generated enums exposes the following public functions:

- `parse_call_data`: take a `PrecompileHandle` and tries to parse the call data. Returns an
`EvmResult<Self>`. It **DOES NOT** execute the code of the annotated `impl` block.
- `supports_selector`: take a selector as a `u32` is returns if this selector is supported by the
precompile(set) as a `bool`. Note that the presence of a fallback function is not taken into
account.
- `selectors`: returns a static array (`&'static [u32]`) of all the supported selectors.
- For each variant/public function `foo`, there is a function `foo_selectors` which returns a static
array of all the supported selectors **for that function**. That can be used to ensure in tests
that some function have a selector that was computed by hand.
- `encode`: take `self` and encodes it in Solidity format. Additionally, `Vec<u8>` implements
`From<CallEnum>` which simply call encodes. This is useful to write tests as you can construct the
variant you want and it will be encoded to Solidity format for you.
Loading

0 comments on commit 8b0ff04

Please sign in to comment.