Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moves precompile-utils from moonbeam into frontier. #1150

Merged
merged 15 commits into from
Aug 29, 2023
1,875 changes: 1,094 additions & 781 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",
"utils/precompiles",
"utils/precompiles/macro",
"utils/precompiles/tests-external",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
"utils/precompiles",
"utils/precompiles/macro",
"utils/precompiles/tests-external",
"precompiles",
"precompiles/macro",
"precompiles/tests-external",

This utils path seems unnecessary

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

]
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,22 @@ 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.19", default-features = false }
num_enum = { version = "0.5.3", default-features = false }
rimbi marked this conversation as resolved.
Show resolved Hide resolved
parity-db = "0.4.10"
parking_lot = "0.12.1"
paste = "1.0.6"
rimbi marked this conversation as resolved.
Show resolved Hide resolved
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"
sha3 = { version = "0.10", default-features = false }
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
sha3 = { version = "0.10", default-features = false }

we could use sp_core::hashing directly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

similar-asserts = "1.1.0"
sqlx = { version = "0.7.1", default-features = false, features = ["macros"] }
thiserror = "1.0"
tokio = "1.29.1"
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 = "utils/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
55 changes: 55 additions & 0 deletions utils/precompiles/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
[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 }
paste = { workspace = true }
scale-info = { workspace = true, optional = true, features = ["derive"] }
serde = { workspace = true, optional = true }
sha3 = { workspace = 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 utils/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 = { version = "0.5.3", default-features = false }
prettyplease = "0.1.18"
proc-macro2 = "1.0"
quote = "1.0"
sha3 = "0.10"
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 = { workspace = true }
sp-std = { workspace = true }
199 changes: 199 additions & 0 deletions utils/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
Loading