Skip to content
This repository was archived by the owner on Apr 18, 2025. It is now read-only.

Commit 4cd5956

Browse files
roynalnarutolispc
andauthored
Precompile ECRECOVER (#529)
* feat: preliminary work * wip * finish assignment to ecrecover * fix: input length may be different than call data length (for precompiles) * fix: input bytes to ecrecover * minor edits * Fix ecrecover input rlc comparison (right-padding zeroes) (#585) * potential approach (pow of rand lookup) * add lookup for pow of rand * fix: right pad only if needed * fix: missing constraint on padded_rlc * constrain pow of rand table * Update step.rs * fix: sig_v sanity check (remove assertions to allow garbage input) * fix: calldata length == 0 handled * chore: renaming precompile_* and parallel iter for tests --------- Co-authored-by: Zhang Zhuo <mycinbrin@gmail.com>
1 parent f24a795 commit 4cd5956

File tree

22 files changed

+1298
-352
lines changed

22 files changed

+1298
-352
lines changed

bus-mapping/src/circuit_input_builder/execution.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
use std::marker::PhantomData;
44

55
use crate::{
6-
circuit_input_builder::CallContext, error::ExecError, exec_trace::OperationRef,
7-
operation::RWCounter, precompile::PrecompileCalls,
6+
circuit_input_builder::CallContext,
7+
error::ExecError,
8+
exec_trace::OperationRef,
9+
operation::RWCounter,
10+
precompile::{PrecompileAuxData, PrecompileCalls},
811
};
912
use eth_types::{
1013
evm_types::{Gas, GasCost, OpcodeId, ProgramCounter},
@@ -51,6 +54,8 @@ pub struct ExecStep {
5154
pub copy_rw_counter_delta: u64,
5255
/// Error generated by this step
5356
pub error: Option<ExecError>,
57+
/// Optional auxiliary data that is attached to precompile call internal states.
58+
pub aux_data: Option<PrecompileAuxData>,
5459
}
5560

5661
impl ExecStep {
@@ -78,6 +83,7 @@ impl ExecStep {
7883
bus_mapping_instance: Vec::new(),
7984
copy_rw_counter_delta: 0,
8085
error: None,
86+
aux_data: None,
8187
}
8288
}
8389

@@ -113,6 +119,7 @@ impl Default for ExecStep {
113119
bus_mapping_instance: Vec::new(),
114120
copy_rw_counter_delta: 0,
115121
error: None,
122+
aux_data: None,
116123
}
117124
}
118125
}
@@ -212,7 +219,7 @@ impl CopyDataTypeIter {
212219
3usize => Some(CopyDataType::TxCalldata),
213220
4usize => Some(CopyDataType::TxLog),
214221
5usize => Some(CopyDataType::RlcAcc),
215-
6usize => Some(CopyDataType::Precompile(PrecompileCalls::ECRecover)),
222+
6usize => Some(CopyDataType::Precompile(PrecompileCalls::Ecrecover)),
216223
7usize => Some(CopyDataType::Precompile(PrecompileCalls::Sha256)),
217224
8usize => Some(CopyDataType::Precompile(PrecompileCalls::Ripemd160)),
218225
9usize => Some(CopyDataType::Precompile(PrecompileCalls::Identity)),

bus-mapping/src/circuit_input_builder/input_state_ref.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use eth_types::{
2626
evm_types::{
2727
gas_utils::memory_expansion_gas_cost, Gas, GasCost, MemoryAddress, OpcodeId, StackAddress,
2828
},
29+
sign_types::SignData,
2930
Address, Bytecode, GethExecStep, ToAddress, ToBigEndian, ToWord, Word, H256, U256,
3031
};
3132
use ethers_core::utils::{get_contract_address, get_create2_address, keccak256};
@@ -1291,6 +1292,11 @@ impl<'a> CircuitInputStateRef<'a> {
12911292
self.block.add_exp_event(event)
12921293
}
12931294

1295+
/// Push an ecrecover event to the state.
1296+
pub fn push_ecrecover(&mut self, event: SignData) {
1297+
self.block.add_ecrecover_event(event)
1298+
}
1299+
12941300
pub(crate) fn get_step_err(
12951301
&self,
12961302
step: &GethExecStep,

bus-mapping/src/evm/opcodes/callop.rs

Lines changed: 49 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -269,12 +269,6 @@ impl<const N_ARGS: usize> Opcode for CallOpcode<N_ARGS> {
269269
callee_gas_left,
270270
);
271271

272-
log::trace!(
273-
"precompile returned data len {} gas {}",
274-
result.len(),
275-
contract_gas_cost
276-
);
277-
278272
// mutate the caller memory.
279273
let caller_ctx_mut = state.caller_ctx_mut()?;
280274
caller_ctx_mut.return_data = result.clone();
@@ -346,11 +340,16 @@ impl<const N_ARGS: usize> Opcode for CallOpcode<N_ARGS> {
346340

347341
// insert a copy event (input) for this step
348342
let rw_counter_start = state.block_ctx.rwc;
349-
if call.call_data_length > 0 {
343+
let n_input_bytes = if let Some(input_len) = precompile_call.input_len() {
344+
std::cmp::min(input_len, call.call_data_length as usize)
345+
} else {
346+
call.call_data_length as usize
347+
};
348+
let input_bytes = if call.call_data_length > 0 {
350349
let bytes: Vec<(u8, bool)> = caller_memory
351350
.iter()
352351
.skip(call.call_data_offset as usize)
353-
.take(call.call_data_length as usize)
352+
.take(n_input_bytes)
354353
.map(|b| (*b, false))
355354
.collect();
356355
for (i, &(byte, _is_code)) in bytes.iter().enumerate() {
@@ -371,49 +370,57 @@ impl<const N_ARGS: usize> Opcode for CallOpcode<N_ARGS> {
371370
src_id: NumberOrHash::Number(call.caller_id),
372371
src_type: CopyDataType::Memory,
373372
src_addr: call.call_data_offset,
374-
src_addr_end: call.call_data_offset + call.call_data_length,
373+
src_addr_end: call.call_data_offset + n_input_bytes as u64,
375374
dst_id: NumberOrHash::Number(call.call_id),
376375
dst_type: CopyDataType::Precompile(precompile_call),
377376
dst_addr: 0,
378377
log_id: None,
379378
rw_counter_start,
380-
bytes,
379+
bytes: bytes.clone(),
381380
},
382381
);
383-
}
382+
Some(bytes.iter().map(|t| t.0).collect())
383+
} else {
384+
None
385+
};
384386

385387
// write the result in the callee's memory.
386388
let rw_counter_start = state.block_ctx.rwc;
387-
if call.is_success() && call.call_data_length > 0 && !result.is_empty() {
388-
let bytes: Vec<(u8, bool)> = result.iter().map(|b| (*b, false)).collect();
389-
for (i, &(byte, _is_code)) in bytes.iter().enumerate() {
390-
// push callee memory write
391-
state.push_op(
389+
let output_bytes =
390+
if call.is_success() && call.call_data_length > 0 && !result.is_empty() {
391+
let bytes: Vec<(u8, bool)> = result.iter().map(|b| (*b, false)).collect();
392+
for (i, &(byte, _is_code)) in bytes.iter().enumerate() {
393+
// push callee memory write
394+
state.push_op(
395+
&mut exec_step,
396+
RW::WRITE,
397+
MemoryOp::new(call.call_id, i.into(), byte),
398+
);
399+
}
400+
state.push_copy(
392401
&mut exec_step,
393-
RW::WRITE,
394-
MemoryOp::new(call.call_id, i.into(), byte),
402+
CopyEvent {
403+
src_id: NumberOrHash::Number(call.call_id),
404+
src_type: CopyDataType::Precompile(precompile_call),
405+
src_addr: 0,
406+
src_addr_end: result.len() as u64,
407+
dst_id: NumberOrHash::Number(call.call_id),
408+
dst_type: CopyDataType::Memory,
409+
dst_addr: 0,
410+
log_id: None,
411+
rw_counter_start,
412+
bytes: bytes.clone(),
413+
},
395414
);
396-
}
397-
state.push_copy(
398-
&mut exec_step,
399-
CopyEvent {
400-
src_id: NumberOrHash::Number(call.call_id),
401-
src_type: CopyDataType::Precompile(precompile_call),
402-
src_addr: 0,
403-
src_addr_end: result.len() as u64,
404-
dst_id: NumberOrHash::Number(call.call_id),
405-
dst_type: CopyDataType::Memory,
406-
dst_addr: 0,
407-
log_id: None,
408-
rw_counter_start,
409-
bytes,
410-
},
411-
);
412-
}
415+
Some(bytes.iter().map(|t| t.0).collect())
416+
} else {
417+
None
418+
};
413419

414420
// insert another copy event (output) for this step.
415421
let rw_counter_start = state.block_ctx.rwc;
416-
if call.is_success() && call.call_data_length > 0 && length > 0 {
422+
let returned_bytes = if call.is_success() && call.call_data_length > 0 && length > 0
423+
{
417424
let bytes: Vec<(u8, bool)> =
418425
result.iter().take(length).map(|b| (*b, false)).collect();
419426
for (i, &(byte, _is_code)) in bytes.iter().enumerate() {
@@ -440,18 +447,20 @@ impl<const N_ARGS: usize> Opcode for CallOpcode<N_ARGS> {
440447
dst_addr: call.return_data_offset,
441448
log_id: None,
442449
rw_counter_start,
443-
bytes,
450+
bytes: bytes.clone(),
444451
},
445452
);
446-
}
453+
Some(bytes.iter().map(|t| t.0).collect())
454+
} else {
455+
None
456+
};
447457

448-
// TODO: when more precompiles are supported and each have their own different
449-
// behaviour, we can separate out the logic specified here.
450458
let mut precompile_step = precompile_associated_ops(
451459
state,
452460
geth_steps[1].clone(),
453461
call.clone(),
454462
precompile_call,
463+
(input_bytes, output_bytes, returned_bytes),
455464
)?;
456465

457466
// Make the Precompile execution step to handle return logic and restore to caller

bus-mapping/src/evm/opcodes/precompiles/mod.rs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,69 @@
1-
use eth_types::{GethExecStep, ToWord, Word};
1+
use eth_types::{
2+
sign_types::{recover_pk, SignData},
3+
Bytes, GethExecStep, ToBigEndian, ToWord, Word,
4+
};
5+
use halo2_proofs::halo2curves::secp256k1::Fq;
26

37
use crate::{
48
circuit_input_builder::{Call, CircuitInputStateRef, ExecState, ExecStep},
59
operation::CallContextField,
6-
precompile::PrecompileCalls,
10+
precompile::{EcrecoverAuxData, PrecompileAuxData, PrecompileCalls},
711
Error,
812
};
913

14+
type InOutRetData = (Option<Vec<u8>>, Option<Vec<u8>>, Option<Vec<u8>>);
15+
1016
pub fn gen_associated_ops(
1117
state: &mut CircuitInputStateRef,
1218
geth_step: GethExecStep,
1319
call: Call,
1420
precompile: PrecompileCalls,
21+
(input_bytes, output_bytes, _returned_bytes): InOutRetData,
1522
) -> Result<ExecStep, Error> {
1623
assert_eq!(call.code_address(), Some(precompile.into()));
1724
let mut exec_step = state.new_step(&geth_step)?;
1825
exec_step.exec_state = ExecState::Precompile(precompile);
1926

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

29+
// TODO: refactor and replace with `match` once we have more branches.
30+
if precompile == PrecompileCalls::Ecrecover {
31+
let input_bytes = input_bytes.map_or(vec![0u8; 128], |mut bytes| {
32+
bytes.resize(128, 0u8);
33+
bytes
34+
});
35+
let output_bytes = output_bytes.map_or(vec![0u8; 32], |mut bytes| {
36+
bytes.resize(32, 0u8);
37+
bytes
38+
});
39+
let aux_data = EcrecoverAuxData::new(input_bytes, output_bytes);
40+
41+
// only if sig_v was a valid recovery ID, then we proceed to populate the ecrecover events.
42+
if let Some(sig_v) = aux_data.recovery_id() {
43+
if let Ok(recovered_pk) = recover_pk(
44+
sig_v,
45+
&aux_data.sig_r,
46+
&aux_data.sig_s,
47+
&aux_data.msg_hash.to_be_bytes(),
48+
) {
49+
let sign_data = SignData {
50+
signature: (
51+
Fq::from_bytes(&aux_data.sig_r.to_be_bytes()).unwrap(),
52+
Fq::from_bytes(&aux_data.sig_s.to_be_bytes()).unwrap(),
53+
sig_v,
54+
),
55+
pk: recovered_pk,
56+
msg: Bytes::default(),
57+
msg_hash: Fq::from_bytes(&aux_data.msg_hash.to_be_bytes()).unwrap(),
58+
};
59+
assert_eq!(aux_data.recovered_addr, sign_data.get_addr());
60+
state.push_ecrecover(sign_data);
61+
}
62+
}
63+
64+
exec_step.aux_data = Some(PrecompileAuxData::Ecrecover(aux_data));
65+
}
66+
2267
Ok(exec_step)
2368
}
2469

bus-mapping/src/precompile.rs

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! precompile helpers
22
3-
use eth_types::{evm_types::GasCost, Address};
3+
use eth_types::{evm_types::GasCost, Address, ToBigEndian, Word};
44
use revm_precompile::{Precompile, Precompiles};
55
use strum::EnumIter;
66

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

4949
impl Default for PrecompileCalls {
5050
fn default() -> Self {
51-
Self::ECRecover
51+
Self::Ecrecover
5252
}
5353
}
5454

@@ -75,7 +75,7 @@ impl From<PrecompileCalls> for usize {
7575
impl From<u8> for PrecompileCalls {
7676
fn from(value: u8) -> Self {
7777
match value {
78-
0x01 => Self::ECRecover,
78+
0x01 => Self::Ecrecover,
7979
0x02 => Self::Sha256,
8080
0x03 => Self::Ripemd160,
8181
0x04 => Self::Identity,
@@ -93,7 +93,7 @@ impl PrecompileCalls {
9393
/// Get the base gas cost for the precompile call.
9494
pub fn base_gas_cost(&self) -> GasCost {
9595
match self {
96-
Self::ECRecover => GasCost::PRECOMPILE_EC_RECOVER_BASE,
96+
Self::Ecrecover => GasCost::PRECOMPILE_ECRECOVER_BASE,
9797
Self::Sha256 => GasCost::PRECOMPILE_SHA256_BASE,
9898
Self::Ripemd160 => GasCost::PRECOMPILE_RIPEMD160_BASE,
9999
Self::Identity => GasCost::PRECOMPILE_IDENTITY_BASE,
@@ -109,4 +109,72 @@ impl PrecompileCalls {
109109
pub fn address(&self) -> u64 {
110110
(*self).into()
111111
}
112+
113+
/// Maximum length of input bytes considered for the precompile call.
114+
pub fn input_len(&self) -> Option<usize> {
115+
match self {
116+
Self::Ecrecover | Self::Bn128Add => Some(128),
117+
Self::Bn128Mul => Some(96),
118+
_ => None,
119+
}
120+
}
121+
}
122+
123+
/// Auxiliary data for Ecrecover
124+
#[derive(Clone, Debug, Default, PartialEq, Eq)]
125+
pub struct EcrecoverAuxData {
126+
/// Keccak hash of the message being signed.
127+
pub msg_hash: Word,
128+
/// v-component of signature.
129+
pub sig_v: Word,
130+
/// r-component of signature.
131+
pub sig_r: Word,
132+
/// s-component of signature.
133+
pub sig_s: Word,
134+
/// Address that was recovered.
135+
pub recovered_addr: Address,
136+
}
137+
138+
impl EcrecoverAuxData {
139+
/// Create a new instance of ecrecover auxiliary data.
140+
pub fn new(input: Vec<u8>, output: Vec<u8>) -> Self {
141+
assert_eq!(input.len(), 128);
142+
assert_eq!(output.len(), 32);
143+
144+
// assert that recovered address is 20 bytes.
145+
assert!(output[0x00..0x0c].iter().all(|&b| b == 0));
146+
let recovered_addr = Address::from_slice(&output[0x0c..0x20]);
147+
148+
Self {
149+
msg_hash: Word::from_big_endian(&input[0x00..0x20]),
150+
sig_v: Word::from_big_endian(&input[0x20..0x40]),
151+
sig_r: Word::from_big_endian(&input[0x40..0x60]),
152+
sig_s: Word::from_big_endian(&input[0x60..0x80]),
153+
recovered_addr,
154+
}
155+
}
156+
157+
/// Sanity check and returns recovery ID.
158+
pub fn recovery_id(&self) -> Option<u8> {
159+
let sig_v_bytes = self.sig_v.to_be_bytes();
160+
let sig_v = sig_v_bytes[31];
161+
if sig_v_bytes.iter().take(31).all(|&b| b == 0) && (sig_v == 27 || sig_v == 28) {
162+
Some(sig_v - 27)
163+
} else {
164+
None
165+
}
166+
}
167+
}
168+
169+
/// Auxiliary data attached to an internal state for precompile verification.
170+
#[derive(Clone, Debug, PartialEq, Eq)]
171+
pub enum PrecompileAuxData {
172+
/// Ecrecover.
173+
Ecrecover(EcrecoverAuxData),
174+
}
175+
176+
impl Default for PrecompileAuxData {
177+
fn default() -> Self {
178+
Self::Ecrecover(EcrecoverAuxData::default())
179+
}
112180
}

0 commit comments

Comments
 (0)