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

Precompile ECRECOVER #529

Merged
merged 15 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions bus-mapping/src/circuit_input_builder/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
use std::marker::PhantomData;

use crate::{
circuit_input_builder::CallContext, error::ExecError, exec_trace::OperationRef,
operation::RWCounter, precompile::PrecompileCalls,
circuit_input_builder::CallContext,
error::ExecError,
exec_trace::OperationRef,
operation::RWCounter,
precompile::{PrecompileAuxData, PrecompileCalls},
};
use eth_types::{
evm_types::{Gas, GasCost, OpcodeId, ProgramCounter},
Expand Down Expand Up @@ -51,6 +54,8 @@ pub struct ExecStep {
pub copy_rw_counter_delta: u64,
/// Error generated by this step
pub error: Option<ExecError>,
/// Optional auxiliary data that is attached to precompile call internal states.
pub aux_data: Option<PrecompileAuxData>,
}

impl ExecStep {
Expand Down Expand Up @@ -78,6 +83,7 @@ impl ExecStep {
bus_mapping_instance: Vec::new(),
copy_rw_counter_delta: 0,
error: None,
aux_data: None,
}
}

Expand Down Expand Up @@ -113,6 +119,7 @@ impl Default for ExecStep {
bus_mapping_instance: Vec::new(),
copy_rw_counter_delta: 0,
error: None,
aux_data: None,
}
}
}
Expand Down Expand Up @@ -212,7 +219,7 @@ impl CopyDataTypeIter {
3usize => Some(CopyDataType::TxCalldata),
4usize => Some(CopyDataType::TxLog),
5usize => Some(CopyDataType::RlcAcc),
6usize => Some(CopyDataType::Precompile(PrecompileCalls::ECRecover)),
6usize => Some(CopyDataType::Precompile(PrecompileCalls::Ecrecover)),
7usize => Some(CopyDataType::Precompile(PrecompileCalls::Sha256)),
8usize => Some(CopyDataType::Precompile(PrecompileCalls::Ripemd160)),
9usize => Some(CopyDataType::Precompile(PrecompileCalls::Identity)),
Expand Down
6 changes: 6 additions & 0 deletions bus-mapping/src/circuit_input_builder/input_state_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use eth_types::{
evm_types::{
gas_utils::memory_expansion_gas_cost, Gas, GasCost, MemoryAddress, OpcodeId, StackAddress,
},
sign_types::SignData,
Address, Bytecode, GethExecStep, ToAddress, ToBigEndian, ToWord, Word, H256, U256,
};
use ethers_core::utils::{get_contract_address, get_create2_address, keccak256};
Expand Down Expand Up @@ -1291,6 +1292,11 @@ impl<'a> CircuitInputStateRef<'a> {
self.block.add_exp_event(event)
}

/// Push an ecrecover event to the state.
pub fn push_ecrecover(&mut self, event: SignData) {
self.block.add_ecrecover_event(event)
}

pub(crate) fn get_step_err(
&self,
step: &GethExecStep,
Expand Down
91 changes: 54 additions & 37 deletions bus-mapping/src/evm/opcodes/callop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,12 @@ impl<const N_ARGS: usize> Opcode for CallOpcode<N_ARGS> {
callee_gas_left,
);

log::trace!(
"precompile returned data len {} gas {}",
log::info!(
"precompile returned {:?} with len {} and gas {} and is_success {}",
result,
result.len(),
contract_gas_cost
contract_gas_cost,
call.is_success(),
);

// mutate the caller memory.
Expand Down Expand Up @@ -346,11 +348,16 @@ impl<const N_ARGS: usize> Opcode for CallOpcode<N_ARGS> {

// insert a copy event (input) for this step
let rw_counter_start = state.block_ctx.rwc;
if call.call_data_length > 0 {
let n_input_bytes = if let Some(input_len) = precompile_call.input_len() {
roynalnaruto marked this conversation as resolved.
Show resolved Hide resolved
std::cmp::min(input_len, call.call_data_length as usize)
} else {
call.call_data_length as usize
};
let input_bytes = if call.call_data_length > 0 {
let bytes: Vec<(u8, bool)> = caller_memory
.iter()
.skip(call.call_data_offset as usize)
.take(call.call_data_length as usize)
.take(n_input_bytes)
.map(|b| (*b, false))
.collect();
for (i, &(byte, _is_code)) in bytes.iter().enumerate() {
Expand All @@ -371,49 +378,57 @@ impl<const N_ARGS: usize> Opcode for CallOpcode<N_ARGS> {
src_id: NumberOrHash::Number(call.caller_id),
src_type: CopyDataType::Memory,
src_addr: call.call_data_offset,
src_addr_end: call.call_data_offset + call.call_data_length,
src_addr_end: call.call_data_offset + n_input_bytes as u64,
dst_id: NumberOrHash::Number(call.call_id),
dst_type: CopyDataType::Precompile(precompile_call),
dst_addr: 0,
log_id: None,
rw_counter_start,
bytes,
bytes: bytes.clone(),
},
);
}
Some(bytes.iter().map(|t| t.0).collect())
} else {
None
};

// write the result in the callee's memory.
let rw_counter_start = state.block_ctx.rwc;
if call.is_success() && call.call_data_length > 0 && !result.is_empty() {
let bytes: Vec<(u8, bool)> = result.iter().map(|b| (*b, false)).collect();
for (i, &(byte, _is_code)) in bytes.iter().enumerate() {
// push callee memory write
state.push_op(
let output_bytes =
if call.is_success() && call.call_data_length > 0 && !result.is_empty() {
let bytes: Vec<(u8, bool)> = result.iter().map(|b| (*b, false)).collect();
for (i, &(byte, _is_code)) in bytes.iter().enumerate() {
// push callee memory write
state.push_op(
&mut exec_step,
RW::WRITE,
MemoryOp::new(call.call_id, i.into(), byte),
);
}
state.push_copy(
&mut exec_step,
RW::WRITE,
MemoryOp::new(call.call_id, i.into(), byte),
CopyEvent {
src_id: NumberOrHash::Number(call.call_id),
src_type: CopyDataType::Precompile(precompile_call),
src_addr: 0,
src_addr_end: result.len() as u64,
dst_id: NumberOrHash::Number(call.call_id),
dst_type: CopyDataType::Memory,
dst_addr: 0,
log_id: None,
rw_counter_start,
bytes: bytes.clone(),
},
);
}
state.push_copy(
&mut exec_step,
CopyEvent {
src_id: NumberOrHash::Number(call.call_id),
src_type: CopyDataType::Precompile(precompile_call),
src_addr: 0,
src_addr_end: result.len() as u64,
dst_id: NumberOrHash::Number(call.call_id),
dst_type: CopyDataType::Memory,
dst_addr: 0,
log_id: None,
rw_counter_start,
bytes,
},
);
}
Some(bytes.iter().map(|t| t.0).collect())
} else {
None
};

// insert another copy event (output) for this step.
let rw_counter_start = state.block_ctx.rwc;
if call.is_success() && call.call_data_length > 0 && length > 0 {
let returned_bytes = if call.is_success() && call.call_data_length > 0 && length > 0
{
let bytes: Vec<(u8, bool)> =
result.iter().take(length).map(|b| (*b, false)).collect();
for (i, &(byte, _is_code)) in bytes.iter().enumerate() {
Expand All @@ -440,18 +455,20 @@ impl<const N_ARGS: usize> Opcode for CallOpcode<N_ARGS> {
dst_addr: call.return_data_offset,
log_id: None,
rw_counter_start,
bytes,
bytes: bytes.clone(),
},
);
}
Some(bytes.iter().map(|t| t.0).collect())
} else {
None
};

// TODO: when more precompiles are supported and each have their own different
// behaviour, we can separate out the logic specified here.
let mut precompile_step = precompile_associated_ops(
state,
geth_steps[1].clone(),
call.clone(),
precompile_call,
(input_bytes, output_bytes, returned_bytes),
)?;

// Make the Precompile execution step to handle return logic and restore to caller
Expand Down
46 changes: 44 additions & 2 deletions bus-mapping/src/evm/opcodes/precompiles/mod.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,66 @@
use eth_types::{GethExecStep, ToWord, Word};
use eth_types::{
sign_types::{recover_pk, SignData},
Bytes, GethExecStep, ToBigEndian, ToWord, Word,
};
use halo2_proofs::halo2curves::secp256k1::Fq;

use crate::{
circuit_input_builder::{Call, CircuitInputStateRef, ExecState, ExecStep},
operation::CallContextField,
precompile::PrecompileCalls,
precompile::{EcrecoverAuxData, PrecompileAuxData, PrecompileCalls},
Error,
};

type InOutRetData = (Option<Vec<u8>>, Option<Vec<u8>>, Option<Vec<u8>>);

pub fn gen_associated_ops(
state: &mut CircuitInputStateRef,
geth_step: GethExecStep,
call: Call,
precompile: PrecompileCalls,
(input_bytes, output_bytes, _returned_bytes): InOutRetData,
) -> Result<ExecStep, Error> {
assert_eq!(call.code_address(), Some(precompile.into()));
let mut exec_step = state.new_step(&geth_step)?;
exec_step.exec_state = ExecState::Precompile(precompile);

common_call_ctx_reads(state, &mut exec_step, &call);

// TODO: refactor and replace with `match` once we have more branches.
if precompile == PrecompileCalls::Ecrecover {
let input_bytes = input_bytes.map_or(vec![0u8; 128], |mut bytes| {
bytes.resize(128, 0u8);
bytes
});
let output_bytes = output_bytes.map_or(vec![0u8; 32], |mut bytes| {
bytes.resize(32, 0u8);
bytes
});
let aux_data = EcrecoverAuxData::new(input_bytes, output_bytes);

if let Ok(recovered_pk) = recover_pk(
aux_data.sig_v,
&aux_data.sig_r,
&aux_data.sig_s,
&aux_data.msg_hash.to_be_bytes(),
) {
let sign_data = SignData {
signature: (
Fq::from_bytes(&aux_data.sig_r.to_be_bytes()).unwrap(),
Fq::from_bytes(&aux_data.sig_s.to_be_bytes()).unwrap(),
aux_data.sig_v,
),
pk: recovered_pk,
msg: Bytes::default(),
msg_hash: Fq::from_bytes(&aux_data.msg_hash.to_be_bytes()).unwrap(),
};
assert_eq!(aux_data.recovered_addr, sign_data.get_addr());
state.push_ecrecover(sign_data);
}

exec_step.aux_data = Some(PrecompileAuxData::Ecrecover(aux_data));
}

Ok(exec_step)
}

Expand Down
72 changes: 67 additions & 5 deletions bus-mapping/src/precompile.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! precompile helpers

use eth_types::{evm_types::GasCost, Address};
use eth_types::{evm_types::GasCost, Address, Word};
use revm_precompile::{Precompile, Precompiles};
use strum::EnumIter;

Expand All @@ -27,7 +27,7 @@ pub(crate) fn execute_precompiled(address: &Address, input: &[u8], gas: u64) ->
#[derive(Copy, Clone, Debug, Eq, PartialEq, EnumIter)]
pub enum PrecompileCalls {
/// Elliptic Curve Recovery
ECRecover = 0x01,
Ecrecover = 0x01,
/// SHA2-256 hash function
Sha256 = 0x02,
/// Ripemd-160 hash function
Expand All @@ -48,7 +48,7 @@ pub enum PrecompileCalls {

impl Default for PrecompileCalls {
fn default() -> Self {
Self::ECRecover
Self::Ecrecover
}
}

Expand All @@ -75,7 +75,7 @@ impl From<PrecompileCalls> for usize {
impl From<u8> for PrecompileCalls {
fn from(value: u8) -> Self {
match value {
0x01 => Self::ECRecover,
0x01 => Self::Ecrecover,
0x02 => Self::Sha256,
0x03 => Self::Ripemd160,
0x04 => Self::Identity,
Expand All @@ -93,7 +93,7 @@ impl PrecompileCalls {
/// Get the base gas cost for the precompile call.
pub fn base_gas_cost(&self) -> GasCost {
match self {
Self::ECRecover => GasCost::PRECOMPILE_EC_RECOVER_BASE,
Self::Ecrecover => GasCost::PRECOMPILE_ECRECOVER_BASE,
Self::Sha256 => GasCost::PRECOMPILE_SHA256_BASE,
Self::Ripemd160 => GasCost::PRECOMPILE_RIPEMD160_BASE,
Self::Identity => GasCost::PRECOMPILE_IDENTITY_BASE,
Expand All @@ -109,4 +109,66 @@ impl PrecompileCalls {
pub fn address(&self) -> u64 {
(*self).into()
}

/// Maximum length of input bytes considered for the precompile call.
pub fn input_len(&self) -> Option<usize> {
match self {
Self::Ecrecover | Self::Bn128Add => Some(128),
Self::Bn128Mul => Some(96),
Self::Blake2F => Some(213),
_ => None,
}
}
}

/// Auxiliary data for Ecrecover
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EcrecoverAuxData {
/// Keccak hash of the message being signed.
pub msg_hash: Word,
/// v-component of signature.
pub sig_v: u8,
/// r-component of signature.
pub sig_r: Word,
/// s-component of signature.
pub sig_s: Word,
/// Address that was recovered.
pub recovered_addr: Address,
}

impl EcrecoverAuxData {
/// Create a new instance of ecrecover auxiliary data.
pub fn new(input: Vec<u8>, output: Vec<u8>) -> Self {
assert_eq!(input.len(), 128);
assert_eq!(output.len(), 32);

// assert that sig v is a byte, which indirectly means the other 31 bytes are 0.
assert!(input[0x20..0x3f].iter().all(|&b| b == 0));
let sig_v = input[0x3f] - 27;

// assert that recovered address is 20 bytes.
assert!(output[0x00..0x0c].iter().all(|&b| b == 0));
let recovered_addr = Address::from_slice(&output[0x0c..0x20]);

Self {
msg_hash: Word::from_big_endian(&input[0x00..0x20]),
sig_v,
sig_r: Word::from_big_endian(&input[0x40..0x60]),
sig_s: Word::from_big_endian(&input[0x60..0x80]),
recovered_addr,
}
}
}

/// Auxiliary data attached to an internal state for precompile verification.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PrecompileAuxData {
/// Ecrecover.
Ecrecover(EcrecoverAuxData),
}

impl Default for PrecompileAuxData {
fn default() -> Self {
Self::Ecrecover(EcrecoverAuxData::default())
}
}
Loading