Skip to content

Commit

Permalink
[pallet-revive] ecrecover (#7652)
Browse files Browse the repository at this point in the history
Add ECrecover 0x1 precompile and remove the unstable equivalent host
function.

- depend on #7676

---------

Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Alexander Theißen <[email protected]>
  • Loading branch information
3 people authored Feb 26, 2025
1 parent 0c0d892 commit 86019ed
Show file tree
Hide file tree
Showing 13 changed files with 339 additions and 189 deletions.
16 changes: 16 additions & 0 deletions prdoc/pr_7652.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
title: '[pallet-revive] ecrecover'
doc:
- audience: Runtime Dev
description: |-
Add ECrecover 0x1 precompile and remove the unstable equivalent host function.
crates:
- name: asset-hub-westend-runtime
bump: minor
- name: pallet-revive-eth-rpc
bump: minor
- name: pallet-revive
bump: minor
- name: pallet-revive-fixtures
bump: minor
- name: pallet-revive-uapi
bump: minor
2 changes: 1 addition & 1 deletion substrate/frame/revive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ environmental = { workspace = true }
ethabi = { workspace = true }
ethereum-types = { workspace = true, features = ["codec", "rlp", "serialize"] }
hex = { workspace = true }
hex-literal = { workspace = true }
impl-trait-for-tuples = { workspace = true }
log = { workspace = true }
paste = { workspace = true }
Expand Down Expand Up @@ -61,7 +62,6 @@ xcm-builder = { workspace = true }
[dev-dependencies]
array-bytes = { workspace = true, default-features = true }
assert_matches = { workspace = true }
hex-literal = { workspace = true }
pretty_assertions = { workspace = true }
secp256k1 = { workspace = true, features = ["recovery"] }
serde_json = { workspace = true }
Expand Down
57 changes: 57 additions & 0 deletions substrate/frame/revive/fixtures/contracts/call_and_return.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// This file is part of Substrate.

// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! This calls another contract as passed as its account id.
#![no_std]
#![no_main]

use common::{input, u256_bytes};
use uapi::{HostFn, HostFnImpl as api, ReturnErrorCode, ReturnFlags};

#[no_mangle]
#[polkavm_derive::polkavm_export]
pub extern "C" fn deploy() {}

#[no_mangle]
#[polkavm_derive::polkavm_export]
pub extern "C" fn call() {
input!(
256,
callee_addr: &[u8; 20],
value: u64,
callee_input: [u8],
);

// Call the callee
let mut output = [0u8; 32];
let output = &mut &mut output[..];

match api::call(
uapi::CallFlags::empty(),
callee_addr,
u64::MAX, // How much ref_time to devote for the execution. u64::MAX = use all.
u64::MAX, // How much proof_size to devote for the execution. u64::MAX = use all.
&[u8::MAX; 32], // No deposit limit.
&u256_bytes(value), // Value transferred to the contract.
callee_input,
Some(output),
) {
Ok(_) => api::return_value(uapi::ReturnFlags::empty(), output),
Err(ReturnErrorCode::CalleeReverted) => api::return_value(ReturnFlags::REVERT, output),
Err(_) => panic!(),
}
}
44 changes: 0 additions & 44 deletions substrate/frame/revive/fixtures/contracts/ecdsa_recover.rs

This file was deleted.

28 changes: 10 additions & 18 deletions substrate/frame/revive/src/benchmarking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use crate::{
evm::runtime::GAS_PRICE,
exec::{Ext, Key, MomentOf},
limits,
pure_precompiles::Precompile,
storage::WriteOutcome,
ConversionPrecision, Pallet as Contracts, *,
};
Expand Down Expand Up @@ -1945,30 +1946,21 @@ mod benchmarks {
}

#[benchmark(pov_mode = Measured)]
fn seal_ecdsa_recover() {
let message_hash = sp_io::hashing::blake2_256("Hello world".as_bytes());
let key_type = sp_core::crypto::KeyTypeId(*b"code");
let signature = {
let pub_key = sp_io::crypto::ecdsa_generate(key_type, None);
let sig = sp_io::crypto::ecdsa_sign_prehashed(key_type, &pub_key, &message_hash)
.expect("Generates signature");
AsRef::<[u8; 65]>::as_ref(&sig).to_vec()
};

build_runtime!(runtime, memory: [signature, message_hash, [0u8; 33], ]);
fn ecdsa_recover() {
use hex_literal::hex;
let input = hex!("18c547e4f7b0f325ad1e56f57e26c745b09a3e503d86e00e5255ff7f715d3d1c000000000000000000000000000000000000000000000000000000000000001c73b1693892219d736caba55bdb67216e485557ea6b6af75f37096c9aa6a5a75feeb940b1d03b21e36b0e47e79769f095fe2ab855bd91e3a38756b7d75a9c4549");
let expected = hex!("000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b");
let mut call_setup = CallSetup::<T>::default();
let (mut ext, _) = call_setup.ext();

let result;

#[block]
{
result = runtime.bench_ecdsa_recover(
memory.as_mut_slice(),
0, // signature_ptr
65, // message_hash_ptr
65 + 32, // output_ptr
);
result = pure_precompiles::ECRecover::execute(ext.gas_meter_mut(), &input);
}

assert_eq!(result.unwrap(), ReturnErrorCode::Success);
assert_eq!(result.unwrap().data, expected);
}

// Only calling the function itself for the list of
Expand Down
99 changes: 88 additions & 11 deletions substrate/frame/revive/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::{
gas::GasMeter,
limits,
primitives::{ExecReturnValue, StorageDeposit},
pure_precompiles::{self, is_precompile},
runtime_decl_for_revive_api::{Decode, Encode, RuntimeDebugNoBound, TypeInfo},
storage::{self, meter::Diff, WriteOutcome},
tracing::if_tracing,
Expand Down Expand Up @@ -1372,13 +1373,80 @@ where
}
Some(System::<T>::block_hash(&block_number).into())
}
}

/// Determine if the given address is a precompile.
/// For now, we consider that all addresses between 0x1 and 0xff are reserved for precompiles.
fn is_precompile(address: &H160) -> bool {
let bytes = address.as_bytes();
bytes.starts_with(&[0u8; 19]) && bytes[19] != 0
fn run_precompile(
&mut self,
precompile_address: H160,
is_delegate: bool,
is_read_only: bool,
value_transferred: U256,
input_data: &[u8],
) -> Result<(), ExecError> {
if_tracing(|tracer| {
tracer.enter_child_span(
self.caller().account_id().map(T::AddressMapper::to_address).unwrap_or_default(),
precompile_address,
is_delegate,
is_read_only,
value_transferred,
&input_data,
self.gas_meter().gas_left(),
);
});

let mut do_transaction = || -> ExecResult {
if !is_delegate {
Self::transfer_from_origin(
&self.origin,
&self.caller(),
&T::AddressMapper::to_fallback_account_id(&precompile_address),
value_transferred,
)?;
}

pure_precompiles::Precompiles::<T>::execute(
precompile_address,
self.gas_meter_mut(),
input_data,
)
.map_err(|e| ExecError { error: e.error, origin: ErrorOrigin::Callee })
};

let transaction_outcome =
with_transaction(|| -> TransactionOutcome<Result<_, DispatchError>> {
let output = do_transaction();
match &output {
Ok(result) if !result.did_revert() => TransactionOutcome::Commit(Ok(output)),
_ => TransactionOutcome::Rollback(Ok(output)),
}
});

let output = match transaction_outcome {
Ok(output) => {
if_tracing(|tracer| {
let gas_consumed = top_frame!(self).nested_gas.gas_consumed();
match &output {
Ok(output) => tracer.exit_child_span(&output, gas_consumed),
Err(e) => tracer.exit_child_span_with_error(e.error.into(), gas_consumed),
}
});

output
},
Err(error) => {
if_tracing(|tracer| {
let gas_consumed = top_frame!(self).nested_gas.gas_consumed();
tracer.exit_child_span_with_error(error.into(), gas_consumed);
});

Err(error.into())
},
};

output.map(|output| {
self.top_frame_mut().last_frame_output = output;
})
}
}

impl<'a, T, E> Ext for Stack<'a, T, E>
Expand Down Expand Up @@ -1411,9 +1479,11 @@ where
*self.last_frame_output_mut() = Default::default();

let try_call = || {
// Enable read-only access if requested; cannot disable it if already set.
let is_read_only = read_only || self.is_read_only();

if is_precompile(dest_addr) {
log::debug!(target: crate::LOG_TARGET, "Unsupported precompile address {dest_addr:?}");
return Err(Error::<T>::UnsupportedPrecompileAddress.into());
return self.run_precompile(*dest_addr, false, is_read_only, value, &input_data);
}

let dest = T::AddressMapper::to_account_id(dest_addr);
Expand All @@ -1434,9 +1504,6 @@ where
_ => None,
});

// Enable read-only access if requested; cannot disable it if already set.
let is_read_only = read_only || self.is_read_only();

if let Some(executable) = self.push_frame(
FrameArgs::Call { dest: dest.clone(), cached_info, delegated_call: None },
value,
Expand Down Expand Up @@ -1494,6 +1561,16 @@ where
address: H160,
input_data: Vec<u8>,
) -> Result<(), ExecError> {
if is_precompile(&address) {
return self.run_precompile(
address,
true,
self.is_read_only(),
0u32.into(),
&input_data,
);
}

// We reset the return data now, so it is cleared out even if no new frame was executed.
// This is for example the case for unknown code hashes or creating the frame fails.
*self.last_frame_output_mut() = Default::default();
Expand Down
1 change: 1 addition & 0 deletions substrate/frame/revive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ mod exec;
mod gas;
mod limits;
mod primitives;
mod pure_precompiles;
mod storage;
mod transient_storage;
mod wasm;
Expand Down
48 changes: 48 additions & 0 deletions substrate/frame/revive/src/pure_precompiles.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// This file is part of Substrate.

// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::{exec::ExecResult, Config, Error, GasMeter, H160};

mod ecrecover;
pub use ecrecover::*;

/// Determine if the given address is a precompile.
/// For now, we consider that all addresses between 0x1 and 0xff are reserved for precompiles.
pub fn is_precompile(address: &H160) -> bool {
let bytes = address.as_bytes();
bytes.starts_with(&[0u8; 19]) && bytes[19] != 0
}

/// The `Precompile` trait defines the functionality for executing a precompiled contract.
pub trait Precompile<T: Config> {
/// Executes the precompile with the provided input data.
fn execute(gas_meter: &mut GasMeter<T>, input: &[u8]) -> ExecResult;
}

pub struct Precompiles<T: Config> {
_phantom: core::marker::PhantomData<T>,
}

impl<T: Config> Precompiles<T> {
pub fn execute(addr: H160, gas_meter: &mut GasMeter<T>, input: &[u8]) -> ExecResult {
if addr == ECRECOVER {
ECRecover::execute(gas_meter, input)
} else {
Err(Error::<T>::UnsupportedPrecompileAddress.into())
}
}
}
Loading

0 comments on commit 86019ed

Please sign in to comment.