From e19d1637c45dce2e269320b97a28b9cbac188a72 Mon Sep 17 00:00:00 2001 From: Kimi Wu Date: Mon, 19 Feb 2024 17:58:10 +0800 Subject: [PATCH] feat/#1665 Precompile ECRECOVER (#1720) closed #1665 This PR was ported from - https://github.com/scroll-tech/zkevm-circuits/pull/529 - https://github.com/scroll-tech/zkevm-circuits/pull/930 which manly includes, 1. the main logic in ecrecover gadget (`ecrecover.rs`) 2. signature verifcation circuiit (`sig_circuit.rs`). What I did is to change rlc to word lo/hi. 3. a new table, `sig_table.rs` 4. ecc circuit (`ecc_circuit.rs`). It's not be used in `ecRecover`, but it was implemented in Scroll's PR. If I removed ecc_circuit, it would be inconvienent for people porting `ecAdd`, `ecMul` or `ecPairing`. That's why I keep it here. 5. dependencies update, using `halo2lib` (includes `halo2-base` and `halo2-ecc`) --------- Co-authored-by: Rohit Narurkar Co-authored-by: Zhang Zhuo --- .github/workflows/main-tests.yml | 2 +- Cargo.lock | 49 + Cargo.toml | 6 + bus-mapping/Cargo.toml | 2 + bus-mapping/src/circuit_input_builder.rs | 8 + .../src/circuit_input_builder/block.rs | 11 +- .../src/circuit_input_builder/execution.rs | 48 +- .../circuit_input_builder/input_state_ref.rs | 8 +- .../src/circuit_input_builder/transaction.rs | 12 +- bus-mapping/src/evm/opcodes/callop.rs | 24 +- .../src/evm/opcodes/precompiles/ecrecover.rs | 60 + .../src/evm/opcodes/precompiles/mod.rs | 38 +- bus-mapping/src/precompile.rs | 95 +- circuit-benchmarks/src/super_circuit.rs | 1 + eth-types/src/geth_types.rs | 21 +- eth-types/src/sign_types.rs | 39 +- .../src/integration_test_circuits.rs | 3 + .../tests/circuit_input_builder.rs | 1 + testool/src/statetest/executor.rs | 2 + zkevm-circuits/Cargo.toml | 3 + zkevm-circuits/src/ecc_circuit.rs | 1377 +++++++++++++++++ zkevm-circuits/src/ecc_circuit/dev.rs | 46 + zkevm-circuits/src/ecc_circuit/test.rs | 620 ++++++++ zkevm-circuits/src/ecc_circuit/util.rs | 107 ++ zkevm-circuits/src/evm_circuit.rs | 14 +- zkevm-circuits/src/evm_circuit/execution.rs | 16 +- .../execution/error_oog_precompile.rs | 13 +- .../execution/precompiles/ecrecover.rs | 575 +++++++ .../evm_circuit/execution/precompiles/mod.rs | 3 + zkevm-circuits/src/evm_circuit/param.rs | 7 +- zkevm-circuits/src/evm_circuit/step.rs | 10 +- zkevm-circuits/src/evm_circuit/table.rs | 33 +- .../evm_circuit/util/constraint_builder.rs | 25 +- .../src/evm_circuit/util/instrumentation.rs | 4 + .../src/evm_circuit/util/precompile_gadget.rs | 16 +- zkevm-circuits/src/lib.rs | 1 + zkevm-circuits/src/root_circuit/test.rs | 1 + zkevm-circuits/src/sig_circuit.rs | 1049 +++++++++++++ zkevm-circuits/src/sig_circuit/dev.rs | 64 + zkevm-circuits/src/sig_circuit/ecdsa.rs | 226 +++ zkevm-circuits/src/sig_circuit/test.rs | 267 ++++ zkevm-circuits/src/sig_circuit/utils.rs | 115 ++ zkevm-circuits/src/super_circuit.rs | 6 +- zkevm-circuits/src/super_circuit/test.rs | 3 + zkevm-circuits/src/table.rs | 3 + zkevm-circuits/src/table/sig_table.rs | 131 ++ zkevm-circuits/src/tx_circuit/sign_verify.rs | 8 +- zkevm-circuits/src/witness/block.rs | 29 +- 48 files changed, 5143 insertions(+), 59 deletions(-) create mode 100644 bus-mapping/src/evm/opcodes/precompiles/ecrecover.rs create mode 100644 zkevm-circuits/src/ecc_circuit.rs create mode 100644 zkevm-circuits/src/ecc_circuit/dev.rs create mode 100644 zkevm-circuits/src/ecc_circuit/test.rs create mode 100644 zkevm-circuits/src/ecc_circuit/util.rs create mode 100644 zkevm-circuits/src/evm_circuit/execution/precompiles/ecrecover.rs create mode 100644 zkevm-circuits/src/sig_circuit.rs create mode 100644 zkevm-circuits/src/sig_circuit/dev.rs create mode 100644 zkevm-circuits/src/sig_circuit/ecdsa.rs create mode 100644 zkevm-circuits/src/sig_circuit/test.rs create mode 100644 zkevm-circuits/src/sig_circuit/utils.rs create mode 100644 zkevm-circuits/src/table/sig_table.rs diff --git a/.github/workflows/main-tests.yml b/.github/workflows/main-tests.yml index 5a4826347a..4fb06d1367 100644 --- a/.github/workflows/main-tests.yml +++ b/.github/workflows/main-tests.yml @@ -79,7 +79,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --verbose --release --all --all-features --exclude integration-tests --exclude circuit-benchmarks + args: --verbose --release --all --all-features --exclude integration-tests --exclude circuit-benchmarks -- --test-threads 24 - name: Run testool internal tests uses: actions-rs/cargo@v1 with: diff --git a/Cargo.lock b/Cargo.lock index ab96dd69af..5f6781d1d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -357,7 +357,9 @@ dependencies = [ "lazy_static", "log", "mock", + "num", "pretty_assertions", + "rand", "revm-precompile", "serde", "serde_json", @@ -1787,6 +1789,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "halo2-base" +version = "0.2.2" +source = "git+https://github.com/scroll-tech/halo2-lib?branch=develop#40ba7e3bbf013b55c59283534c9489701f9212d0" +dependencies = [ + "ff", + "halo2_proofs", + "itertools 0.10.5", + "num-bigint", + "num-integer", + "num-traits", + "rand_chacha", + "rustc-hash", +] + +[[package]] +name = "halo2-ecc" +version = "0.2.2" +source = "git+https://github.com/scroll-tech/halo2-lib?branch=develop#40ba7e3bbf013b55c59283534c9489701f9212d0" +dependencies = [ + "ff", + "group", + "halo2-base", + "itertools 0.10.5", + "num-bigint", + "num-integer", + "num-traits", + "rand", + "rand_chacha", + "rand_core", + "serde", + "serde_json", +] + [[package]] name = "halo2_proofs" version = "0.3.0" @@ -3465,6 +3501,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hex" version = "2.1.0" @@ -4964,6 +5006,8 @@ dependencies = [ "ethers-signers", "gadgets", "getrandom", + "halo2-base", + "halo2-ecc", "halo2_proofs", "hex", "integer", @@ -5017,3 +5061,8 @@ dependencies = [ "cc", "pkg-config", ] + +[[patch.unused]] +name = "halo2curves" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml index 00da3baf01..0e88d667b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,12 @@ members = [ [patch.crates-io] halo2_proofs = { git = "https://github.com/privacy-scaling-explorations/halo2.git", tag = "v0.3.0" } +[patch."https://github.com/scroll-tech/halo2.git"] +halo2_proofs = { git = "https://github.com/privacy-scaling-explorations/halo2.git", tag = "v0.3.0" } + +[patch."https://github.com/privacy-scaling-explorations/halo2curves.git"] +halo2curves = { version = "0.1.0", features = ["derive_serde"] } + # Definition of benchmarks profile to use. [profile.bench] opt-level = 3 diff --git a/bus-mapping/Cargo.toml b/bus-mapping/Cargo.toml index d58111b309..bf8dd08b25 100644 --- a/bus-mapping/Cargo.toml +++ b/bus-mapping/Cargo.toml @@ -16,6 +16,8 @@ halo2_proofs = { git = "https://github.com/privacy-scaling-explorations/halo2.gi itertools = "0.10" lazy_static = "1.4" log = "0.4.14" +num = "0.4" +rand = { version = "0.8", optional = true } serde = {version = "1.0.130", features = ["derive"] } serde_json = "1.0.66" strum = "0.24" diff --git a/bus-mapping/src/circuit_input_builder.rs b/bus-mapping/src/circuit_input_builder.rs index 6861a870ae..b8ea23f419 100644 --- a/bus-mapping/src/circuit_input_builder.rs +++ b/bus-mapping/src/circuit_input_builder.rs @@ -31,6 +31,7 @@ use eth_types::{ use ethers_providers::JsonRpcClient; pub use execution::{ CopyDataType, CopyEvent, CopyStep, ExecState, ExecStep, ExpEvent, ExpStep, NumberOrHash, + PrecompileEvent, PrecompileEvents, N_BYTES_PER_PAIR, N_PAIRING_PER_OP, }; pub use input_state_ref::CircuitInputStateRef; use itertools::Itertools; @@ -112,6 +113,11 @@ pub struct FixedCParams { /// calculated, so the same circuit will not be able to prove different /// witnesses. pub max_keccak_rows: usize, + /// This number indicate what 100% usage means, for example if we can support up to 2 + /// ecPairing inside circuit, and max_vertical_circuit_rows is set to 1_000_000, + /// then if there is 1 ecPairing in the input, we will return 500_000 as the "row usage" + /// for the ec circuit. + pub max_vertical_circuit_rows: usize, } /// Unset Circuits Parameters @@ -153,6 +159,7 @@ impl Default for FixedCParams { max_bytecode: 512, max_evm_rows: 0, max_keccak_rows: 0, + max_vertical_circuit_rows: 0, } } } @@ -497,6 +504,7 @@ impl CircuitInputBuilder { max_bytecode, max_evm_rows, max_keccak_rows, + max_vertical_circuit_rows: 0, } }; let mut cib = CircuitInputBuilder:: { diff --git a/bus-mapping/src/circuit_input_builder/block.rs b/bus-mapping/src/circuit_input_builder/block.rs index d2d97f4a0b..655eb8c073 100644 --- a/bus-mapping/src/circuit_input_builder/block.rs +++ b/bus-mapping/src/circuit_input_builder/block.rs @@ -1,7 +1,8 @@ //! Block-related utility module use super::{ - execution::ExecState, transaction::Transaction, CopyEvent, ExecStep, ExpEvent, Withdrawal, + execution::ExecState, transaction::Transaction, CopyEvent, ExecStep, ExpEvent, PrecompileEvent, + PrecompileEvents, Withdrawal, }; use crate::{ operation::{OperationContainer, RWCounter}, @@ -87,6 +88,8 @@ pub struct Block { pub sha3_inputs: Vec>, /// Exponentiation events in the block. pub exp_events: Vec, + /// IO to/from the precompiled contract calls. + pub precompile_events: PrecompileEvents, /// Original block from geth pub eth_block: eth_types::Block, } @@ -145,6 +148,7 @@ impl Block { copy_events: Vec::new(), exp_events: Vec::new(), sha3_inputs: Vec::new(), + precompile_events: PrecompileEvents { events: Vec::new() }, eth_block: eth_block.clone(), }) } @@ -191,4 +195,9 @@ impl Block { pub fn add_exp_event(&mut self, event: ExpEvent) { self.exp_events.push(event); } + + /// Push a precompile event to the block. + pub fn add_precompile_event(&mut self, event: PrecompileEvent) { + self.precompile_events.events.push(event); + } } diff --git a/bus-mapping/src/circuit_input_builder/execution.rs b/bus-mapping/src/circuit_input_builder/execution.rs index f23e80f7f2..045ba2e44b 100644 --- a/bus-mapping/src/circuit_input_builder/execution.rs +++ b/bus-mapping/src/circuit_input_builder/execution.rs @@ -5,9 +5,9 @@ use crate::{ error::{ExecError, OogError}, exec_trace::OperationRef, operation::RWCounter, - precompile::PrecompileCalls, + precompile::{PrecompileAuxData, PrecompileCalls}, }; -use eth_types::{evm_types::OpcodeId, GethExecStep, Word, H256}; +use eth_types::{evm_types::OpcodeId, sign_types::SignData, GethExecStep, Word, H256}; use gadgets::impl_expr; use halo2_proofs::plonk::Expression; use strum_macros::EnumIter; @@ -49,6 +49,8 @@ pub struct ExecStep { pub copy_rw_counter_delta: u64, /// Error generated by this step pub error: Option, + /// Optional auxiliary data that is attached to precompile call internal states. + pub aux_data: Option, } impl ExecStep { @@ -77,6 +79,7 @@ impl ExecStep { bus_mapping_instance: Vec::new(), copy_rw_counter_delta: 0, error: None, + aux_data: None, } } @@ -364,3 +367,44 @@ impl Default for ExpEvent { } } } + +/// I/Os from all precompiled contract calls in a block. +#[derive(Clone, Debug, Default)] +pub struct PrecompileEvents { + /// All events. + pub events: Vec, +} + +impl PrecompileEvents { + /// Get all ecrecover events. + pub fn get_ecrecover_events(&self) -> Vec { + self.events + .iter() + .map(|e| { + let PrecompileEvent::Ecrecover(sign_data) = e; + sign_data + }) + .cloned() + .collect() + } +} + +/// I/O from a precompiled contract call. +#[derive(Clone, Debug)] +pub enum PrecompileEvent { + /// Represents the I/O from Ecrecover call. + Ecrecover(SignData), +} + +impl Default for PrecompileEvent { + fn default() -> Self { + Self::Ecrecover(SignData::default()) + } +} + +/// The number of pairing inputs per pairing operation. If the inputs provided to the precompile +/// call are < 4, we append (G1::infinity, G2::generator) until we have the required no. of inputs. +pub const N_PAIRING_PER_OP: usize = 4; + +/// The number of bytes taken to represent a pair (G1, G2). +pub const N_BYTES_PER_PAIR: usize = 192; diff --git a/bus-mapping/src/circuit_input_builder/input_state_ref.rs b/bus-mapping/src/circuit_input_builder/input_state_ref.rs index 3b5b74c1e8..09b8089ef1 100644 --- a/bus-mapping/src/circuit_input_builder/input_state_ref.rs +++ b/bus-mapping/src/circuit_input_builder/input_state_ref.rs @@ -2,7 +2,7 @@ use super::{ get_call_memory_offset_length, get_create_init_code, Block, BlockContext, Call, CallContext, - CallKind, CodeSource, CopyEvent, ExecState, ExecStep, ExpEvent, Transaction, + CallKind, CodeSource, CopyEvent, ExecState, ExecStep, ExpEvent, PrecompileEvent, Transaction, TransactionContext, }; use crate::{ @@ -1404,6 +1404,11 @@ impl<'a> CircuitInputStateRef<'a> { self.block.add_exp_event(event) } + /// Push an event representing auxiliary data for a precompile call to the state. + pub fn push_precompile_event(&mut self, event: PrecompileEvent) { + self.block.add_precompile_event(event) + } + pub(crate) fn get_step_err( &self, step: &GethExecStep, @@ -1614,7 +1619,6 @@ impl<'a> CircuitInputStateRef<'a> { PrecompileCalls::Sha256 | PrecompileCalls::Ripemd160 | PrecompileCalls::Blake2F - | PrecompileCalls::ECRecover | PrecompileCalls::Bn128Add | PrecompileCalls::Bn128Mul | PrecompileCalls::Bn128Pairing diff --git a/bus-mapping/src/circuit_input_builder/transaction.rs b/bus-mapping/src/circuit_input_builder/transaction.rs index 9b86e197e1..6ca84d27ce 100644 --- a/bus-mapping/src/circuit_input_builder/transaction.rs +++ b/bus-mapping/src/circuit_input_builder/transaction.rs @@ -182,7 +182,7 @@ pub struct Transaction { /// The transaction id pub id: u64, /// The raw transaction fields - tx: geth_types::Transaction, + pub tx: geth_types::Transaction, /// Calls made in the transaction pub(crate) calls: Vec, /// Execution steps @@ -190,6 +190,16 @@ pub struct Transaction { } impl Transaction { + /// Create a dummy Transaction with zero values + pub fn dummy() -> Self { + Self { + id: 0, + calls: Vec::new(), + steps: Vec::new(), + tx: geth_types::Transaction::dummy(), + } + } + /// Create a new Self. pub fn new( id: u64, diff --git a/bus-mapping/src/evm/opcodes/callop.rs b/bus-mapping/src/evm/opcodes/callop.rs index 316bfe454d..350915302a 100644 --- a/bus-mapping/src/evm/opcodes/callop.rs +++ b/bus-mapping/src/evm/opcodes/callop.rs @@ -362,7 +362,7 @@ impl Opcode for CallOpcode { // insert a copy event (input) for this step and generate memory op let rw_counter_start = state.block_ctx.rwc; - if call.call_data_length > 0 { + let input_bytes = if call.call_data_length > 0 { let n_input_bytes = if let Some(input_len) = precompile_call.input_len() { min(input_len, call.call_data_length as usize) } else { @@ -390,11 +390,14 @@ impl Opcode for CallOpcode { bytes: input_bytes.iter().map(|s| (*s, false)).collect(), }, ); - } + Some(input_bytes) + } else { + None + }; // write the result in the callee's memory let rw_counter_start = state.block_ctx.rwc; - if call.is_success && !result.is_empty() { + let output_bytes = if call.is_success && !result.is_empty() { let (output_bytes, _prev_bytes) = state .gen_copy_steps_for_precompile_callee_memory(&mut exec_step, &result)?; @@ -413,11 +416,14 @@ impl Opcode for CallOpcode { bytes: output_bytes.iter().map(|s| (*s, false)).collect(), }, ); - } + Some(output_bytes) + } else { + None + }; // insert another copy event (output) for this step. let rw_counter_start = state.block_ctx.rwc; - if call.is_success && length > 0 { + let return_bytes = if call.is_success && length > 0 { let return_bytes = state.gen_copy_steps_for_precompile_returndata( &mut exec_step, call.return_data_offset, @@ -439,7 +445,10 @@ impl Opcode for CallOpcode { bytes: return_bytes.iter().map(|s| (*s, false)).collect(), }, ); - } + Some(return_bytes) + } else { + None + }; if has_oog_err { let mut oog_step = ErrorOOGPrecompile::gen_associated_ops( @@ -462,6 +471,9 @@ impl Opcode for CallOpcode { geth_steps[1].clone(), call.clone(), precompile_call, + &input_bytes.unwrap_or_default(), + &output_bytes.unwrap_or_default(), + &return_bytes.unwrap_or_default(), )?; // Set gas left and gas cost for precompile step. diff --git a/bus-mapping/src/evm/opcodes/precompiles/ecrecover.rs b/bus-mapping/src/evm/opcodes/precompiles/ecrecover.rs new file mode 100644 index 0000000000..6bcf5f36ca --- /dev/null +++ b/bus-mapping/src/evm/opcodes/precompiles/ecrecover.rs @@ -0,0 +1,60 @@ +use eth_types::{ + sign_types::{biguint_to_32bytes_le, recover_pk, SignData, SECP256K1_Q}, + ToBigEndian, ToLittleEndian, +}; +use halo2_proofs::halo2curves::{ + group::{ff::PrimeField, prime::PrimeCurveAffine}, + secp256k1::{Fq, Secp256k1Affine}, +}; +use num::{BigUint, Integer}; + +use crate::{ + circuit_input_builder::PrecompileEvent, + precompile::{EcrecoverAuxData, PrecompileAuxData}, +}; + +pub(crate) fn opt_data( + input_bytes: &[u8], + output_bytes: &[u8], + return_bytes: &[u8], +) -> (Option, Option) { + let aux_data = EcrecoverAuxData::new(input_bytes, output_bytes, return_bytes); + + // We skip the validation through sig circuit if r or s was not in canonical form. + let opt_sig_r: Option = Fq::from_bytes(&aux_data.sig_r.to_le_bytes()).into(); + let opt_sig_s: Option = Fq::from_bytes(&aux_data.sig_s.to_le_bytes()).into(); + if opt_sig_r.zip(opt_sig_s).is_none() { + return (None, Some(PrecompileAuxData::Ecrecover(aux_data))); + } + + if let Some(sig_v) = aux_data.recovery_id() { + let recovered_pk = recover_pk( + sig_v, + &aux_data.sig_r, + &aux_data.sig_s, + &aux_data.msg_hash.to_be_bytes(), + ) + .unwrap_or(Secp256k1Affine::identity()); + let sign_data = SignData { + signature: ( + Fq::from_bytes(&aux_data.sig_r.to_le_bytes()).unwrap(), + Fq::from_bytes(&aux_data.sig_s.to_le_bytes()).unwrap(), + sig_v, + ), + pk: recovered_pk, + msg: aux_data.input_bytes.clone().into(), + msg_hash: { + let msg_hash = BigUint::from_bytes_be(&aux_data.msg_hash.to_be_bytes()); + let msg_hash = msg_hash.mod_floor(&*SECP256K1_Q); + let msg_hash_le = biguint_to_32bytes_le(msg_hash); + Fq::from_repr(msg_hash_le).unwrap() + }, + }; + ( + Some(PrecompileEvent::Ecrecover(sign_data)), + Some(PrecompileAuxData::Ecrecover(aux_data)), + ) + } else { + (None, Some(PrecompileAuxData::Ecrecover(aux_data))) + } +} diff --git a/bus-mapping/src/evm/opcodes/precompiles/mod.rs b/bus-mapping/src/evm/opcodes/precompiles/mod.rs index 70e62ce54c..ac16d21f6b 100644 --- a/bus-mapping/src/evm/opcodes/precompiles/mod.rs +++ b/bus-mapping/src/evm/opcodes/precompiles/mod.rs @@ -3,15 +3,22 @@ use eth_types::{GethExecStep, ToWord, Word}; use crate::{ circuit_input_builder::{Call, CircuitInputStateRef, ExecState, ExecStep}, operation::CallContextField, - precompile::PrecompileCalls, + precompile::{PrecompileAuxData, PrecompileCalls}, Error, }; +mod ecrecover; + +use ecrecover::opt_data as opt_data_ecrecover; + pub fn gen_associated_ops( state: &mut CircuitInputStateRef, geth_step: GethExecStep, call: Call, precompile: PrecompileCalls, + input_bytes: &[u8], + output_bytes: &[u8], + return_bytes: &[u8], ) -> Result { assert_eq!(call.code_address(), Some(precompile.into())); let mut exec_step = state.new_step(&geth_step)?; @@ -19,6 +26,35 @@ pub fn gen_associated_ops( common_call_ctx_reads(state, &mut exec_step, &call)?; + let (opt_event, aux_data) = match precompile { + PrecompileCalls::Ecrecover => opt_data_ecrecover(input_bytes, output_bytes, return_bytes), + PrecompileCalls::Identity => ( + None, + Some(PrecompileAuxData::Base { + input_bytes: input_bytes.to_vec(), + output_bytes: output_bytes.to_vec(), + return_bytes: return_bytes.to_vec(), + }), + ), + _ => { + log::warn!("precompile {:?} unsupported in circuits", precompile); + ( + None, + Some(PrecompileAuxData::Base { + input_bytes: input_bytes.to_vec(), + output_bytes: output_bytes.to_vec(), + return_bytes: return_bytes.to_vec(), + }), + ) + } + }; + log::trace!("precompile event {opt_event:?}, aux data {aux_data:?}"); + + if let Some(event) = opt_event { + state.push_precompile_event(event); + } + exec_step.aux_data = aux_data; + Ok(exec_step) } diff --git a/bus-mapping/src/precompile.rs b/bus-mapping/src/precompile.rs index 9b43d844a9..3ccc940964 100644 --- a/bus-mapping/src/precompile.rs +++ b/bus-mapping/src/precompile.rs @@ -2,7 +2,7 @@ use eth_types::{ evm_types::{GasCost, OpcodeId}, - Address, Bytecode, Word, + Address, Bytecode, ToBigEndian, Word, }; #[cfg(not(target_arch = "wasm32"))] use revm_precompile::{Precompile, PrecompileError, Precompiles}; @@ -63,7 +63,7 @@ pub(crate) fn execute_precompiled( #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum PrecompileCalls { /// Elliptic Curve Recovery - ECRecover = 0x01, + Ecrecover = 0x01, /// SHA2-256 hash function Sha256 = 0x02, /// Ripemd-160 hash function @@ -82,6 +82,12 @@ pub enum PrecompileCalls { Blake2F = 0x09, } +impl Default for PrecompileCalls { + fn default() -> Self { + Self::Ecrecover + } +} + impl From for Address { fn from(value: PrecompileCalls) -> Self { let mut addr = [0u8; 20]; @@ -105,7 +111,7 @@ impl From for usize { impl From for PrecompileCalls { fn from(value: u8) -> Self { match value { - 0x01 => Self::ECRecover, + 0x01 => Self::Ecrecover, 0x02 => Self::Sha256, 0x03 => Self::Ripemd160, 0x04 => Self::Identity, @@ -123,7 +129,7 @@ impl PrecompileCalls { /// Get the base gas cost for the precompile call. pub fn base_gas_cost(&self) -> u64 { match self { - Self::ECRecover => GasCost::PRECOMPILE_ECRECOVER_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, @@ -143,7 +149,7 @@ impl PrecompileCalls { /// Maximum length of input bytes considered for the precompile call. pub fn input_len(&self) -> Option { match self { - Self::ECRecover | Self::Bn128Add => Some(128), + Self::Ecrecover | Self::Bn128Add => Some(128), Self::Bn128Mul => Some(96), _ => None, } @@ -221,3 +227,82 @@ impl PrecompileCallArgs { code } } + +/// 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: Word, + /// r-component of signature. + pub sig_r: Word, + /// s-component of signature. + pub sig_s: Word, + /// Address that was recovered. + pub recovered_addr: Address, + /// Input bytes to the ecrecover call. + pub input_bytes: Vec, + /// Output bytes from the ecrecover call. + pub output_bytes: Vec, + /// Bytes returned to the caller from the ecrecover call. + pub return_bytes: Vec, +} + +impl EcrecoverAuxData { + /// Create a new instance of ecrecover auxiliary data. + pub fn new(input: &[u8], output: &[u8], return_bytes: &[u8]) -> Self { + let mut resized_input = input.to_vec(); + resized_input.resize(128, 0u8); + let mut resized_output = output.to_vec(); + resized_output.resize(32, 0u8); + + // assert that recovered address is 20 bytes. + assert!(resized_output[0x00..0x0c].iter().all(|&b| b == 0)); + let recovered_addr = Address::from_slice(&resized_output[0x0c..0x20]); + + Self { + msg_hash: Word::from_big_endian(&resized_input[0x00..0x20]), + sig_v: Word::from_big_endian(&resized_input[0x20..0x40]), + sig_r: Word::from_big_endian(&resized_input[0x40..0x60]), + sig_s: Word::from_big_endian(&resized_input[0x60..0x80]), + recovered_addr, + input_bytes: input.to_vec(), + output_bytes: output.to_vec(), + return_bytes: return_bytes.to_vec(), + } + } + + /// Sanity check and returns recovery ID. + pub fn recovery_id(&self) -> Option { + let sig_v_bytes = self.sig_v.to_be_bytes(); + let sig_v = sig_v_bytes[31]; + if sig_v_bytes.iter().take(31).all(|&b| b == 0) && (sig_v == 27 || sig_v == 28) { + Some(sig_v - 27) + } else { + None + } + } +} + +/// Auxiliary data attached to an internal state for precompile verification. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PrecompileAuxData { + /// Base precompile (used for Identity, SHA256, RIPEMD-160 and BLAKE2F). + Base { + /// input bytes to the identity call. + input_bytes: Vec, + /// output bytes from the identity call. + output_bytes: Vec, + /// bytes returned back to the caller from the identity call. + return_bytes: Vec, + }, + /// Ecrecover. + Ecrecover(EcrecoverAuxData), +} + +impl Default for PrecompileAuxData { + fn default() -> Self { + Self::Ecrecover(EcrecoverAuxData::default()) + } +} diff --git a/circuit-benchmarks/src/super_circuit.rs b/circuit-benchmarks/src/super_circuit.rs index 450a912d94..e9868aabbe 100644 --- a/circuit-benchmarks/src/super_circuit.rs +++ b/circuit-benchmarks/src/super_circuit.rs @@ -89,6 +89,7 @@ mod tests { max_bytecode: 512, max_evm_rows: 0, max_keccak_rows: 0, + max_vertical_circuit_rows: 0, }; let (_, circuit, instance, _) = SuperCircuit::build(block, circuits_params, Fr::from(0x100)).unwrap(); diff --git a/eth-types/src/geth_types.rs b/eth-types/src/geth_types.rs index b606d8547b..b40c107a47 100644 --- a/eth-types/src/geth_types.rs +++ b/eth-types/src/geth_types.rs @@ -247,6 +247,24 @@ impl From<&Transaction> for TransactionRequest { } impl Transaction { + /// Create a dummy Transaction with zero values + pub fn dummy() -> Self { + Self { + from: Address::zero(), + to: Some(Address::zero()), + nonce: U64::zero(), + gas_limit: U64::zero(), + value: Word::zero(), + gas_price: Word::zero(), + gas_tip_cap: Word::zero(), + gas_fee_cap: Word::zero(), + call_data: Bytes::new(), + access_list: None, + v: 0, + r: Word::zero(), + s: Word::zero(), + } + } /// Return the SignData associated with this Transaction. pub fn sign_data(&self, chain_id: u64) -> Result { let sig_r_le = self.r.to_le_bytes(); @@ -277,8 +295,9 @@ impl Transaction { libsecp256k1::Error::InvalidMessage, )?; Ok(SignData { - signature: (sig_r, sig_s), + signature: (sig_r, sig_s, v), pk, + msg, msg_hash, }) } diff --git a/eth-types/src/sign_types.rs b/eth-types/src/sign_types.rs index 696b57bb6b..af5dd261c4 100644 --- a/eth-types/src/sign_types.rs +++ b/eth-types/src/sign_types.rs @@ -1,13 +1,15 @@ //! secp256k1 signature types and helper functions. use crate::{ToBigEndian, Word}; +use ethers_core::{ + types::{Address, Bytes}, + utils::keccak256, +}; use halo2_proofs::{ arithmetic::{CurveAffine, Field}, halo2curves::{ - group::{ - ff::{FromUniformBytes, PrimeField}, - Curve, - }, + ff::FromUniformBytes, + group::{ff::PrimeField, prime::PrimeCurveAffine, Curve}, secp256k1::{self, Secp256k1Affine}, Coordinates, }, @@ -21,7 +23,7 @@ pub fn sign( randomness: secp256k1::Fq, sk: secp256k1::Fq, msg_hash: secp256k1::Fq, -) -> (secp256k1::Fq, secp256k1::Fq) { +) -> (secp256k1::Fq, secp256k1::Fq, u8) { let randomness_inv = Option::::from(randomness.invert()).expect("cannot invert randomness"); let generator = Secp256k1Affine::generator(); @@ -38,34 +40,51 @@ pub fn sign( let sig_r = secp256k1::Fq::from_uniform_bytes(&x_bytes); // get x coordinate (E::Base) on E::Scalar let sig_s = randomness_inv * (msg_hash + sig_r * sk); - (sig_r, sig_s) + let sig_v = sig_point.to_affine().y.is_odd().unwrap_u8(); + (sig_r, sig_s, sig_v) } /// Signature data required by the SignVerify Chip as input to verify a /// signature. #[derive(Clone, Debug)] pub struct SignData { - /// Secp256k1 signature point - pub signature: (secp256k1::Fq, secp256k1::Fq), + /// Secp256k1 signature point (r, s, v) + /// v must be 0 or 1 + pub signature: (secp256k1::Fq, secp256k1::Fq, u8), /// Secp256k1 public key pub pk: Secp256k1Affine, + /// Message being hashed before signing. + pub msg: Bytes, /// Hash of the message that is being signed pub msg_hash: secp256k1::Fq, } +impl SignData { + /// Recover address of the signature + pub fn get_addr(&self) -> Address { + if self.pk == Secp256k1Affine::identity() { + return Address::zero(); + } + let pk_hash = keccak256(pk_bytes_swap_endianness(&pk_bytes_le(&self.pk))); + Address::from_slice(&pk_hash[12..]) + } +} + lazy_static! { static ref SIGN_DATA_DEFAULT: SignData = { let generator = Secp256k1Affine::generator(); let sk = secp256k1::Fq::ONE; let pk = generator * sk; let pk = pk.to_affine(); + let msg = Bytes::new(); let msg_hash = secp256k1::Fq::ONE; let randomness = secp256k1::Fq::ONE; - let (sig_r, sig_s) = sign(randomness, sk, msg_hash); + let (sig_r, sig_s, sig_v) = sign(randomness, sk, msg_hash); SignData { - signature: (sig_r, sig_s), + signature: (sig_r, sig_s, sig_v), pk, + msg, msg_hash, } }; diff --git a/integration-tests/src/integration_test_circuits.rs b/integration-tests/src/integration_test_circuits.rs index 639143f738..9631b36316 100644 --- a/integration-tests/src/integration_test_circuits.rs +++ b/integration-tests/src/integration_test_circuits.rs @@ -66,6 +66,8 @@ const MAX_EVM_ROWS: usize = 10000; const MAX_EXP_STEPS: usize = 1000; const MAX_KECCAK_ROWS: usize = 38000; +/// MAX_VERTICAL_CIRCUIT_ROWS +const MAX_VERTICAL_CIRCUIT_ROWS: usize = 0; const CIRCUITS_PARAMS: FixedCParams = FixedCParams { max_rws: MAX_RWS, @@ -77,6 +79,7 @@ const CIRCUITS_PARAMS: FixedCParams = FixedCParams { max_evm_rows: MAX_EVM_ROWS, max_exp_steps: MAX_EXP_STEPS, max_keccak_rows: MAX_KECCAK_ROWS, + max_vertical_circuit_rows: MAX_VERTICAL_CIRCUIT_ROWS, }; const EVM_CIRCUIT_DEGREE: u32 = 18; diff --git a/integration-tests/tests/circuit_input_builder.rs b/integration-tests/tests/circuit_input_builder.rs index 339f243d02..d2fbd6a22b 100644 --- a/integration-tests/tests/circuit_input_builder.rs +++ b/integration-tests/tests/circuit_input_builder.rs @@ -25,6 +25,7 @@ async fn test_circuit_input_builder_block(block_num: u64) { max_evm_rows: 0, max_exp_steps: 1000, max_keccak_rows: 0, + max_vertical_circuit_rows: 0, }, ) .await diff --git a/testool/src/statetest/executor.rs b/testool/src/statetest/executor.rs index 1a6fa800b2..a2b2a3c849 100644 --- a/testool/src/statetest/executor.rs +++ b/testool/src/statetest/executor.rs @@ -337,6 +337,7 @@ pub fn run_test( max_evm_rows: 0, max_exp_steps: 5000, max_keccak_rows: 0, + max_vertical_circuit_rows: 0, }; let block_data = BlockData::new_from_geth_data_with_params(geth_data, circuits_params); @@ -375,6 +376,7 @@ pub fn run_test( max_bytecode: 512, max_evm_rows: 0, max_keccak_rows: 0, + max_vertical_circuit_rows: 0, }; let (k, circuit, instance, _builder) = SuperCircuit::::build(geth_data, circuits_params, Fr::from(0x100)).unwrap(); diff --git a/zkevm-circuits/Cargo.toml b/zkevm-circuits/Cargo.toml index 475444f48f..3a64d19a9f 100644 --- a/zkevm-circuits/Cargo.toml +++ b/zkevm-circuits/Cargo.toml @@ -40,6 +40,9 @@ serde = { version = "1.0.130", features = ["derive"] } serde_json = "1.0.78" thiserror = "1.0" hex = {version = "0.4.3", features = ["serde"]} +halo2-base = { git = "https://github.com/scroll-tech/halo2-lib", branch = "develop", default-features=false, features=["halo2-pse","display"] } +halo2-ecc = { git = "https://github.com/scroll-tech/halo2-lib", branch = "develop", default-features=false, features=["halo2-pse","display"] } + [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] } diff --git a/zkevm-circuits/src/ecc_circuit.rs b/zkevm-circuits/src/ecc_circuit.rs new file mode 100644 index 0000000000..0764e19d76 --- /dev/null +++ b/zkevm-circuits/src/ecc_circuit.rs @@ -0,0 +1,1377 @@ +//! The ECC circuit is responsible for verifying ECC-related operations from precompiled contract +//! calls, namely, EcAdd, EcMul and EcPairing. + +use std::{iter, marker::PhantomData}; + +use bus_mapping::{ + circuit_input_builder::{EcAddOp, EcMulOp, EcPairingOp, N_BYTES_PER_PAIR, N_PAIRING_PER_OP}, + precompile::PrecompileCalls, +}; +use eth_types::{Field, ToLittleEndian, ToScalar, U256}; +use halo2_base::{ + gates::{GateInstructions, RangeInstructions}, + utils::{decompose_bigint_option, fe_to_biguint, modulus}, + AssignedValue, Context, QuantumCell, SKIP_FIRST_PASS, +}; +use halo2_ecc::{ + bigint::{big_is_zero, CRTInteger, OverflowInteger}, + bn254::pairing::PairingChip, + ecc::{EcPoint, EccChip}, + fields::{ + fp::{FpConfig, FpStrategy}, + fp12::Fp12Chip, + fp2::Fp2Chip, + FieldChip, FieldExtPoint, + }, +}; +use halo2_proofs::{ + arithmetic::Field as Halo2Field, + circuit::{Layouter, Value}, + halo2curves::{ + bn256::{Fq, Fq12, Fq2, Fr, G1Affine, G2Affine}, + CurveAffine, + }, + plonk::{ConstraintSystem, Error, Expression}, +}; +use itertools::Itertools; +use log::error; +use snark_verifier::util::arithmetic::PrimeCurveAffine; + +use crate::{ + evm_circuit::{param::N_BYTES_WORD, EvmCircuit}, + keccak_circuit::KeccakCircuit, + table::{EccTable, LookupTable}, + util::{Challenges, SubCircuit, SubCircuitConfig}, + witness::Block, +}; + +mod dev; +mod test; +mod util; + +use util::{ + EcAddAssigned, EcAddDecomposed, EcMulAssigned, EcMulDecomposed, EcOpsAssigned, + EcPairingAssigned, EcPairingDecomposed, G1Assigned, G1Decomposed, G2Decomposed, ScalarAssigned, + LOG_TOTAL_NUM_ROWS, +}; + +macro_rules! log_context_cursor { + ($ctx: ident) => {{ + log::trace!("Ctx cell pos: {:?}", $ctx.advice_alloc); + }}; +} + +/// Arguments accepted to configure the EccCircuitConfig. +#[derive(Clone, Debug)] +pub struct EccCircuitConfigArgs { + /// ECC table that is connected to the ECC circuit. + pub ecc_table: EccTable, + /// zkEVM challenge API. + pub challenges: Challenges>, +} + +/// Config for the ECC circuit. +#[derive(Clone, Debug)] +pub struct EccCircuitConfig { + /// Field config for halo2_proofs::halo2curves::bn256::Fq. + fp_config: FpConfig, + /// Lookup table for I/Os to the EcAdd, EcMul and EcPairing operations. + ecc_table: EccTable, + + /// Number of limbs to represent Fp. + num_limbs: usize, + /// Number of bits per limb. + limb_bits: usize, + + _marker: PhantomData, +} + +impl SubCircuitConfig for EccCircuitConfig { + type ConfigArgs = EccCircuitConfigArgs; + + fn new( + meta: &mut ConstraintSystem, + Self::ConfigArgs { + ecc_table, + challenges: _, + }: Self::ConfigArgs, + ) -> Self { + let num_limbs = 3; + let limb_bits = 88; + let num_advice = [35, 1]; + + let fp_config = FpConfig::configure( + meta, + FpStrategy::Simple, + &num_advice, + &[17], // num lookup advice + 1, // num fixed + 13, // lookup bits + limb_bits, + num_limbs, + modulus::(), + 0, + LOG_TOTAL_NUM_ROWS as usize, // k + ); + + for column in >::advice_columns(&ecc_table) { + meta.enable_equality(column); + } + + Self { + fp_config, + ecc_table, + num_limbs, + limb_bits, + _marker: PhantomData, + } + } +} + +/// The ECC Circuit is a sub-circuit of the super circuit, responsible for verifying the following +/// ECC operations: +/// 1. Point Addition (R = P + Q) +/// 2. Scalar Multiplication (R = s.P) +/// 3. Pairing-based bilinear function +/// +/// We follow a strategy to pre-allocate maximum number of cells for each of the above ECC +/// operations, which means a witness that exceeds the pre-allocated number of cells for any of the +/// operations will be invalid. +#[derive(Clone, Debug, Default)] +pub struct EccCircuit { + /// Maximum number of EcAdd operations supported in one instance of the ECC Circuit. + pub max_add_ops: usize, + /// Maximum number of scalar multiplication operations supported in one instance of the ECC + /// Circuit. + pub max_mul_ops: usize, + /// Maximum number of pairing operations supported in one instance of the ECC Circuit. + pub max_pairing_ops: usize, + + /// EcAdd operations provided as witness data to the ECC circuit. + pub add_ops: Vec, + /// EcMul operations provided as witness data to the ECC circuit. + pub mul_ops: Vec, + /// EcPairing operations provided as witness data to the ECC circuit. + pub pairing_ops: Vec, + + _marker: PhantomData, +} + +impl EccCircuit { + /// Return the minimum number of rows required to prove an input of a + /// particular size. + pub fn min_num_rows() -> usize { + // EccCircuit can't determine usable rows independently. + // Instead, the blinding area is determined by other advise columns with most counts of + // rotation queries. This value is typically determined by either the Keccak or EVM + // circuit. + + let max_blinding_factor = Self::unusable_rows() - 1; + + // same formula as halo2-lib's FlexGate + (1 << LOG_TOTAL_NUM_ROWS) - (max_blinding_factor + 3) + } + + /// Assign witness from the ecXX ops to the circuit. + pub(crate) fn assign( + &self, + layouter: &mut impl Layouter, + config: &>::Config, + challenges: &Challenges>, + ) -> Result<(), Error> { + if self.add_ops.len() > self.max_add_ops + || self.mul_ops.len() > self.max_mul_ops + || self.pairing_ops.len() > self.max_pairing_ops + { + error!( + "add ops = {}, mul ops = {}, pairing ops = {} > max add ops = {}, max mul ops = {}, max pairing ops = {}", + self.add_ops.len(), + self.mul_ops.len(), + self.pairing_ops.len(), + self.max_add_ops, + self.max_mul_ops, + self.max_pairing_ops, + ); + return Err(Error::Synthesis); + } + + // keccak powers of randomness. + let keccak_powers = std::iter::successors(Some(Value::known(F::one())), |coeff| { + Some(challenges.keccak_input() * coeff) + }) + .take(N_PAIRING_PER_OP * N_BYTES_PER_PAIR) + .map(|x| QuantumCell::Witness(x)) + .collect_vec(); + + let powers_of_256 = iter::successors(Some(F::one()), |coeff| Some(F::from(256) * coeff)) + .take(N_BYTES_WORD) + .map(|x| QuantumCell::Constant(x)) + .collect_vec(); + + let ecc_chip = EccChip::>::construct(config.fp_config.clone()); + let fr_chip = FpConfig::::construct( + config.fp_config.range.clone(), + config.limb_bits, + config.num_limbs, + modulus::(), + ); + let pairing_chip = PairingChip::construct(config.fp_config.clone()); + let fp12_chip = + Fp12Chip::, Fq12, XI_0>::construct(config.fp_config.clone()); + + let mut first_pass = SKIP_FIRST_PASS; + + let assigned_ec_ops = layouter.assign_region( + || "ecc circuit", + |region| { + if first_pass { + first_pass = false; + return Ok(EcOpsAssigned::default()); + } + + let mut ctx = config.fp_config.new_context(region); + + macro_rules! decompose_ec_op { + ($op_type:ident, $ops:expr, $n_ops:expr, $decompose_fn:ident) => { + $ops.iter() + .filter(|op| !op.skip_by_ecc_circuit()) + .chain(std::iter::repeat(&$op_type::default())) + .take($n_ops) + .map(|op| { + self.$decompose_fn( + &mut ctx, + &ecc_chip, + &fr_chip, + &pairing_chip, + &fp12_chip, + &powers_of_256, + &op, + ) + }) + .collect_vec() + }; + } + + macro_rules! assign_ec_op { + ($decomposed_ops:expr, $assign_fn:ident) => { + $decomposed_ops + .iter() + .map(|decomposed_op| { + self.$assign_fn(&mut ctx, decomposed_op, &ecc_chip, &keccak_powers) + }) + .collect_vec() + }; + } + + // P + Q == R + let ec_adds_decomposed = + decompose_ec_op!(EcAddOp, self.add_ops, self.max_add_ops, decompose_ec_add_op); + + // s.P = R + let ec_muls_decomposed = + decompose_ec_op!(EcMulOp, self.mul_ops, self.max_mul_ops, decompose_ec_mul_op); + + // e(G1 . G2) * ... * e(G1 . G2) -> Gt + let ec_pairings_decomposed = decompose_ec_op!( + EcPairingOp, + self.pairing_ops, + self.max_pairing_ops, + decompose_ec_pairing_op + ); + + // finalize after first phase. + config.fp_config.finalize(&mut ctx); + ctx.next_phase(); + + let ec_adds_assigned = assign_ec_op!(ec_adds_decomposed, assign_ec_add); + let ec_muls_assigned = assign_ec_op!(ec_muls_decomposed, assign_ec_mul); + let ec_pairings_assigned = assign_ec_op!(ec_pairings_decomposed, assign_ec_pairing); + + // Finalize the Fp config always at the end of assignment. + let lookup_cells = config.fp_config.finalize(&mut ctx); + log::info!("total number of lookup cells: {}", lookup_cells); + ctx.print_stats(&["EccCircuit: FpConfig context"]); + + Ok(EcOpsAssigned { + ec_adds_assigned, + ec_muls_assigned, + ec_pairings_assigned, + }) + }, + )?; + + layouter.assign_region( + || "expose ecc table", + |mut region| { + // handle EcAdd ops. + for (idx, ec_add_assigned) in assigned_ec_ops.ec_adds_assigned.iter().enumerate() { + region.assign_fixed( + || "assign ecc_table op_type", + config.ecc_table.op_type, + idx, + || Value::known(F::from(u64::from(PrecompileCalls::Bn128Add))), + )?; + ec_add_assigned.is_valid.copy_advice( + &mut region, + config.ecc_table.is_valid, + idx, + ); + // P_x + ec_add_assigned.point_p.x_rlc.copy_advice( + &mut region, + config.ecc_table.arg1_rlc, + idx, + ); + // P_y + ec_add_assigned.point_p.y_rlc.copy_advice( + &mut region, + config.ecc_table.arg2_rlc, + idx, + ); + // Q_x + ec_add_assigned.point_q.x_rlc.copy_advice( + &mut region, + config.ecc_table.arg3_rlc, + idx, + ); + // Q_y + ec_add_assigned.point_q.y_rlc.copy_advice( + &mut region, + config.ecc_table.arg4_rlc, + idx, + ); + // R_x + ec_add_assigned.point_r.x_rlc.copy_advice( + &mut region, + config.ecc_table.output1_rlc, + idx, + ); + // R_y + ec_add_assigned.point_r.y_rlc.copy_advice( + &mut region, + config.ecc_table.output2_rlc, + idx, + ); + // input_rlc == 0 + region.assign_advice( + || format!("input_rlc at offset = {idx}"), + config.ecc_table.input_rlc, + idx, + || Value::known(F::zero()), + )?; + } + + // handle EcMul ops. + for (idx, ec_mul_assigned) in assigned_ec_ops.ec_muls_assigned.iter().enumerate() { + let idx = idx + self.max_add_ops; + region.assign_fixed( + || "assign ecc_table op_type", + config.ecc_table.op_type, + idx, + || Value::known(F::from(u64::from(PrecompileCalls::Bn128Mul))), + )?; + // Is valid + ec_mul_assigned.is_valid.copy_advice( + &mut region, + config.ecc_table.is_valid, + idx, + ); + // P_x + ec_mul_assigned.point_p.x_rlc.copy_advice( + &mut region, + config.ecc_table.arg1_rlc, + idx, + ); + // P_y + ec_mul_assigned.point_p.y_rlc.copy_advice( + &mut region, + config.ecc_table.arg2_rlc, + idx, + ); + // Scalar s + ec_mul_assigned.scalar_s.scalar.native.copy_advice( + &mut region, + config.ecc_table.arg3_rlc, + idx, + ); + // R_x + ec_mul_assigned.point_r.x_rlc.copy_advice( + &mut region, + config.ecc_table.output1_rlc, + idx, + ); + // R_y + ec_mul_assigned.point_r.y_rlc.copy_advice( + &mut region, + config.ecc_table.output2_rlc, + idx, + ); + for &col in [config.ecc_table.arg4_rlc, config.ecc_table.input_rlc].iter() { + region.assign_advice( + || format!("{col:?} at offset = {idx}"), + col, + idx, + || Value::known(F::zero()), + )?; + } + } + + // handle EcPairing ops. + for (idx, ec_pairing_assigned) in + assigned_ec_ops.ec_pairings_assigned.iter().enumerate() + { + let idx = idx + self.max_add_ops + self.max_mul_ops; + region.assign_fixed( + || "assign ecc_table op_type", + config.ecc_table.op_type, + idx, + || Value::known(F::from(u64::from(PrecompileCalls::Bn128Pairing))), + )?; + // is valid. + ec_pairing_assigned.is_valid.copy_advice( + &mut region, + config.ecc_table.is_valid, + idx, + ); + // RLC(input_bytes) + ec_pairing_assigned.input_rlc.copy_advice( + &mut region, + config.ecc_table.input_rlc, + idx, + ); + // success + ec_pairing_assigned.success.copy_advice( + &mut region, + config.ecc_table.output1_rlc, + idx, + ); + for &col in [ + config.ecc_table.arg1_rlc, + config.ecc_table.arg2_rlc, + config.ecc_table.arg3_rlc, + config.ecc_table.arg4_rlc, + config.ecc_table.output2_rlc, + ] + .iter() + { + region.assign_advice( + || format!("{col:?} at offset = {idx}"), + col, + idx, + || Value::known(F::zero()), + )?; + } + } + + Ok(()) + }, + )?; + + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn decompose_ec_add_op( + &self, + ctx: &mut Context, + ecc_chip: &EccChip>, + _fr_chip: &FpConfig, + _pairing_chip: &PairingChip, + _fp12_chip: &Fp12Chip, Fq12, XI_0>, + powers_of_256: &[QuantumCell], + op: &EcAddOp, + ) -> EcAddDecomposed { + log::trace!("[ECC] ==> EcAdd Assignment START:"); + log_context_cursor!(ctx); + + let (px, px_cells, px_valid, px_is_zero) = + self.precheck_fq(ctx, ecc_chip, op.p.0, powers_of_256); + let (py, py_cells, py_valid, py_is_zero) = + self.precheck_fq(ctx, ecc_chip, op.p.1, powers_of_256); + let p_is_on_curve_or_infinity = + self.is_on_curveg1_or_infinity(ctx, ecc_chip, &px, px_is_zero, &py, py_is_zero); + let (qx, qx_cells, qx_valid, qx_is_zero) = + self.precheck_fq(ctx, ecc_chip, op.q.0, powers_of_256); + let (qy, qy_cells, qy_valid, qy_is_zero) = + self.precheck_fq(ctx, ecc_chip, op.q.1, powers_of_256); + let q_is_on_curve_or_infinity = + self.is_on_curveg1_or_infinity(ctx, ecc_chip, &qx, qx_is_zero, &qy, qy_is_zero); + + let point_p = EcPoint::construct(px, py); + let point_q = EcPoint::construct(qx, qy); + + let inputs_valid = ecc_chip.field_chip().range().gate().and_many( + ctx, + vec![ + QuantumCell::Existing(px_valid), + QuantumCell::Existing(py_valid), + QuantumCell::Existing(p_is_on_curve_or_infinity), + QuantumCell::Existing(qx_valid), + QuantumCell::Existing(qy_valid), + QuantumCell::Existing(q_is_on_curve_or_infinity), + ], + ); + let inputs_invalid = ecc_chip + .field_chip() + .range() + .gate() + .not(ctx, QuantumCell::Existing(inputs_valid)); + + log::trace!("[ECC] EcAdd Inputs Assigned:"); + log_context_cursor!(ctx); + + // We follow the approach mentioned below to handle many edge cases for the points P, Q and + // R so that we can maintain the same fixed and permutation columns and reduce the overall + // validation process from the EVM Circuit. + // + // To check the validity of P + Q == R, we check: + // r + P + Q - R == r + // where r is a random point on the curve. + // + // We cover cases such as: + // - P == (0, 0) and/or Q == (0, 0) + // - P == -Q, i.e. P + Q == R == (0, 0) + let res = op.r.unwrap_or(G1Affine::identity()); + let point_r = self.handle_g1(ctx, ecc_chip, res, powers_of_256); + let rx_is_zero = ecc_chip.field_chip.is_zero(ctx, &point_r.ec_point.x); + let ry_is_zero = ecc_chip.field_chip.is_zero(ctx, &point_r.ec_point.y); + + let rand_point = ecc_chip.load_random_point::(ctx); + let point_p_is_zero = ecc_chip.field_chip.range().gate().or_and( + ctx, + QuantumCell::Existing(inputs_invalid), + QuantumCell::Existing(px_is_zero), + QuantumCell::Existing(py_is_zero), + ); + let point_q_is_zero = ecc_chip.field_chip.range().gate().or_and( + ctx, + QuantumCell::Existing(inputs_invalid), + QuantumCell::Existing(qx_is_zero), + QuantumCell::Existing(qy_is_zero), + ); + let point_r_is_zero = ecc_chip.field_chip.range().gate().or_and( + ctx, + QuantumCell::Existing(inputs_invalid), + QuantumCell::Existing(rx_is_zero), + QuantumCell::Existing(ry_is_zero), + ); + + // sum1 = if P == (0, 0) then r else r + P + let sum1 = ecc_chip.add_unequal(ctx, &rand_point, &point_p, true); + let sum1 = ecc_chip.select(ctx, &rand_point, &sum1, &point_p_is_zero); + + // sum2 = if Q == (0, 0) then sum1 else sum1 + Q + let sum2 = ecc_chip.add_unequal(ctx, &sum1, &point_q, true); + let sum2 = ecc_chip.select(ctx, &sum1, &sum2, &point_q_is_zero); + + // sum3 = if R == (0, 0) then sum2 else sum2 - R + let sum3 = ecc_chip.sub_unequal(ctx, &sum2, &point_r.ec_point, true); + let sum3 = ecc_chip.select(ctx, &sum2, &sum3, &point_r_is_zero); + + ecc_chip.assert_equal(ctx, &rand_point, &sum3); + + log::trace!("[ECC] EcAdd Assignmnet END:"); + log_context_cursor!(ctx); + + EcAddDecomposed { + is_valid: inputs_valid, + point_p: G1Decomposed { + ec_point: point_p, + x_cells: px_cells, + y_cells: py_cells, + }, + point_q: G1Decomposed { + ec_point: point_q, + x_cells: qx_cells, + y_cells: qy_cells, + }, + point_r, + } + } + + /// Decomposes an EcMul operation to return each G1 element as cells representing its byte + /// form. + #[allow(clippy::too_many_arguments)] + fn decompose_ec_mul_op( + &self, + ctx: &mut Context, + ecc_chip: &EccChip>, + fr_chip: &FpConfig, + _pairing_chip: &PairingChip, + _fp12_chip: &Fp12Chip, Fq12, XI_0>, + powers_of_256: &[QuantumCell], + op: &EcMulOp, + ) -> EcMulDecomposed { + log::trace!("[ECC] ==> EcMul Assignmnet START:"); + log_context_cursor!(ctx); + + let (px, px_cells, px_valid, px_is_zero) = + self.precheck_fq(ctx, ecc_chip, op.p.0, powers_of_256); + let (py, py_cells, py_valid, py_is_zero) = + self.precheck_fq(ctx, ecc_chip, op.p.1, powers_of_256); + let p_is_on_curve_or_infinity = + self.is_on_curveg1_or_infinity(ctx, ecc_chip, &px, px_is_zero, &py, py_is_zero); + + // point at infinity + let infinity = EcPoint::construct( + ecc_chip + .field_chip() + .load_constant(ctx, fe_to_biguint(&Fq::zero())), + ecc_chip + .field_chip() + .load_constant(ctx, fe_to_biguint(&Fq::zero())), + ); + // for invalid case, take a random point. + let dummy_g1 = ecc_chip.load_random_point::(ctx); + + let point_p = EcPoint::construct(px, py); + let is_valid = ecc_chip.field_chip().range().gate().and_many( + ctx, + vec![ + QuantumCell::Existing(px_valid), + QuantumCell::Existing(py_valid), + QuantumCell::Existing(p_is_on_curve_or_infinity), + ], + ); + let point_p = ecc_chip.select(ctx, &point_p, &dummy_g1, &is_valid); + + let scalar_s = self.handle_fr(ctx, fr_chip, op.s); + + let res = op.r.unwrap_or(G1Affine::identity()); + let point_r = self.handle_g1(ctx, ecc_chip, res, powers_of_256); + + log::trace!("[ECC] EcMul Inputs Assigned:"); + log_context_cursor!(ctx); + + let point_r_got = ecc_chip.scalar_mult( + ctx, + &point_p, + &scalar_s.scalar.limbs().to_vec(), + fr_chip.limb_bits, + 4, + ); + let point_r_got = ecc_chip.select(ctx, &point_r_got, &infinity, &is_valid); + ecc_chip.assert_equal(ctx, &point_r.ec_point, &point_r_got); + + log::trace!("[ECC] EcMul Assignmnet END:"); + log_context_cursor!(ctx); + + EcMulDecomposed { + is_valid, + point_p: G1Decomposed { + ec_point: point_p, + x_cells: px_cells, + y_cells: py_cells, + }, + scalar_s, + point_r, + } + } + + /// Decomposes an EcPairing operation and returns cells that represent the LE-bytes of all + /// (G1, G2) pairs. In phase2 they will be RLC'd with the keccak randomness. + #[allow(clippy::too_many_arguments)] + fn decompose_ec_pairing_op( + &self, + ctx: &mut Context, + ecc_chip: &EccChip>, + _fr_chip: &FpConfig, + pairing_chip: &PairingChip, + fp12_chip: &Fp12Chip, Fq12, XI_0>, + powers_of_256: &[QuantumCell], + op: &EcPairingOp, + ) -> EcPairingDecomposed { + log::trace!("[ECC] ==> EcPairing Assignment START:"); + log_context_cursor!(ctx); + + let fp2_chip = Fp2Chip::, Fq2>::construct(pairing_chip.fp_chip.clone()); + let ecc2_chip = EccChip::construct(fp2_chip.clone()); + + let decomposed_pairs = op + .pairs + .iter() + .map(|pair| { + // process x and y co-ordinates of G1. + let (g1x, g1x_cells, g1x_valid, g1x_is_zero) = + self.precheck_fq(ctx, ecc_chip, pair.g1_point.0, powers_of_256); + let (g1y, g1y_cells, g1y_valid, g1y_is_zero) = + self.precheck_fq(ctx, ecc_chip, pair.g1_point.1, powers_of_256); + let g1_point = EcPoint::>::construct(g1x, g1y); + let g1_is_on_curve_or_infinity = self.is_on_curveg1_or_infinity( + ctx, + ecc_chip, + &g1_point.x, + g1x_is_zero, + &g1_point.y, + g1y_is_zero, + ); + let is_g1_valid = ecc_chip.field_chip().range().gate().and_many( + ctx, + vec![ + QuantumCell::Existing(g1x_valid), + QuantumCell::Existing(g1y_valid), + QuantumCell::Existing(g1_is_on_curve_or_infinity), + ], + ); + let is_g1_identity = ecc_chip.field_chip().range().gate().and( + ctx, + QuantumCell::Existing(g1x_is_zero), + QuantumCell::Existing(g1y_is_zero), + ); + + // process x and y co-ordinates of G2. + let (g2x0, g2x0_cells, g2x0_valid, g2x0_is_zero) = + self.precheck_fq(ctx, ecc_chip, pair.g2_point.1, powers_of_256); + let (g2x1, g2x1_cells, g2x1_valid, g2x1_is_zero) = + self.precheck_fq(ctx, ecc_chip, pair.g2_point.0, powers_of_256); + let (g2y0, g2y0_cells, g2y0_valid, g2y0_is_zero) = + self.precheck_fq(ctx, ecc_chip, pair.g2_point.3, powers_of_256); + let (g2y1, g2y1_cells, g2y1_valid, g2y1_is_zero) = + self.precheck_fq(ctx, ecc_chip, pair.g2_point.2, powers_of_256); + let g2_point = EcPoint::>>::construct( + FieldExtPoint::construct(vec![g2x0, g2x1]), + FieldExtPoint::construct(vec![g2y0, g2y1]), + ); + let g2x_is_zero = ecc_chip.field_chip().range().gate().and( + ctx, + QuantumCell::Existing(g2x0_is_zero), + QuantumCell::Existing(g2x1_is_zero), + ); + let g2y_is_zero = ecc_chip.field_chip().range().gate().and( + ctx, + QuantumCell::Existing(g2y0_is_zero), + QuantumCell::Existing(g2y1_is_zero), + ); + let g2_is_on_curve_or_infinity = self.is_on_curveg2_or_infinity( + ctx, + &fp2_chip, + &g2_point.x, + g2x_is_zero, + &g2_point.y, + g2y_is_zero, + ); + let is_g2_valid = ecc_chip.field_chip().range().gate().and_many( + ctx, + vec![ + QuantumCell::Existing(g2x0_valid), + QuantumCell::Existing(g2x1_valid), + QuantumCell::Existing(g2y0_valid), + QuantumCell::Existing(g2y1_valid), + QuantumCell::Existing(g2_is_on_curve_or_infinity), + ], + ); + let is_g2_identity = ecc_chip.field_chip().range().gate().and_many( + ctx, + vec![ + QuantumCell::Existing(g2x0_is_zero), + QuantumCell::Existing(g2x1_is_zero), + QuantumCell::Existing(g2y0_is_zero), + QuantumCell::Existing(g2y1_is_zero), + ], + ); + + // Whether the pair (G1, G2) is valid, i.e. + // - G1 is a point on curve or infinity. + // - G2 is a point on curve or infinity. + let is_pair_valid = ecc_chip.field_chip().range().gate().and( + ctx, + QuantumCell::Existing(is_g1_valid), + QuantumCell::Existing(is_g2_valid), + ); + // Whether the pair (G1, G2) is a zero pair, i.e. + // - G1 == G1::infinity && G2 is valid. + // - G2 == G2::infinity && G1 is valid. + let is_zero_pair = { + let is_zero_pair = ecc_chip.field_chip().range().gate().or( + ctx, + QuantumCell::Existing(is_g1_identity), + QuantumCell::Existing(is_g2_identity), + ); + ecc_chip.field_chip().range().gate().and( + ctx, + QuantumCell::Existing(is_zero_pair), + QuantumCell::Existing(is_pair_valid), + ) + }; + + ( + is_zero_pair, + is_pair_valid, + G1Decomposed { + ec_point: g1_point, + x_cells: g1x_cells, + y_cells: g1y_cells, + }, + G2Decomposed { + ec_point: g2_point, + x_c0_cells: g2x0_cells, + x_c1_cells: g2x1_cells, + y_c0_cells: g2y0_cells, + y_c1_cells: g2y1_cells, + }, + ) + }) + .collect_vec(); + + log::trace!("[ECC] EcPairing g1s and g2s Assigned:"); + log_context_cursor!(ctx); + + // EVM input for EcPairing in Big-Endian representation, padded by 0 bytes so that the + // total number of bytes are N_PAIRING_PER_OP * N_BYTES_PER_PAIR. + let input_cells = decomposed_pairs + .iter() + .flat_map(|(_, _, g1, g2)| { + std::iter::empty() + .chain(g1.x_cells.iter().rev()) + .chain(g1.y_cells.iter().rev()) + .chain(g2.x_c1_cells.iter().rev()) + .chain(g2.x_c0_cells.iter().rev()) + .chain(g2.y_c1_cells.iter().rev()) + .chain(g2.y_c0_cells.iter().rev()) + .cloned() + .collect::>>() + }) + .collect::>>(); + + log::trace!("[ECC] EcPairing Inputs RLC Assigned:"); + log_context_cursor!(ctx); + + // Whether all the pairs are (G1::identity, G2::valid) or (G1::valid, G2::identity) form. + let all_pairs_zero = ecc_chip.field_chip().range().gate().and_many( + ctx, + decomposed_pairs + .iter() + .map(|(is_zero_pair, _, _, _)| QuantumCell::Existing(*is_zero_pair)) + .collect_vec(), + ); + + // dummy G1, G2 points and G1::identity, G2::generator. + let dummy_g1 = ecc_chip.load_random_point::(ctx); + let dummy_g2 = ecc2_chip.load_random_point::(ctx); + let identity_g1 = EcPoint::construct( + ecc_chip + .field_chip() + .load_constant(ctx, fe_to_biguint(&Fq::zero())), + ecc_chip + .field_chip() + .load_constant(ctx, fe_to_biguint(&Fq::zero())), + ); + let generator_g2 = { + let g2_gen = G2Affine::generator(); + EcPoint::>>::construct( + ecc2_chip.field_chip().load_constant(ctx, g2_gen.x), + ecc2_chip.field_chip().load_constant(ctx, g2_gen.y), + ) + }; + // A pairing op satisfying the pairing check. + type TupleG1sG2s = ( + Vec>>, + Vec>>>, + ); + let (dummy_pair_check_ok_g1s, dummy_pair_check_ok_g2s): TupleG1sG2s = + EcPairingOp::dummy_pairing_check_ok() + .pairs + .iter() + .map(|pair| { + let (g1_point, g2_point) = + pair.as_g1_g2().expect("dummy pairing check OK pair"); + ( + EcPoint::>::construct( + ecc_chip + .field_chip() + .load_constant(ctx, fe_to_biguint(&g1_point.x)), + ecc_chip + .field_chip() + .load_constant(ctx, fe_to_biguint(&g1_point.y)), + ), + EcPoint::>>::construct( + ecc2_chip.field_chip().load_constant(ctx, g2_point.x), + ecc2_chip.field_chip().load_constant(ctx, g2_point.y), + ), + ) + }) + .unzip(); + + // process pairs so that we pass only valid input to the multi_miller_loop. + let pairs = decomposed_pairs + .iter() + .enumerate() + .map(|(idx, (is_zero_pair, is_pair_valid, g1, g2))| { + // we should swap (G1, G2) with (G1::identity, G2::generator) if: + // - G1 == (0, 0) && G2 is valid + // - G2 == (0, 0, 0, 0) && G1 is valid + // + // we should swap (G1, G2) with (G1::random, G2::random) if: + // - G1 is invalid + // - G2 is invalid + ( + { + let swapped_g1 = + ecc_chip.select(ctx, &g1.ec_point, &dummy_g1, is_pair_valid); + let swapped_g1 = + ecc_chip.select(ctx, &identity_g1, &swapped_g1, is_zero_pair); + ecc_chip.select( + ctx, + &dummy_pair_check_ok_g1s[idx], + &swapped_g1, + &all_pairs_zero, + ) + }, + { + let swapped_x = + fp2_chip.select(ctx, &g2.ec_point.x, &dummy_g2.x, is_pair_valid); + let swapped_x = + fp2_chip.select(ctx, &generator_g2.x, &swapped_x, is_zero_pair); + let swapped_x = fp2_chip.select( + ctx, + &dummy_pair_check_ok_g2s[idx].x, + &swapped_x, + &all_pairs_zero, + ); + let swapped_y = + fp2_chip.select(ctx, &g2.ec_point.y, &dummy_g2.y, is_pair_valid); + let swapped_y = + fp2_chip.select(ctx, &generator_g2.y, &swapped_y, is_zero_pair); + let swapped_y = fp2_chip.select( + ctx, + &dummy_pair_check_ok_g2s[idx].y, + &swapped_y, + &all_pairs_zero, + ); + EcPoint::construct(swapped_x, swapped_y) + }, + ) + }) + .collect_vec(); + let pairs = pairs.iter().map(|(g1, g2)| (g1, g2)).collect_vec(); + + // if the entire input to ecPairing is valid. + let is_valid = ecc_chip.field_chip().range().gate().and_many( + ctx, + decomposed_pairs + .iter() + .map(|&(_, is_pair_valid, _, _)| QuantumCell::Existing(is_pair_valid)) + .collect_vec(), + ); + + // multi-miller loop and final exponentiation to do pairing check. + let success = { + let gt = { + let gt = pairing_chip.multi_miller_loop(ctx, pairs); + pairing_chip.final_exp(ctx, >) + }; + // whether pairing check was successful. + let one = fp12_chip.load_constant(ctx, Fq12::one()); + fp12_chip.is_equal(ctx, >, &one) + }; + // success == true only if pairing check and validity are both satisfied. + let success = ecc_chip.field_chip().range().gate().and( + ctx, + QuantumCell::Existing(is_valid), + QuantumCell::Existing(success), + ); + // if all inputs were zeroes, i.e. either: + // - G1 == (0, 0) and G2 == random valid point on G2 + // - G2 == (0, 0, 0, 0) and G1 == random valid point on G1 + // + // then success == true, i.e. success - all_pairs_zero == boolean + let success_minus_all_pairs_zero = ecc_chip + .field_chip() + .range() + .gate() + .load_witness(ctx, success.value - all_pairs_zero.value); + ecc_chip + .field_chip() + .range() + .gate() + .assert_bit(ctx, success_minus_all_pairs_zero); + + let op_output = ecc_chip.field_chip().range().gate().load_witness( + ctx, + Value::known(op.output.to_scalar().expect("EcPairing output = {0, 1}")), + ); + ecc_chip.field_chip().range().gate().assert_equal( + ctx, + QuantumCell::Existing(success), + QuantumCell::Existing(op_output), + ); + + log::trace!("[ECC] EcPairingAssignment END:"); + log_context_cursor!(ctx); + + EcPairingDecomposed { + is_valid, + input_cells, + success, + } + } + + /// Handles Phase2 for EcAdd operation and returns the RLC'd x and y co-ordinates of the G1 + /// elements. + fn assign_ec_add( + &self, + ctx: &mut Context, + ec_add_decomposed: &EcAddDecomposed, + ecc_chip: &EccChip>, + keccak_powers: &[QuantumCell], + ) -> EcAddAssigned { + EcAddAssigned { + is_valid: ec_add_decomposed.is_valid, + point_p: G1Assigned { + x_rlc: ecc_chip.field_chip().range().gate().inner_product( + ctx, + ec_add_decomposed.point_p.x_cells.clone(), + keccak_powers.iter().cloned(), + ), + y_rlc: ecc_chip.field_chip().range().gate().inner_product( + ctx, + ec_add_decomposed.point_p.y_cells.clone(), + keccak_powers.iter().cloned(), + ), + }, + point_q: G1Assigned { + x_rlc: ecc_chip.field_chip().range().gate().inner_product( + ctx, + ec_add_decomposed.point_q.x_cells.clone(), + keccak_powers.iter().cloned(), + ), + y_rlc: ecc_chip.field_chip().range().gate().inner_product( + ctx, + ec_add_decomposed.point_q.y_cells.clone(), + keccak_powers.iter().cloned(), + ), + }, + point_r: G1Assigned { + x_rlc: ecc_chip.field_chip().range().gate().inner_product( + ctx, + ec_add_decomposed.point_r.x_cells.clone(), + keccak_powers.iter().cloned(), + ), + y_rlc: ecc_chip.field_chip().range().gate().inner_product( + ctx, + ec_add_decomposed.point_r.y_cells.clone(), + keccak_powers.iter().cloned(), + ), + }, + } + } + + /// Handles Phase2 for EcMul operation and returns the RLC'd x and y co-ordinates of the G1 + /// elements, and the assigned scalar field element. + fn assign_ec_mul( + &self, + ctx: &mut Context, + ec_mul_decomposed: &EcMulDecomposed, + ecc_chip: &EccChip>, + keccak_powers: &[QuantumCell], + ) -> EcMulAssigned { + EcMulAssigned { + is_valid: ec_mul_decomposed.is_valid, + point_p: G1Assigned { + x_rlc: ecc_chip.field_chip().range().gate().inner_product( + ctx, + ec_mul_decomposed.point_p.x_cells.clone(), + keccak_powers.iter().cloned(), + ), + y_rlc: ecc_chip.field_chip().range().gate().inner_product( + ctx, + ec_mul_decomposed.point_p.y_cells.clone(), + keccak_powers.iter().cloned(), + ), + }, + scalar_s: ec_mul_decomposed.scalar_s.clone(), + point_r: G1Assigned { + x_rlc: ecc_chip.field_chip().range().gate().inner_product( + ctx, + ec_mul_decomposed.point_r.x_cells.clone(), + keccak_powers.iter().cloned(), + ), + y_rlc: ecc_chip.field_chip().range().gate().inner_product( + ctx, + ec_mul_decomposed.point_r.y_cells.clone(), + keccak_powers.iter().cloned(), + ), + }, + } + } + + /// Handles Phase2 for EcPairing operation and returns the RLC'd input bytes. + fn assign_ec_pairing( + &self, + ctx: &mut Context, + ec_pairing_decomposed: &EcPairingDecomposed, + ecc_chip: &EccChip>, + keccak_powers: &[QuantumCell], + ) -> EcPairingAssigned { + EcPairingAssigned { + is_valid: ec_pairing_decomposed.is_valid, + input_rlc: ecc_chip.field_chip().range().gate().inner_product( + ctx, + ec_pairing_decomposed.input_cells.clone().into_iter().rev(), + keccak_powers.iter().cloned(), + ), + success: ec_pairing_decomposed.success, + } + } + + /// Handle G1 point and return its decomposed state. + fn handle_g1( + &self, + ctx: &mut Context, + ecc_chip: &EccChip>, + g1: G1Affine, + powers_of_256: &[QuantumCell], + ) -> G1Decomposed { + let ec_point = ecc_chip.load_private(ctx, (Value::known(g1.x), Value::known(g1.y))); + let (x_cells, y_cells) = self.decompose_g1(g1); + self.assert_crt_repr(ctx, ecc_chip, &ec_point.x, &x_cells, powers_of_256); + self.assert_crt_repr(ctx, ecc_chip, &ec_point.y, &y_cells, powers_of_256); + G1Decomposed { + ec_point, + x_cells, + y_cells, + } + } + + /// Handle a scalar field element and return its assigned state. + fn handle_fr( + &self, + ctx: &mut Context, + fr_chip: &FpConfig, + s: Fr, + ) -> ScalarAssigned { + let scalar = fr_chip.load_private(ctx, FpConfig::::fe_to_witness(&Value::known(s))); + ScalarAssigned { scalar } + } + + /// Precheck a 32-bytes word input supposed to be bn256::Fq and return its CRT integer + /// representation. We also return the LE-bytes and assigned values to indicate whether the + /// value is within Fq::MODULUS and whether or not it is zero. + fn precheck_fq( + &self, + ctx: &mut Context, + ecc_chip: &EccChip>, + word_value: U256, + powers_of_256: &[QuantumCell], + ) -> ( + CRTInteger, // CRT representation. + Vec>, // LE bytes as witness. + AssignedValue, // value < Fq::MODULUS + AssignedValue, // value == 0 + ) { + let value = Value::known(num_bigint::BigInt::from( + num_bigint::BigUint::from_bytes_le(&word_value.to_le_bytes()), + )); + let vec_value = decompose_bigint_option::( + value.as_ref(), + ecc_chip.field_chip.num_limbs, + ecc_chip.field_chip.limb_bits, + ); + let limbs = ecc_chip + .field_chip() + .range() + .gate() + .assign_witnesses(ctx, vec_value); + let native_value = OverflowInteger::evaluate( + ecc_chip.field_chip().range().gate(), + ctx, + &limbs, + ecc_chip.field_chip.limb_bases.iter().cloned(), + ); + let overflow_int = OverflowInteger::construct(limbs, ecc_chip.field_chip.limb_bits); + let crt_int = CRTInteger::construct(overflow_int, native_value, value); + let cells = word_value + .to_le_bytes() + .map(|b| QuantumCell::Witness(Value::known(F::from(b as u64)))); + self.assert_crt_repr(ctx, ecc_chip, &crt_int, &cells, powers_of_256); + let is_lt_mod = ecc_chip.field_chip().is_less_than_p(ctx, &crt_int); + let is_zero = big_is_zero::positive( + ecc_chip.field_chip().range().gate(), + ctx, + &crt_int.truncation, + ); + let is_zero = ecc_chip.field_chip().range().gate().and( + ctx, + QuantumCell::Existing(is_lt_mod), + QuantumCell::Existing(is_zero), + ); + (crt_int, cells.to_vec(), is_lt_mod, is_zero) + } + + /// Return an assigned value that indicates whether the given point is on curve G1 or identity + /// point. + fn is_on_curveg1_or_infinity( + &self, + ctx: &mut Context, + ecc_chip: &EccChip>, + x: &CRTInteger, + x_is_zero: AssignedValue, + y: &CRTInteger, + y_is_zero: AssignedValue, + ) -> AssignedValue { + let lhs = ecc_chip.field_chip().mul_no_carry(ctx, y, y); + let mut rhs = ecc_chip.field_chip().mul(ctx, x, x); + rhs = ecc_chip.field_chip().mul_no_carry(ctx, &rhs, x); + + let b = FpConfig::::fe_to_constant(G1Affine::b()); + rhs = ecc_chip.field_chip().add_constant_no_carry(ctx, &rhs, b); + let mut diff = ecc_chip.field_chip().sub_no_carry(ctx, &lhs, &rhs); + diff = ecc_chip.field_chip().carry_mod(ctx, &diff); + + let is_on_curve = ecc_chip.field_chip().is_zero(ctx, &diff); + + ecc_chip.field_chip().range().gate().or_and( + ctx, + QuantumCell::Existing(is_on_curve), + QuantumCell::Existing(x_is_zero), + QuantumCell::Existing(y_is_zero), + ) + } + + /// Return an assigned value that indicates whether the given point is on curve G2 or identity + /// point. + fn is_on_curveg2_or_infinity( + &self, + ctx: &mut Context, + fp2_chip: &Fp2Chip, Fq2>, + x: &FieldExtPoint>, + x_is_zero: AssignedValue, + y: &FieldExtPoint>, + y_is_zero: AssignedValue, + ) -> AssignedValue { + let lhs = fp2_chip.mul_no_carry(ctx, y, y); + let mut rhs = fp2_chip.mul(ctx, x, x); + rhs = fp2_chip.mul_no_carry(ctx, &rhs, x); + + let b = Fp2Chip::, Fq2>::fe_to_constant(G2Affine::b()); + rhs = fp2_chip.add_constant_no_carry(ctx, &rhs, b); + let mut diff = fp2_chip.sub_no_carry(ctx, &lhs, &rhs); + diff = fp2_chip.carry_mod(ctx, &diff); + + let is_on_curve = fp2_chip.is_zero(ctx, &diff); + + fp2_chip.range().gate().or_and( + ctx, + QuantumCell::Existing(is_on_curve), + QuantumCell::Existing(x_is_zero), + QuantumCell::Existing(y_is_zero), + ) + } + + /// Assert that a CRT integer's bytes representation matches the limb values. + fn assert_crt_repr( + &self, + ctx: &mut Context, + ecc_chip: &EccChip>, + crt_int: &CRTInteger, + bytes: &[QuantumCell], + powers_of_256: &[QuantumCell], + ) { + debug_assert_eq!(bytes.len(), 32); + debug_assert!(powers_of_256.len() >= 11); + + let limbs = [ + bytes[0..11].to_vec(), + bytes[11..22].to_vec(), + bytes[22..32].to_vec(), + ] + .map(|limb_bytes| { + ecc_chip.field_chip().range().gate().inner_product( + ctx, + limb_bytes, + powers_of_256[0..11].to_vec(), + ) + }); + + for (&limb_recovered, &limb_value) in limbs.iter().zip_eq(crt_int.truncation.limbs.iter()) { + ecc_chip.field_chip().range().gate().assert_equal( + ctx, + QuantumCell::Existing(limb_recovered), + QuantumCell::Existing(limb_value), + ); + } + } + + /// Decompose G1 element into cells representing its x and y co-ordinates. + fn decompose_g1(&self, g1: G1Affine) -> (Vec>, Vec>) { + ( + g1.x.to_bytes() + .iter() + .map(|&x| QuantumCell::Witness(Value::known(F::from(u64::from(x))))) + .collect_vec(), + g1.y.to_bytes() + .iter() + .map(|&y| QuantumCell::Witness(Value::known(F::from(u64::from(y))))) + .collect_vec(), + ) + } +} + +impl SubCircuit for EccCircuit { + type Config = EccCircuitConfig; + + fn new_from_block(block: &Block) -> Self { + Self { + max_add_ops: block.circuits_params.max_ec_ops.ec_add, + max_mul_ops: block.circuits_params.max_ec_ops.ec_mul, + max_pairing_ops: block.circuits_params.max_ec_ops.ec_pairing, + add_ops: block.get_ec_add_ops(), + mul_ops: block.get_ec_mul_ops(), + pairing_ops: block.get_ec_pairing_ops(), + _marker: PhantomData, + } + } + + /// Returns number of unusable rows of the SubCircuit, which should be + /// `meta.blinding_factors() + 1`. + fn unusable_rows() -> usize { + [ + KeccakCircuit::::unusable_rows(), + EvmCircuit::::unusable_rows(), + // may include additional subcircuits here + ] + .into_iter() + .max() + .unwrap() + } + + fn synthesize_sub( + &self, + config: &Self::Config, + challenges: &Challenges>, + layouter: &mut impl Layouter, + ) -> Result<(), Error> { + config.fp_config.range.load_lookup_table(layouter)?; + self.assign(layouter, config, challenges)?; + Ok(()) + } + + fn min_num_rows_block(block: &Block) -> (usize, usize) { + let row_num = if block.circuits_params.max_vertical_circuit_rows == 0 { + Self::min_num_rows() + } else { + block.circuits_params.max_vertical_circuit_rows + }; + + let ec_adds = block.get_ec_add_ops().len(); + let ec_muls = block.get_ec_mul_ops().len(); + let ec_pairings = block.get_ec_pairing_ops().len(); + let max_ec_ops = &block.circuits_params.max_ec_ops; + log::debug!("ecc circuit row usage: ecadd {ec_adds}/{}, ecmul {ec_muls}/{}, ecpairing {ec_pairings}/{}", + max_ec_ops.ec_add, max_ec_ops.ec_mul, max_ec_ops.ec_pairing); + + // Instead of showing actual minimum row usage, + // halo2-lib based circuits use min_row_num to represent a percentage of total-used capacity + // This functionality allows l2geth to decide if additional ops can be added. + let min_row_num = [ + (row_num / max_ec_ops.ec_add) * ec_adds, + (row_num / max_ec_ops.ec_mul) * ec_muls, + (row_num / max_ec_ops.ec_pairing) * ec_pairings, + ] + .into_iter() + .max() + .unwrap(); + + (min_row_num, row_num) + } +} diff --git a/zkevm-circuits/src/ecc_circuit/dev.rs b/zkevm-circuits/src/ecc_circuit/dev.rs new file mode 100644 index 0000000000..21147349bd --- /dev/null +++ b/zkevm-circuits/src/ecc_circuit/dev.rs @@ -0,0 +1,46 @@ +use eth_types::Field; +use halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner}, + plonk::{Challenge, Circuit, ConstraintSystem, Error}, +}; + +use crate::{ + table::EccTable, + util::{Challenges, SubCircuit, SubCircuitConfig}, +}; + +use super::{EccCircuit, EccCircuitConfig, EccCircuitConfigArgs}; + +impl Circuit for EccCircuit { + type Config = (EccCircuitConfig, Challenges); + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self::default() + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let ecc_table = EccTable::construct(meta); + let challenges = Challenges::construct(meta); + let challenge_exprs = challenges.exprs(meta); + ( + EccCircuitConfig::new( + meta, + EccCircuitConfigArgs { + ecc_table, + challenges: challenge_exprs, + }, + ), + challenges, + ) + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + let challenge_values = config.1.values(&layouter); + self.synthesize_sub(&config.0, &challenge_values, &mut layouter) + } +} diff --git a/zkevm-circuits/src/ecc_circuit/test.rs b/zkevm-circuits/src/ecc_circuit/test.rs new file mode 100644 index 0000000000..4de6e9fc3b --- /dev/null +++ b/zkevm-circuits/src/ecc_circuit/test.rs @@ -0,0 +1,620 @@ +use std::{ + marker::PhantomData, + ops::{Add, Mul, Neg}, +}; + +use bus_mapping::circuit_input_builder::{ + EcAddOp, EcMulOp, EcPairingOp, EcPairingPair, PrecompileEcParams, +}; +use eth_types::{Field, U256}; +use halo2_proofs::{ + arithmetic::Field as ArithmeticField, + dev::MockProver, + halo2curves::bn256::{Fr, G1Affine, G2Affine}, +}; +use rand::{CryptoRng, Rng, RngCore}; + +use crate::ecc_circuit::EccCircuit; + +fn run( + k: u32, + max_ec_ops: PrecompileEcParams, + add_ops: Vec, + mul_ops: Vec, + pairing_ops: Vec, +) { + let circuit = EccCircuit:: { + max_add_ops: max_ec_ops.ec_add, + max_mul_ops: max_ec_ops.ec_mul, + max_pairing_ops: max_ec_ops.ec_pairing, + add_ops, + mul_ops, + pairing_ops, + _marker: PhantomData, + }; + + let prover = match MockProver::run(k, &circuit, vec![]) { + Ok(prover) => prover, + Err(e) => panic!("{e:#?}"), + }; + + if MUST_FAIL { + if let Ok(()) = prover.verify() { + panic!("expected failure, found success"); + } + } else if let Err(e) = prover.verify() { + panic!("{e:#?}"); + } +} + +trait GenRand { + fn gen_rand(r: &mut R, is_neg: bool) -> Self; +} + +impl GenRand for EcAddOp { + fn gen_rand(mut r: &mut R, is_neg: bool) -> Self { + let p = G1Affine::random(&mut r); + let q = G1Affine::random(&mut r); + let r = if is_neg { + G1Affine::random(&mut r) + } else { + p.add(&q).into() + }; + Self { + p: ( + U256::from_little_endian(&p.x.to_bytes()), + U256::from_little_endian(&p.y.to_bytes()), + ), + q: ( + U256::from_little_endian(&q.x.to_bytes()), + U256::from_little_endian(&q.y.to_bytes()), + ), + r: Some(r), + } + } +} + +impl GenRand for EcMulOp { + fn gen_rand(mut r: &mut R, is_neg: bool) -> Self { + let p = G1Affine::random(&mut r); + let s = match r.gen::() { + true => ::random(&mut r), + false => Fr::one(), + }; + let r = if is_neg { + G1Affine::random(&mut r) + } else { + p.mul(&s).into() + }; + Self { + p: ( + U256::from_little_endian(&p.x.to_bytes()), + U256::from_little_endian(&p.y.to_bytes()), + ), + s, + r: Some(r), + } + } +} + +impl GenRand for EcPairingOp { + fn gen_rand(mut r: &mut R, is_neg: bool) -> Self { + let alpha = Fr::random(&mut r); + let beta = Fr::random(&mut r); + let point_p = G1Affine::from(G1Affine::generator() * alpha); + let point_p_negated = point_p.neg(); + let point_q = G2Affine::from(G2Affine::generator() * beta); + let point_s = G1Affine::from(G1Affine::generator() * alpha * beta); + let point_t = G2Affine::generator(); + + let alpha = Fr::random(&mut r); + let beta = Fr::random(&mut r); + let point_a = G1Affine::from(G1Affine::generator() * alpha); + let point_a_negated = point_a.neg(); + let point_b = G2Affine::from(G2Affine::generator() * beta); + let point_c = G1Affine::from(G1Affine::generator() * alpha * beta); + let point_d = G2Affine::generator(); + + let mut pairs = [ + EcPairingPair::new(point_p_negated, point_q), + EcPairingPair::new(point_s, point_t), + EcPairingPair::new(point_a_negated, point_b), + EcPairingPair::new(point_c, point_d), + ]; + let output = eth_types::U256::one(); + + if is_neg { + match r.gen::() { + // change output. + true => Self { + pairs, + output: eth_types::U256::one() - output, + ..Default::default() + }, + // change a point in one of the pairs. + false => { + let altered: G1Affine = point_p_negated.add(&G1Affine::generator()).into(); + pairs[0].g1_point.0 = U256::from_little_endian(&altered.x.to_bytes()); + pairs[0].g1_point.1 = U256::from_little_endian(&altered.y.to_bytes()); + Self { + pairs, + output, + ..Default::default() + } + } + } + } else { + Self { + pairs, + output, + ..Default::default() + } + } + } +} + +fn gen(mut r: &mut R, max_len: usize, is_neg: bool) -> Vec { + std::iter::repeat(0) + .take(max_len) + .map(move |_| T::gen_rand(&mut r, is_neg)) + .collect() +} + +mod valid_invalid_cases { + use super::*; + use eth_types::word; + use snark_verifier::util::arithmetic::PrimeCurveAffine; + use std::sync::LazyLock; + + pub(crate) static EC_ADD_OPS: LazyLock> = LazyLock::new(|| { + vec![ + // 1. valid: P == Q == G1::generator + { + let p = G1Affine::generator(); + EcAddOp { + p: (U256::from(1), U256::from(2)), + q: (U256::from(1), U256::from(2)), + r: Some(p.add(&p).into()), + } + }, + // 2. invalid: P not on curve + EcAddOp { + p: (U256::from(2), U256::from(3)), + q: (U256::from(1), U256::from(2)), + r: None, + }, + // 3. valid: all zeroes + EcAddOp { + p: (U256::zero(), U256::zero()), + q: (U256::zero(), U256::zero()), + r: Some(G1Affine::identity()), + }, + // 4. invalid: Px and Py > Fq::MODULUS + EcAddOp { + p: ( + word!("0x30644E72E131A029B85045B68181585D97816A916871CA8D3C208C16D87CFD48"), /* p + 1 */ + word!("0x30644E72E131A029B85045B68181585D97816A916871CA8D3C208C16D87CFD49"), /* p + 2 */ + ), + q: (U256::from(1), U256::from(2)), + r: None, + }, + // 5. valid: P == -Q + EcAddOp { + p: (U256::from(1), U256::from(2)), + q: ( + U256::from(1), + word!("0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd45"), + ), + r: Some(G1Affine::identity()), + }, + ] + }); + + pub(crate) static EC_MUL_OPS: LazyLock> = LazyLock::new(|| { + vec![ + // 1. valid: P = G1::generator, s = 3 + EcMulOp { + p: (U256::from(1), U256::from(2)), + s: Fr::from(3), + r: Some({ + let p = G1Affine::generator(); + let s = Fr::from(3); + p.mul(s).into() + }), + }, + // 2. invalid: P = (2, 3), i.e. not on curve + EcMulOp { + p: (U256::from(2), U256::from(3)), + s: Fr::from(3), + r: None, + }, + // 3. invalid: P == (p + 1, p + 2), i.e. > Fq::MODULUS + EcMulOp { + p: ( + word!("0x30644E72E131A029B85045B68181585D97816A916871CA8D3C208C16D87CFD48"), /* p + 1 */ + word!("0x30644E72E131A029B85045B68181585D97816A916871CA8D3C208C16D87CFD49"), /* p + 2 */ + ), + s: Fr::from(3), + r: None, + }, + ] + }); + pub(crate) static EC_PAIRING_OPS1: LazyLock> = LazyLock::new(|| { + vec![ + // 1. valid: pairing_check == 1 + { + let alpha = Fr::from(0x102030); + let beta = Fr::from(0x413121); + let point_p = G1Affine::from(G1Affine::generator() * alpha); + let point_p_negated = point_p.neg(); + let point_q = G2Affine::from(G2Affine::generator() * beta); + let point_s = G1Affine::from(G1Affine::generator() * alpha * beta); + let point_t = G2Affine::generator(); + let pairs = [ + EcPairingPair::new(point_p_negated, point_q), + EcPairingPair::new(point_s, point_t), + EcPairingPair::padding_pair(), + EcPairingPair::padding_pair(), + ]; + EcPairingOp { + pairs, + output: U256::one(), + ..Default::default() + } + }, + // 2. invalid: field element > Fq::MODULUS, mod p is OK + { + let alpha = Fr::from(0x102030); + let beta = Fr::from(0x413121); + let point_p = G1Affine::from(G1Affine::generator() * alpha); + let point_p_negated = point_p.neg(); + let point_q = G2Affine::from(G2Affine::generator() * beta); + let point_t = G2Affine::from(G2Affine::generator() * alpha * beta); + let pairs = [ + EcPairingPair::new(point_p_negated, point_q), + EcPairingPair { + g1_point: ( + word!("0x30644E72E131A029B85045B68181585D97816A916871CA8D3C208C16D87CFD48"), + word!("0x30644E72E131A029B85045B68181585D97816A916871CA8D3C208C16D87CFD49"), + ), + g2_point: ( + U256::from_little_endian(&point_t.x.c1.to_bytes()), + U256::from_little_endian(&point_t.x.c0.to_bytes()), + U256::from_little_endian(&point_t.y.c1.to_bytes()), + U256::from_little_endian(&point_t.y.c0.to_bytes()), + ), + }, + EcPairingPair::padding_pair(), + EcPairingPair::padding_pair(), + ]; + EcPairingOp { + pairs, + output: U256::zero(), + ..Default::default() + } + }, + ] + }); + pub(crate) static EC_PAIRING_OPS2: LazyLock> = LazyLock::new(|| { + vec![ + // 3. valid: pairing_check == 0 + { + let alpha = Fr::from(0x102030); + let beta = Fr::from(0x413121); + let gamma = Fr::from(0x591242); + let point_p = G1Affine::from(G1Affine::generator() * alpha); + let point_p_negated = point_p.neg(); + let point_q = G2Affine::from(G2Affine::generator() * beta); + let point_s = G1Affine::from(G1Affine::generator() * gamma); + let point_t = G2Affine::generator(); + let pairs = [ + EcPairingPair::new(point_p_negated, point_q), + EcPairingPair::new(point_s, point_t), + EcPairingPair::padding_pair(), + EcPairingPair::padding_pair(), + ]; + EcPairingOp { + pairs, + output: U256::zero(), + ..Default::default() + } + }, + // 4. invalid: not on curve G1. + EcPairingOp { + pairs: [ + EcPairingPair { + g1_point: (U256::from(3), U256::from(4)), + g2_point: (U256::zero(), U256::zero(), U256::zero(), U256::zero()), + }, + EcPairingPair::padding_pair(), + EcPairingPair::padding_pair(), + EcPairingPair::padding_pair(), + ], + output: 0.into(), + ..Default::default() + }, + ] + }); + pub(crate) static EC_PAIRING_OPS3: LazyLock> = LazyLock::new(|| { + vec![ + // 5. invalid: not on curve G2. + EcPairingOp { + pairs: [ + EcPairingPair { + g1_point: (U256::zero(), U256::zero()), + g2_point: (U256::from(3), U256::from(4), U256::from(5), U256::from(6)), + }, + EcPairingPair::padding_pair(), + EcPairingPair::padding_pair(), + EcPairingPair::padding_pair(), + ], + output: 0.into(), + ..Default::default() + }, + // 6. valid: all zero. + EcPairingOp { + pairs: [ + EcPairingPair::padding_pair(), + EcPairingPair::padding_pair(), + EcPairingPair::padding_pair(), + EcPairingPair::padding_pair(), + ], + output: 1.into(), + ..Default::default() + }, + ] + }); + pub(crate) static EC_PAIRING_OPS4: LazyLock> = LazyLock::new(|| { + vec![ + // 7. valid: [(G1::gen, G2::gen), (-G1::gen, G2::gen)] + EcPairingOp { + pairs: [ + EcPairingPair::new(G1Affine::generator(), G2Affine::generator()), + EcPairingPair::new(G1Affine::generator().neg(), G2Affine::generator()), + EcPairingPair::padding_pair(), + EcPairingPair::padding_pair(), + ], + output: 1.into(), + ..Default::default() + }, + // 8. valid: [(G1::gen, G2::gen), (-G1::gen, G2::gen); 2] + EcPairingOp { + pairs: [ + EcPairingPair::new(G1Affine::generator(), G2Affine::generator()), + EcPairingPair::new(G1Affine::generator().neg(), G2Affine::generator()), + EcPairingPair::new(G1Affine::generator(), G2Affine::generator()), + EcPairingPair::new(G1Affine::generator().neg(), G2Affine::generator()), + ], + output: 1.into(), + ..Default::default() + }, + ] + }); +} + +#[test] +fn test_ecc_circuit_valid_invalid() { + use crate::ecc_circuit::util::LOG_TOTAL_NUM_ROWS; + use halo2_proofs::halo2curves::bn256::Fr; + use valid_invalid_cases::{ + EC_ADD_OPS, EC_MUL_OPS, EC_PAIRING_OPS1, EC_PAIRING_OPS2, EC_PAIRING_OPS3, EC_PAIRING_OPS4, + }; + + run::( + LOG_TOTAL_NUM_ROWS, + PrecompileEcParams::default(), + EC_ADD_OPS.clone(), + EC_MUL_OPS.clone(), + EC_PAIRING_OPS1.clone(), + ); + + run::( + LOG_TOTAL_NUM_ROWS, + PrecompileEcParams { + ec_add: 0, + ec_mul: 0, + ec_pairing: 2, + }, + vec![], + vec![], + EC_PAIRING_OPS2.clone(), + ); + + run::( + LOG_TOTAL_NUM_ROWS, + PrecompileEcParams { + ec_add: 0, + ec_mul: 0, + ec_pairing: 2, + }, + vec![], + vec![], + EC_PAIRING_OPS3.clone(), + ); + + run::( + LOG_TOTAL_NUM_ROWS, + PrecompileEcParams { + ec_add: 0, + ec_mul: 0, + ec_pairing: 2, + }, + vec![], + vec![], + EC_PAIRING_OPS4.clone(), + ); +} + +#[ignore = "generate a lot of random invalid inputs for bn254 add"] +#[test] +fn test_invalid_ec_add() { + use crate::ecc_circuit::util::LOG_TOTAL_NUM_ROWS; + use halo2_proofs::halo2curves::{bn256::Fq, group::ff::PrimeField}; + use itertools::Itertools; + use num::Integer; + use rand::thread_rng; + + let u256_max = U256::from_little_endian(&[0xff; 32]); + + // 1. p is on g1 but p.x > p and p.y < p + // and p.x is close to 2^256 + // and q is generator + let mut rng = thread_rng(); + let get_g1_from_x_coordinate = |x: Fq| { + let b = Fq::from(3); + // y^2 = x^3 + b (mod Fq) + let y2 = x * x * x + b; + let y = Fq::sqrt(&y2); + y.map(|y| { + ( + U256::from_little_endian(&x.to_repr()), + U256::from_little_endian(&y.to_repr()), + ) + }) + }; + let ec_adds = (0..50) + .map(|_| { + let px = u256_max - (rng.next_u64() % 1024); + let x = Fq::from_raw(px.0); + (px, get_g1_from_x_coordinate(x)) + }) + .filter(|(_, p)| p.is_some().into()) + .map(|(px, p)| { + let p = (px, p.unwrap().1); + let q = (U256::from(1), U256::from(2)); + + EcAddOp { p, q, r: None } + }) + .collect_vec(); + + run::( + LOG_TOTAL_NUM_ROWS, + PrecompileEcParams { + ec_add: ec_adds.len(), + ec_mul: 0, + ec_pairing: 0, + }, + ec_adds, + vec![], + vec![], + ); + + // 2. p is on g1 but p.x[i] is close to Fq::MODULUS.limbs[i] and p.y < p + // and q is generator + let ec_adds = (0..50) + .map(|_| { + let fq_limbs: [u64; 4] = [ + 0x3c208c16d87cfd47, + 0x97816a916871ca8d, + 0xb85045b68181585d, + 0x30644e72e131a029, + ]; + let mut px_limbs = [0u64; 4]; + px_limbs.iter_mut().enumerate().for_each(|(j, limb)| { + if j.is_odd() { + *limb = fq_limbs[j] + rng.next_u64() % 1024; + } else { + *limb = fq_limbs[j] - rng.next_u64() % 1024; + } + }); + let x = Fq::from_raw(px_limbs); + let px = U256(px_limbs); + (px, get_g1_from_x_coordinate(x)) + }) + .filter(|(_, p)| p.is_some().into()) + .map(|(px, p)| { + let p = (px, p.unwrap().1); + let q = (U256::from(1), U256::from(2)); + + EcAddOp { p, q, r: None } + }) + .collect_vec(); + + run::( + LOG_TOTAL_NUM_ROWS, + PrecompileEcParams { + ec_add: ec_adds.len(), + ec_mul: 0, + ec_pairing: 0, + }, + ec_adds, + vec![], + vec![], + ); +} + +#[test] +fn test_ecc_circuit_positive() { + use crate::ecc_circuit::util::LOG_TOTAL_NUM_ROWS; + use halo2_proofs::halo2curves::bn256::Fr; + + let mut rng = rand::thread_rng(); + + run::( + LOG_TOTAL_NUM_ROWS, + PrecompileEcParams::default(), + gen(&mut rng, 9, false), + gen(&mut rng, 9, false), + gen(&mut rng, 1, false), + ); +} + +#[test] +fn test_ecc_circuit_negative() { + use crate::ecc_circuit::util::LOG_TOTAL_NUM_ROWS; + use halo2_proofs::halo2curves::bn256::Fr; + + let mut rng = rand::thread_rng(); + + run::( + LOG_TOTAL_NUM_ROWS, + PrecompileEcParams::default(), + gen(&mut rng, 9, true), + gen(&mut rng, 9, true), + gen(&mut rng, 1, true), + ); +} + +#[test] +fn variadic_size_check() { + use crate::ecc_circuit::util::LOG_TOTAL_NUM_ROWS; + use halo2_proofs::halo2curves::bn256::Fr; + use valid_invalid_cases::{EC_ADD_OPS, EC_MUL_OPS, EC_PAIRING_OPS1, EC_PAIRING_OPS2}; + + let mut rng = rand::thread_rng(); + + let default_params = PrecompileEcParams::default(); + + let circuit = EccCircuit:: { + max_add_ops: default_params.ec_add, + max_mul_ops: default_params.ec_mul, + max_pairing_ops: default_params.ec_pairing, + add_ops: gen(&mut rng, 25, false), + mul_ops: gen(&mut rng, 20, false), + pairing_ops: EC_PAIRING_OPS1.clone(), + _marker: PhantomData, + }; + let prover1 = MockProver::::run(LOG_TOTAL_NUM_ROWS, &circuit, vec![]).unwrap(); + + let circuit = EccCircuit:: { + max_add_ops: default_params.ec_add, + max_mul_ops: default_params.ec_mul, + max_pairing_ops: default_params.ec_pairing, + add_ops: { + let mut ops = gen(&mut rng, 30, false); + ops.extend_from_slice(&EC_ADD_OPS); + ops + }, + mul_ops: { + let mut ops = gen(&mut rng, 30, false); + ops.extend_from_slice(&EC_MUL_OPS); + ops + }, + pairing_ops: EC_PAIRING_OPS2.clone(), + _marker: PhantomData, + }; + let prover2 = MockProver::::run(LOG_TOTAL_NUM_ROWS, &circuit, vec![]).unwrap(); + + assert_eq!(prover1.fixed(), prover2.fixed()); + assert_eq!(prover1.permutation(), prover2.permutation()); +} diff --git a/zkevm-circuits/src/ecc_circuit/util.rs b/zkevm-circuits/src/ecc_circuit/util.rs new file mode 100644 index 0000000000..92c49c35d3 --- /dev/null +++ b/zkevm-circuits/src/ecc_circuit/util.rs @@ -0,0 +1,107 @@ +use eth_types::Field; +use halo2_base::{AssignedValue, QuantumCell}; +use halo2_ecc::{bigint::CRTInteger, ecc::EcPoint, fields::FieldExtPoint}; + +// Total number of rows allowable for ECC circuit +pub const LOG_TOTAL_NUM_ROWS: u32 = 20; + +// Cell usage accounting for EcAdd, EcMul and EcPairing +// Roud up to nearest 100 +pub(super) const EC_ADD_CELLS: usize = 6_900; // actual: 6_851 +pub(super) const EC_MUL_CELLS: usize = 405_500; // actual: 405_476 +pub(super) const EC_PAIRING_CELLS: usize = 6_627_500; // actual: 6_627_442 +pub(super) const COLUMN_NUM_LIMIT: usize = 150; // Max number of columns allowed + +/// Decomposed state of a G1 curve point. +pub(super) struct G1Decomposed { + /// EcPoint on G1. + pub ec_point: EcPoint>, + /// Cells for the x-coordinate of the G1 curve point in LE format. + pub x_cells: Vec>, + /// Cells for the y-coordinate of the G1 curve point in LE format. + pub y_cells: Vec>, +} + +/// State of the G1 curve point after RLC operations. +pub(super) struct G1Assigned { + /// RLC of x-coordinate that will be copied to the ECC Table. + pub x_rlc: AssignedValue, + /// RLC of y-coordinate that will be copied to the ECC Table. + pub y_rlc: AssignedValue, +} + +/// State of a scalar field element. Since the scalar fits within the field, we don't need to take +/// RLC over its bytes/cells. Hence we can decompose and assign the scalar in the first phase. +#[derive(Clone)] +pub(super) struct ScalarAssigned { + pub scalar: CRTInteger, +} + +/// Decomposed state of a G2 curve point. +pub(super) struct G2Decomposed { + /// The assigned EcPoint. + pub ec_point: EcPoint>>, + /// Cells for the coeff 0 of x-coordinate of the G2 curve point in LE format. + pub x_c0_cells: Vec>, + /// Cells for the coeff 1 of x-coordinate of the G2 curve point in LE format. + pub x_c1_cells: Vec>, + /// Cells for the coeff 0 of y-coordinate of the G2 curve point in LE format. + pub y_c0_cells: Vec>, + /// Cells for the coeff 1 of y-coordinate of the G2 curve point in LE format. + pub y_c1_cells: Vec>, +} + +/// State of EcAdd operation post first phase. +pub(super) struct EcAddDecomposed { + pub is_valid: AssignedValue, + pub point_p: G1Decomposed, + pub point_q: G1Decomposed, + pub point_r: G1Decomposed, +} + +/// State of EcAdd operation post second phase. +pub(super) struct EcAddAssigned { + pub is_valid: AssignedValue, + pub point_p: G1Assigned, + pub point_q: G1Assigned, + pub point_r: G1Assigned, +} + +/// State of EcMul operation post first phase. +pub(super) struct EcMulDecomposed { + pub is_valid: AssignedValue, + pub point_p: G1Decomposed, + pub scalar_s: ScalarAssigned, + pub point_r: G1Decomposed, +} + +/// State of EcMul operation post second phase. +pub(super) struct EcMulAssigned { + pub is_valid: AssignedValue, + pub point_p: G1Assigned, + pub scalar_s: ScalarAssigned, + pub point_r: G1Assigned, +} + +/// State of EcPairing operation post first phase. +pub(super) struct EcPairingDecomposed { + pub is_valid: AssignedValue, + pub input_cells: Vec>, + pub success: AssignedValue, +} + +/// State of EcPairing operation post second phase. +pub(super) struct EcPairingAssigned { + pub is_valid: AssignedValue, + /// RLC of (G1, G2) pairs. + pub input_rlc: AssignedValue, + pub success: AssignedValue, +} + +/// Wrapper struct that holds all the ecXX states post second phase. +#[derive(Default)] +pub(super) struct EcOpsAssigned { + pub ec_adds_assigned: Vec>, + pub ec_muls_assigned: Vec>, + pub ec_pairings_assigned: Vec>, +} diff --git a/zkevm-circuits/src/evm_circuit.rs b/zkevm-circuits/src/evm_circuit.rs index 7afff29d6d..6e1e5b1147 100644 --- a/zkevm-circuits/src/evm_circuit.rs +++ b/zkevm-circuits/src/evm_circuit.rs @@ -21,8 +21,8 @@ pub use crate::witness; use crate::{ evm_circuit::param::{MAX_STEP_HEIGHT, STEP_STATE_HEIGHT}, table::{ - BlockTable, BytecodeTable, CopyTable, ExpTable, KeccakTable, LookupTable, RwTable, TxTable, - UXTable, + BlockTable, BytecodeTable, CopyTable, ExpTable, KeccakTable, LookupTable, RwTable, + SigTable, TxTable, UXTable, }, util::{Challenges, SubCircuit, SubCircuitConfig}, }; @@ -50,6 +50,7 @@ pub struct EvmCircuitConfig { copy_table: CopyTable, keccak_table: KeccakTable, exp_table: ExpTable, + sig_table: SigTable, } /// Circuit configuration arguments @@ -74,6 +75,8 @@ pub struct EvmCircuitConfigArgs { pub u8_table: UXTable<8>, /// U16Table pub u16_table: UXTable<16>, + /// SigTable + pub sig_table: SigTable, /// Feature config pub feature_config: FeatureConfig, } @@ -95,6 +98,7 @@ impl SubCircuitConfig for EvmCircuitConfig { exp_table, u8_table, u16_table, + sig_table, feature_config, }: Self::ConfigArgs, ) -> Self { @@ -112,6 +116,7 @@ impl SubCircuitConfig for EvmCircuitConfig { ©_table, &keccak_table, &exp_table, + &sig_table, feature_config, )); @@ -129,6 +134,7 @@ impl SubCircuitConfig for EvmCircuitConfig { exp_table.annotate_columns(meta); u8_table.annotate_columns(meta); u16_table.annotate_columns(meta); + sig_table.annotate_columns(meta); Self { fixed_table, @@ -142,6 +148,7 @@ impl SubCircuitConfig for EvmCircuitConfig { copy_table, keccak_table, exp_table, + sig_table, } } } @@ -389,6 +396,7 @@ impl Circuit for EvmCircuit { let challenges = Challenges::construct(meta); let challenges_expr = challenges.exprs(meta); + let sig_table = SigTable::construct(meta); ( EvmCircuitConfig::new( meta, @@ -403,6 +411,7 @@ impl Circuit for EvmCircuit { exp_table, u8_table, u16_table, + sig_table, feature_config: params, }, ), @@ -448,6 +457,7 @@ impl Circuit for EvmCircuit { config.u8_table.load(&mut layouter)?; config.u16_table.load(&mut layouter)?; + config.sig_table.dev_load(&mut layouter, block)?; self.synthesize_sub(&config, &challenges, &mut layouter) } diff --git a/zkevm-circuits/src/evm_circuit/execution.rs b/zkevm-circuits/src/evm_circuit/execution.rs index 1f7ed1c19e..0e106b8412 100644 --- a/zkevm-circuits/src/evm_circuit/execution.rs +++ b/zkevm-circuits/src/evm_circuit/execution.rs @@ -2,7 +2,7 @@ use super::{ param::{ BLOCK_TABLE_LOOKUPS, BYTECODE_TABLE_LOOKUPS, COPY_TABLE_LOOKUPS, EXP_TABLE_LOOKUPS, FIXED_TABLE_LOOKUPS, KECCAK_TABLE_LOOKUPS, N_COPY_COLUMNS, N_PHASE1_COLUMNS, N_U16_LOOKUPS, - N_U8_LOOKUPS, RW_TABLE_LOOKUPS, TX_TABLE_LOOKUPS, + N_U8_LOOKUPS, RW_TABLE_LOOKUPS, SIG_TABLE_LOOKUPS, TX_TABLE_LOOKUPS, }, step::HasExecutionState, util::{instrumentation::Instrument, CachedRegion, StoredExpression}, @@ -190,7 +190,7 @@ use opcode_not::NotGadget; use origin::OriginGadget; use pc::PcGadget; use pop::PopGadget; -use precompiles::IdentityGadget; +use precompiles::{EcrecoverGadget, IdentityGadget}; use push::PushGadget; use return_revert::ReturnRevertGadget; use returndatacopy::ReturnDataCopyGadget; @@ -337,6 +337,8 @@ pub struct ExecutionConfig { error_invalid_creation_code: Box>, error_precompile_failed: Box>, error_return_data_out_of_bound: Box>, + // precompile calls + precompile_ecrecover_gadget: Box>, precompile_identity_gadget: Box>, invalid_tx: Option>>, } @@ -357,6 +359,7 @@ impl ExecutionConfig { copy_table: &dyn LookupTable, keccak_table: &dyn LookupTable, exp_table: &dyn LookupTable, + sig_table: &dyn LookupTable, feature_config: FeatureConfig, ) -> Self { let mut instrument = Instrument::default(); @@ -615,6 +618,7 @@ impl ExecutionConfig { error_return_data_out_of_bound: configure_gadget!(), // precompile calls precompile_identity_gadget: configure_gadget!(), + precompile_ecrecover_gadget: configure_gadget!(), // step and presets step: step_curr, height_map, @@ -635,6 +639,7 @@ impl ExecutionConfig { copy_table, keccak_table, exp_table, + sig_table, &challenges, &cell_manager, ); @@ -880,6 +885,7 @@ impl ExecutionConfig { copy_table: &dyn LookupTable, keccak_table: &dyn LookupTable, exp_table: &dyn LookupTable, + sig_table: &dyn LookupTable, challenges: &Challenges>, cell_manager: &CellManager, ) { @@ -899,6 +905,7 @@ impl ExecutionConfig { Table::Copy => copy_table, Table::Keccak => keccak_table, Table::Exp => exp_table, + Table::Sig => sig_table, } .table_exprs(meta); vec![( @@ -1110,6 +1117,7 @@ impl ExecutionConfig { ("EVM_lookup_copy", COPY_TABLE_LOOKUPS), ("EVM_lookup_keccak", KECCAK_TABLE_LOOKUPS), ("EVM_lookup_exp", EXP_TABLE_LOOKUPS), + ("EVM_lookup_sig", SIG_TABLE_LOOKUPS), ("EVM_adv_phase2", N_PHASE2_COLUMNS), ("EVM_copy", N_COPY_COLUMNS), ("EVM_lookup_u8", N_U8_LOOKUPS), @@ -1410,7 +1418,9 @@ impl ExecutionConfig { ExecutionState::ErrorPrecompileFailed => { assign_exec_step!(self.error_precompile_failed) } - // precompile calls + ExecutionState::PrecompileEcrecover => { + assign_exec_step!(self.precompile_ecrecover_gadget) + } ExecutionState::PrecompileIdentity => { assign_exec_step!(self.precompile_identity_gadget) } diff --git a/zkevm-circuits/src/evm_circuit/execution/error_oog_precompile.rs b/zkevm-circuits/src/evm_circuit/execution/error_oog_precompile.rs index 5242dbc363..6514a47790 100644 --- a/zkevm-circuits/src/evm_circuit/execution/error_oog_precompile.rs +++ b/zkevm-circuits/src/evm_circuit/execution/error_oog_precompile.rs @@ -64,10 +64,10 @@ impl ExecutionGadget for ErrorOOGPrecompileGadget { // calculate required gas for precompile let precompiles_required_gas = [ - // ( - // addr_bits.value_equals(PrecompileCalls::ECRecover), - // GasCost::PRECOMPILE_ECRECOVER_BASE.expr(), - // ), + ( + addr_bits.value_equals(PrecompileCalls::Ecrecover), + GasCost::PRECOMPILE_ECRECOVER_BASE.expr(), + ), // addr_bits.value_equals(PrecompileCalls::Sha256), // addr_bits.value_equals(PrecompileCalls::Ripemd160), // addr_bits.value_equals(PrecompileCalls::Blake2F), @@ -181,6 +181,7 @@ impl ExecutionGadget for ErrorOOGPrecompileGadget { // required_gas let precompile_call: PrecompileCalls = precompile_addr.to_fixed_bytes()[19].into(); let required_gas = match precompile_call { + PrecompileCalls::Ecrecover => precompile_call.base_gas_cost(), // PrecompileCalls::Bn128Pairing => { // precompile_call.base_gas_cost() // + n_pairs * GasCost::PRECOMPILE_BN256PAIRING_PER_PAIR @@ -189,8 +190,8 @@ impl ExecutionGadget for ErrorOOGPrecompileGadget { let n_words = (call.call_data_length + 31) / 32; precompile_call.base_gas_cost() + n_words * GasCost::PRECOMPILE_IDENTITY_PER_WORD } - // PrecompileCalls::Bn128Add | PrecompileCalls::Bn128Mul | PrecompileCalls::ECRecover => - // { precompile_call.base_gas_cost() + // PrecompileCalls::Bn128Add | PrecompileCalls::Bn128Mul => { + // precompile_call.base_gas_cost() // } _ => unreachable!(), }; diff --git a/zkevm-circuits/src/evm_circuit/execution/precompiles/ecrecover.rs b/zkevm-circuits/src/evm_circuit/execution/precompiles/ecrecover.rs new file mode 100644 index 0000000000..1e14430c99 --- /dev/null +++ b/zkevm-circuits/src/evm_circuit/execution/precompiles/ecrecover.rs @@ -0,0 +1,575 @@ +use bus_mapping::precompile::{PrecompileAuxData, PrecompileCalls}; +use eth_types::{evm_types::GasCost, word, Field, ToLittleEndian, ToScalar, U256}; +use ethers_core::k256::elliptic_curve::PrimeField; +use gadgets::util::{and, not, or, select, Expr}; +use halo2_proofs::{circuit::Value, halo2curves::secp256k1::Fq, plonk::Error}; + +use crate::{ + evm_circuit::{ + execution::ExecutionGadget, + step::ExecutionState, + util::{ + common_gadget::RestoreContextGadget, + constraint_builder::{ConstrainBuilderCommon, EVMConstraintBuilder}, + from_bytes, + math_gadget::{IsEqualGadget, IsZeroGadget, IsZeroWordGadget, LtWordGadget, ModGadget}, + CachedRegion, Cell, + }, + }, + table::CallContextFieldTag, + util::word::{Word32Cell, WordExpr, WordLimbs, WordLoHi, WordLoHiCell}, + witness::{Block, Call, ExecStep, Transaction}, +}; + +#[derive(Clone, Debug)] +pub struct EcrecoverGadget { + is_recovered: Cell, + recovered_addr: Cell, + + fq_modulus: Word32Cell, + msg_hash: Word32Cell, + msg_hash_raw: Word32Cell, + msg_hash_mod: ModGadget, + sig_r: Word32Cell, + sig_s: Word32Cell, + sig_v: WordLoHiCell, + + sig_r_canonical: LtWordGadget, + sig_s_canonical: LtWordGadget, + is_zero_sig_r: IsZeroWordGadget>, + is_zero_sig_s: IsZeroWordGadget>, + + is_zero_sig_v_hi: IsZeroGadget, + is_sig_v_27: IsEqualGadget, + is_sig_v_28: IsEqualGadget, + + is_success: Cell, + callee_address: Cell, + caller_id: Cell, + restore_context: RestoreContextGadget, +} + +impl ExecutionGadget for EcrecoverGadget { + const EXECUTION_STATE: ExecutionState = ExecutionState::PrecompileEcrecover; + const NAME: &'static str = "ECRECOVER"; + + fn configure(cb: &mut EVMConstraintBuilder) -> Self { + let is_recovered = cb.query_bool(); + let recovered_addr = cb.query_cell(); + + let fq_modulus = cb.query_word32(); + let msg_hash = cb.query_word32(); + let msg_hash_raw = cb.query_word32(); + let sig_r = cb.query_word32(); + let sig_s = cb.query_word32(); + let sig_v = cb.query_word_unchecked(); + + let msg_hash_mod = ModGadget::construct(cb, [&msg_hash_raw, &fq_modulus, &msg_hash]); + + // verify sig_r and sig_s + // the range is 0 < sig_r/sig_s < Fq::MODULUS + let mut sig_r_be = sig_r.limbs.clone(); + let mut sig_s_be = sig_s.limbs.clone(); + sig_r_be.reverse(); + sig_s_be.reverse(); + let sig_r_canonical = LtWordGadget::construct( + cb, + &WordLimbs::new(sig_r_be.clone()).to_word(), + &fq_modulus.to_word(), + ); + let sig_s_canonical = LtWordGadget::construct( + cb, + &WordLimbs::new(sig_s_be.clone()).to_word(), + &fq_modulus.to_word(), + ); + let is_zero_sig_r = IsZeroWordGadget::construct(cb, &sig_r); + let is_zero_sig_s = IsZeroWordGadget::construct(cb, &sig_s); + let is_valid_r_s = and::expr([ + sig_r_canonical.expr(), + sig_s_canonical.expr(), + not::expr(or::expr([is_zero_sig_r.expr(), is_zero_sig_s.expr()])), + ]); + + // sig_v is valid if sig_v == 27 || sig_v == 28 + let is_zero_sig_v_hi = IsZeroGadget::construct(cb, sig_v.hi().expr()); + let is_sig_v_27 = IsEqualGadget::construct(cb, sig_v.lo().expr(), 27.expr()); + let is_sig_v_28 = IsEqualGadget::construct(cb, sig_v.lo().expr(), 28.expr()); + let is_valid_sig_v = and::expr([ + or::expr([is_sig_v_27.expr(), is_sig_v_28.expr()]), + is_zero_sig_v_hi.expr(), + ]); + + let [is_success, callee_address, caller_id] = [ + CallContextFieldTag::IsSuccess, + CallContextFieldTag::CalleeAddress, + CallContextFieldTag::CallerId, + ] + .map(|tag| cb.call_context(None, tag)); + + let input_len = PrecompileCalls::Ecrecover.input_len().unwrap(); + for (field_tag, value) in [ + (CallContextFieldTag::CallDataOffset, 0.expr()), + (CallContextFieldTag::CallDataLength, input_len.expr()), + ( + CallContextFieldTag::ReturnDataOffset, + select::expr(is_recovered.expr(), input_len.expr(), 0.expr()), + ), + ( + CallContextFieldTag::ReturnDataLength, + select::expr(is_recovered.expr(), 32.expr(), 0.expr()), + ), + ] { + cb.call_context_lookup_read(None, field_tag, WordLoHi::from_lo_unchecked(value)); + } + + let gas_cost = select::expr( + is_success.expr(), + GasCost::PRECOMPILE_ECRECOVER_BASE.expr(), + cb.curr.state.gas_left.expr(), + ); + + // lookup to the sign_verify table: + let is_valid_sig = and::expr([is_valid_r_s.expr(), is_valid_sig_v.expr()]); + cb.condition(is_valid_sig.expr(), |cb| { + let mut msg_hash_le = msg_hash.limbs.clone(); + msg_hash_le.reverse(); + cb.sig_table_lookup( + WordLimbs::new(msg_hash_le).to_word(), + sig_v.lo().expr() - 27.expr(), + sig_r.to_word(), + sig_s.to_word(), + select::expr(is_recovered.expr(), recovered_addr.expr(), 0.expr()), + is_recovered.expr(), + ); + }); + + cb.condition(not::expr(is_valid_sig.expr()), |cb| { + cb.require_zero( + "is_recovered == false if r, s or v not canonical", + is_recovered.expr(), + ); + }); + + cb.condition(not::expr(is_recovered.expr()), |cb| { + cb.require_zero( + "address == 0 if address could not be recovered", + recovered_addr.expr(), + ); + }); + + cb.precompile_info_lookup( + cb.execution_state().as_u64().expr(), + callee_address.expr(), + cb.execution_state().precompile_base_gas_cost().expr(), + ); + + let restore_context = RestoreContextGadget::construct2( + cb, + is_success.expr(), + gas_cost.expr(), + 0.expr(), + 0.expr(), + select::expr(is_recovered.expr(), 32.expr(), 0.expr()), + 0.expr(), + 0.expr(), + ); + + Self { + is_recovered, + recovered_addr, + fq_modulus, + + msg_hash, + msg_hash_raw, + msg_hash_mod, + sig_r, + sig_s, + sig_v, + + sig_r_canonical, + sig_s_canonical, + is_zero_sig_v_hi, + is_zero_sig_r, + is_zero_sig_s, + is_sig_v_27, + is_sig_v_28, + + is_success, + callee_address, + caller_id, + restore_context, + } + } + + fn assign_exec_step( + &self, + region: &mut CachedRegion<'_, '_, F>, + offset: usize, + block: &Block, + _tx: &Transaction, + call: &Call, + step: &ExecStep, + ) -> Result<(), Error> { + if let Some(PrecompileAuxData::Ecrecover(aux_data)) = &step.aux_data { + let recovered = !aux_data.recovered_addr.is_zero(); + self.is_recovered + .assign(region, offset, Value::known(F::from(recovered as u64)))?; + let mut recovered_addr = aux_data.recovered_addr.to_fixed_bytes(); + recovered_addr.reverse(); + self.recovered_addr.assign( + region, + offset, + Value::known(from_bytes::value(&recovered_addr)), + )?; + self.fq_modulus + .assign_u256(region, offset, word!(Fq::MODULUS))?; + + let sig_r = U256::from(aux_data.sig_r.to_le_bytes()); + let sig_s = U256::from(aux_data.sig_s.to_le_bytes()); + self.sig_r.assign_u256(region, offset, sig_r)?; + self.sig_s.assign_u256(region, offset, sig_s)?; + self.sig_v.assign_u256(region, offset, aux_data.sig_v)?; + + let (quotient, remainder) = aux_data.msg_hash.div_mod(word!(Fq::MODULUS)); + self.msg_hash_raw + .assign_u256(region, offset, aux_data.msg_hash)?; + self.msg_hash.assign_u256(region, offset, remainder)?; + self.msg_hash_mod.assign( + region, + offset, + aux_data.msg_hash, + word!(Fq::MODULUS), + remainder, + quotient, + )?; + + self.sig_r_canonical + .assign(region, offset, aux_data.sig_r, word!(Fq::MODULUS))?; + self.sig_s_canonical + .assign(region, offset, aux_data.sig_s, word!(Fq::MODULUS))?; + self.is_zero_sig_r.assign_u256(region, offset, sig_r)?; + self.is_zero_sig_s.assign_u256(region, offset, sig_s)?; + + let sig_v_bytes = aux_data.sig_v.to_le_bytes(); + self.is_zero_sig_v_hi + .assign(region, offset, from_bytes::value(&sig_v_bytes[16..]))?; + self.is_sig_v_27 + .assign(region, offset, F::from(sig_v_bytes[0] as u64), F::from(27))?; + self.is_sig_v_28 + .assign(region, offset, F::from(sig_v_bytes[0] as u64), F::from(28))?; + } + + self.is_success.assign( + region, + offset, + Value::known(F::from(u64::from(call.is_success))), + )?; + + self.callee_address.assign( + region, + offset, + Value::known(call.code_address().unwrap().to_scalar().unwrap()), + )?; + self.caller_id + .assign(region, offset, Value::known(F::from(call.caller_id as u64)))?; + + self.restore_context + .assign(region, offset, block, call, step, 7) + } +} + +#[cfg(test)] +mod test { + use bus_mapping::{ + evm::OpcodeId, + precompile::{PrecompileCallArgs, PrecompileCalls}, + }; + use eth_types::{bytecode, word, ToWord}; + use mock::TestContext; + // use rayon::{iter::ParallelIterator, prelude::IntoParallelRefIterator}; + + use crate::test_util::CircuitTestBuilder; + + lazy_static::lazy_static! { + static ref TEST_VECTOR: Vec = { + vec![ + PrecompileCallArgs { + name: "ecrecover (valid sig, addr recovered)", + setup_code: bytecode! { + // msg hash from 0x00 + PUSH32(word!("0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3")) + PUSH1(0x00) + MSTORE + // signature v from 0x20 + PUSH1(28) + PUSH1(0x20) + MSTORE + // signature r from 0x40 + PUSH32(word!("0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608")) + PUSH1(0x40) + MSTORE + // signature s from 0x60 + PUSH32(word!("0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada")) + PUSH1(0x60) + MSTORE + }, + // copy 128 bytes from memory addr 0. Address is recovered and the signature is + // valid. + call_data_offset: 0x00.into(), + call_data_length: 0x80.into(), + // return 32 bytes and write from memory addr 128 + ret_offset: 0x80.into(), + ret_size: 0x20.into(), + address: PrecompileCalls::Ecrecover.address().to_word(), + ..Default::default() + }, + + PrecompileCallArgs { + name: "ecrecover (overflowing msg_hash)", + setup_code: bytecode! { + // msg hash from 0x00 + PUSH32(word!("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee")) + PUSH1(0x00) + MSTORE + // signature v from 0x20 + PUSH1(28) + PUSH1(0x20) + MSTORE + // signature r from 0x40 + PUSH32(word!("0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608")) + PUSH1(0x40) + MSTORE + // signature s from 0x60 + PUSH32(word!("0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada")) + PUSH1(0x60) + MSTORE + }, + call_data_offset: 0x00.into(), + call_data_length: 0x80.into(), + ret_offset: 0x80.into(), + ret_size: 0x20.into(), + address: PrecompileCalls::Ecrecover.address().to_word(), + ..Default::default() + }, + + PrecompileCallArgs { + name: "ecrecover (invalid overflowing sig_r)", + setup_code: bytecode! { + // msg hash from 0x00 + PUSH32(word!("0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3")) + PUSH1(0x00) + MSTORE + // signature v from 0x20 + PUSH1(28) + PUSH1(0x20) + MSTORE + // signature r from 0x40 + PUSH32(word!("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee")) + PUSH1(0x40) + MSTORE + // signature s from 0x60 + PUSH32(word!("0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada")) + PUSH1(0x60) + MSTORE + }, + call_data_offset: 0x00.into(), + call_data_length: 0x80.into(), + ret_offset: 0x00.into(), + ret_size: 0x00.into(), + address: PrecompileCalls::Ecrecover.address().to_word(), + ..Default::default() + }, + + PrecompileCallArgs { + name: "ecrecover (invalid overflowing sig_s)", + setup_code: bytecode! { + // msg hash from 0x00 + PUSH32(word!("0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3")) + PUSH1(0x00) + MSTORE + // signature v from 0x20 + PUSH1(28) + PUSH1(0x20) + MSTORE + // signature r from 0x40 + PUSH32(word!("0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608")) + PUSH1(0x40) + MSTORE + // signature s from 0x60 + PUSH32(word!("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee")) + PUSH1(0x60) + MSTORE + }, + call_data_offset: 0x00.into(), + call_data_length: 0x80.into(), + ret_offset: 0x00.into(), + ret_size: 0x00.into(), + address: PrecompileCalls::Ecrecover.address().to_word(), + ..Default::default() + }, + + PrecompileCallArgs { + name: "ecrecover (invalid v > 28, single byte)", + setup_code: bytecode! { + // msg hash from 0x00 + PUSH32(word!("0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3")) + PUSH1(0x00) + MSTORE + // signature v from 0x20 + PUSH1(29) + PUSH1(0x20) + MSTORE + // signature r from 0x40 + PUSH32(word!("0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608")) + PUSH1(0x40) + MSTORE + // signature s from 0x60 + PUSH32(word!("0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada")) + PUSH1(0x60) + MSTORE + }, + call_data_offset: 0x00.into(), + call_data_length: 0x80.into(), + ret_offset: 0x00.into(), + ret_size: 0x00.into(), + address: PrecompileCalls::Ecrecover.address().to_word(), + ..Default::default() + }, + + PrecompileCallArgs { + name: "ecrecover (invalid v < 27, single byte)", + setup_code: bytecode! { + // msg hash from 0x00 + PUSH32(word!("0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3")) + PUSH1(0x00) + MSTORE + // signature v from 0x20 + PUSH1(26) + PUSH1(0x20) + MSTORE + // signature r from 0x40 + PUSH32(word!("0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608")) + PUSH1(0x40) + MSTORE + // signature s from 0x60 + PUSH32(word!("0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada")) + PUSH1(0x60) + MSTORE + }, + call_data_offset: 0x00.into(), + call_data_length: 0x80.into(), + ret_offset: 0x00.into(), + ret_size: 0x00.into(), + address: PrecompileCalls::Ecrecover.address().to_word(), + ..Default::default() + }, + PrecompileCallArgs { + name: "ecrecover (invalid v, multi-byte, last byte == 28)", + setup_code: bytecode! { + // msg hash from 0x00 + PUSH32(word!("0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3")) + PUSH1(0x00) + MSTORE + // signature v from 0x20, 1c == 28 (but not single byte) + PUSH32(word!("0x010000000000000000000000000000000000000000000000000000000000001c")) + PUSH1(0x20) + MSTORE + // signature r from 0x40 + PUSH32(word!("0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608")) + PUSH1(0x40) + MSTORE + // signature s from 0x60 + PUSH32(word!("0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada")) + PUSH1(0x60) + MSTORE + }, + call_data_offset: 0x00.into(), + call_data_length: 0x80.into(), + ret_offset: 0x00.into(), + ret_size: 0x00.into(), + address: PrecompileCalls::Ecrecover.address().to_word(), + ..Default::default() + }, + ] + }; + } + + lazy_static::lazy_static! { + static ref OOG_TEST_VECTOR: Vec = { + vec![PrecompileCallArgs { + name: "ecrecover (oog)", + setup_code: bytecode! { + // msg hash from 0x00 + PUSH32(word!("0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3")) + PUSH1(0x00) + MSTORE + // signature v from 0x20 + PUSH1(28) + PUSH1(0x20) + MSTORE + // signature r from 0x40 + PUSH32(word!("0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608")) + PUSH1(0x40) + MSTORE + // signature s from 0x60 + PUSH32(word!("0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada")) + PUSH1(0x60) + MSTORE + }, + // copy 128 bytes from memory addr 0. Address is recovered and the signature is + // valid. + call_data_offset: 0x00.into(), + call_data_length: 0x80.into(), + // return 32 bytes and write from memory addr 128 + ret_offset: 0x80.into(), + ret_size: 0x20.into(), + gas: 0.into(), + value: 2.into(), + address: PrecompileCalls::Ecrecover.address().to_word(), + ..Default::default() + }] + }; + } + + #[test] + fn precompile_ecrecover_test() { + let call_kinds = vec![ + OpcodeId::CALL, + OpcodeId::STATICCALL, + OpcodeId::DELEGATECALL, + OpcodeId::CALLCODE, + ]; + + TEST_VECTOR.iter().for_each(|test_vector| { + for &call_kind in &call_kinds { + let bytecode = test_vector.with_call_op(call_kind); + + CircuitTestBuilder::new_from_test_ctx( + TestContext::<2, 1>::simple_ctx_with_bytecode(bytecode).unwrap(), + ) + .run(); + } + }); + } + + #[test] + fn precompile_ecrecover_oog_test() { + let call_kinds = vec![ + OpcodeId::CALL, + OpcodeId::STATICCALL, + OpcodeId::DELEGATECALL, + OpcodeId::CALLCODE, + ]; + + OOG_TEST_VECTOR.iter().for_each(|test_vector| { + for &call_kind in &call_kinds { + let bytecode = test_vector.with_call_op(call_kind); + + CircuitTestBuilder::new_from_test_ctx( + TestContext::<2, 1>::simple_ctx_with_bytecode(bytecode).unwrap(), + ) + .run(); + } + }) + } +} diff --git a/zkevm-circuits/src/evm_circuit/execution/precompiles/mod.rs b/zkevm-circuits/src/evm_circuit/execution/precompiles/mod.rs index 942ed62ef4..5aad1f14fe 100644 --- a/zkevm-circuits/src/evm_circuit/execution/precompiles/mod.rs +++ b/zkevm-circuits/src/evm_circuit/execution/precompiles/mod.rs @@ -1,2 +1,5 @@ +mod ecrecover; +pub use ecrecover::EcrecoverGadget; + mod identity; pub use identity::IdentityGadget; diff --git a/zkevm-circuits/src/evm_circuit/param.rs b/zkevm-circuits/src/evm_circuit/param.rs index 4a06970258..c69cfeb503 100644 --- a/zkevm-circuits/src/evm_circuit/param.rs +++ b/zkevm-circuits/src/evm_circuit/param.rs @@ -42,7 +42,8 @@ pub(crate) const EVM_LOOKUP_COLS: usize = FIXED_TABLE_LOOKUPS + BLOCK_TABLE_LOOKUPS + COPY_TABLE_LOOKUPS + KECCAK_TABLE_LOOKUPS - + EXP_TABLE_LOOKUPS; + + EXP_TABLE_LOOKUPS + + SIG_TABLE_LOOKUPS; /// Lookups done per row. pub const LOOKUP_CONFIG: &[(Table, usize)] = &[ @@ -54,6 +55,7 @@ pub const LOOKUP_CONFIG: &[(Table, usize)] = &[ (Table::Copy, COPY_TABLE_LOOKUPS), (Table::Keccak, KECCAK_TABLE_LOOKUPS), (Table::Exp, EXP_TABLE_LOOKUPS), + (Table::Sig, SIG_TABLE_LOOKUPS), ]; /// Fixed Table lookups done in EVMCircuit @@ -80,6 +82,9 @@ pub const KECCAK_TABLE_LOOKUPS: usize = 1; /// Exp Table lookups done in EVMCircuit pub const EXP_TABLE_LOOKUPS: usize = 1; +/// Sig Table lookups done in EVMCircuit +pub const SIG_TABLE_LOOKUPS: usize = 1; + /// Maximum number of bytes that an integer can fit in field without wrapping /// around. pub(crate) const MAX_N_BYTES_INTEGER: usize = 31; diff --git a/zkevm-circuits/src/evm_circuit/step.rs b/zkevm-circuits/src/evm_circuit/step.rs index b17b7619eb..85deb21ba6 100644 --- a/zkevm-circuits/src/evm_circuit/step.rs +++ b/zkevm-circuits/src/evm_circuit/step.rs @@ -36,7 +36,7 @@ use strum_macros::EnumIter; impl From for ExecutionState { fn from(value: PrecompileCalls) -> Self { match value { - PrecompileCalls::ECRecover => ExecutionState::PrecompileEcRecover, + PrecompileCalls::Ecrecover => ExecutionState::PrecompileEcrecover, PrecompileCalls::Sha256 => ExecutionState::PrecompileSha256, PrecompileCalls::Ripemd160 => ExecutionState::PrecompileRipemd160, PrecompileCalls::Identity => ExecutionState::PrecompileIdentity, @@ -158,7 +158,7 @@ pub enum ExecutionState { ErrorOutOfGasCREATE, ErrorOutOfGasSELFDESTRUCT, // Precompiles - PrecompileEcRecover, + PrecompileEcrecover, PrecompileSha256, PrecompileRipemd160, PrecompileIdentity, @@ -330,7 +330,7 @@ impl From<&ExecStep> for ExecutionState { } } ExecState::Precompile(precompile) => match precompile { - PrecompileCalls::ECRecover => ExecutionState::PrecompileEcRecover, + PrecompileCalls::Ecrecover => ExecutionState::PrecompileEcrecover, PrecompileCalls::Sha256 => ExecutionState::PrecompileSha256, PrecompileCalls::Ripemd160 => ExecutionState::PrecompileRipemd160, PrecompileCalls::Identity => ExecutionState::PrecompileIdentity, @@ -370,7 +370,7 @@ impl ExecutionState { pub(crate) fn is_precompiled(&self) -> bool { matches!( self, - Self::PrecompileEcRecover + Self::PrecompileEcrecover | Self::PrecompileSha256 | Self::PrecompileRipemd160 | Self::PrecompileIdentity @@ -384,7 +384,7 @@ impl ExecutionState { pub(crate) fn precompile_base_gas_cost(&self) -> u64 { (match self { - Self::PrecompileEcRecover => PrecompileCalls::ECRecover, + Self::PrecompileEcrecover => PrecompileCalls::Ecrecover, Self::PrecompileSha256 => PrecompileCalls::Sha256, Self::PrecompileRipemd160 => PrecompileCalls::Ripemd160, Self::PrecompileIdentity => PrecompileCalls::Identity, diff --git a/zkevm-circuits/src/evm_circuit/table.rs b/zkevm-circuits/src/evm_circuit/table.rs index a42b2c0e0c..da51d63c69 100644 --- a/zkevm-circuits/src/evm_circuit/table.rs +++ b/zkevm-circuits/src/evm_circuit/table.rs @@ -141,7 +141,7 @@ impl FixedTableTag { ), Self::PrecompileInfo => Box::new( vec![ - PrecompileCalls::ECRecover, + PrecompileCalls::Ecrecover, PrecompileCalls::Sha256, PrecompileCalls::Ripemd160, PrecompileCalls::Identity, @@ -191,6 +191,8 @@ pub enum Table { Keccak, /// Lookup for exp table Exp, + /// Lookup for sig table + Sig, } #[derive(Clone, Debug)] @@ -359,6 +361,15 @@ pub(crate) enum Lookup { exponent_lo_hi: [Expression; 2], exponentiation_lo_hi: [Expression; 2], }, + SigTable { + msg_hash: WordLoHi>, + sig_v: Expression, + sig_r: WordLoHi>, + sig_s: WordLoHi>, + recovered_addr: Expression, + is_valid: Expression, + }, + /// Conditional lookup enabled by the first element. Conditional(Expression, Box>), } @@ -378,6 +389,7 @@ impl Lookup { Self::CopyTable { .. } => Table::Copy, Self::KeccakTable { .. } => Table::Keccak, Self::ExpTable { .. } => Table::Exp, + Self::SigTable { .. } => Table::Sig, Self::Conditional(_, lookup) => lookup.table(), } } @@ -496,6 +508,25 @@ impl Lookup { exponentiation_lo_hi[0].clone(), exponentiation_lo_hi[1].clone(), ], + Self::SigTable { + msg_hash, + sig_v, + sig_r, + sig_s, + recovered_addr, + is_valid, + } => vec![ + 1.expr(), // q_enable + msg_hash.lo(), + msg_hash.hi(), + sig_v.clone(), + sig_r.lo(), + sig_r.hi(), + sig_s.lo(), + sig_s.hi(), + recovered_addr.clone(), + is_valid.clone(), + ], Self::Conditional(condition, lookup) => lookup .input_exprs() .into_iter() diff --git a/zkevm-circuits/src/evm_circuit/util/constraint_builder.rs b/zkevm-circuits/src/evm_circuit/util/constraint_builder.rs index faa52eb515..01342631f3 100644 --- a/zkevm-circuits/src/evm_circuit/util/constraint_builder.rs +++ b/zkevm-circuits/src/evm_circuit/util/constraint_builder.rs @@ -1490,7 +1490,30 @@ impl<'a, F: Field> EVMConstraintBuilder<'a, F> { ); } - // Keccak Table + /// Sig Table + pub(crate) fn sig_table_lookup( + &mut self, + msg_hash: WordLoHi>, + sig_v: Expression, + sig_r: WordLoHi>, + sig_s: WordLoHi>, + recovered_addr: Expression, + is_valid: Expression, + ) { + self.add_lookup( + "sig table", + Lookup::SigTable { + msg_hash, + sig_v, + sig_r, + sig_s, + recovered_addr, + is_valid, + }, + ); + } + + /// Keccak Table pub(crate) fn keccak_table_lookup( &mut self, input_rlc: Expression, diff --git a/zkevm-circuits/src/evm_circuit/util/instrumentation.rs b/zkevm-circuits/src/evm_circuit/util/instrumentation.rs index 1674fcb636..a238a542f8 100644 --- a/zkevm-circuits/src/evm_circuit/util/instrumentation.rs +++ b/zkevm-circuits/src/evm_circuit/util/instrumentation.rs @@ -104,6 +104,9 @@ impl Instrument { CellType::Lookup(Table::Exp) => { report.exp_table = data_entry; } + CellType::Lookup(Table::Sig) => { + report.sig_table = data_entry; + } } } report_collection.push(report); @@ -131,6 +134,7 @@ pub struct ExecStateReport { pub copy_table: StateReportRow, pub keccak_table: StateReportRow, pub exp_table: StateReportRow, + pub sig_table: StateReportRow, } impl From for ExecStateReport { diff --git a/zkevm-circuits/src/evm_circuit/util/precompile_gadget.rs b/zkevm-circuits/src/evm_circuit/util/precompile_gadget.rs index da0ce68e32..0929467d41 100644 --- a/zkevm-circuits/src/evm_circuit/util/precompile_gadget.rs +++ b/zkevm-circuits/src/evm_circuit/util/precompile_gadget.rs @@ -39,6 +39,7 @@ impl PrecompileGadget { let conditions = vec![ address.value_equals(PrecompileCalls::Identity), + address.value_equals(PrecompileCalls::Ecrecover), // match more precompiles ] .into_iter() @@ -49,6 +50,7 @@ impl PrecompileGadget { let next_states = vec![ ExecutionState::PrecompileIdentity, // add more precompile execution states + ExecutionState::PrecompileEcrecover, ]; let constraints: Vec> = vec![ @@ -56,10 +58,18 @@ impl PrecompileGadget { // Identity cb.require_equal( "input length and precompile return length are the same", - cd_length, - precompile_return_length, + cd_length.clone(), + precompile_return_length.clone(), ); - }), // add more precompile constraint closures + }), + Box::new(|cb| { + // EcRecover + cb.require_equal( + "ECRecover: input length is 128 bytes", + cd_length.clone(), + 128.expr(), + ); + }), ]; cb.constrain_mutually_exclusive_next_step(conditions, next_states, constraints); diff --git a/zkevm-circuits/src/lib.rs b/zkevm-circuits/src/lib.rs index 56bf0d4934..d3924cf133 100644 --- a/zkevm-circuits/src/lib.rs +++ b/zkevm-circuits/src/lib.rs @@ -34,6 +34,7 @@ pub mod mpt_circuit; pub mod pi_circuit; #[cfg(not(target_arch = "wasm32"))] pub mod root_circuit; +pub mod sig_circuit; pub mod state_circuit; pub mod super_circuit; pub mod table; diff --git a/zkevm-circuits/src/root_circuit/test.rs b/zkevm-circuits/src/root_circuit/test.rs index 5474c18385..de87bd58e1 100644 --- a/zkevm-circuits/src/root_circuit/test.rs +++ b/zkevm-circuits/src/root_circuit/test.rs @@ -32,6 +32,7 @@ fn test_root_circuit() { max_bytecode: 512, max_evm_rows: 0, max_keccak_rows: 0, + max_vertical_circuit_rows: 0, }; let (k, circuit, instance, _) = SuperCircuit::<_>::build(block_1tx(), circuits_params, TEST_MOCK_RANDOMNESS.into()) diff --git a/zkevm-circuits/src/sig_circuit.rs b/zkevm-circuits/src/sig_circuit.rs new file mode 100644 index 0000000000..7cb3883815 --- /dev/null +++ b/zkevm-circuits/src/sig_circuit.rs @@ -0,0 +1,1049 @@ +//! Circuit to verify multiple ECDSA secp256k1 signatures. +// This module uses halo2-ecc's ecdsa chip +// - to prove the correctness of secp signatures +// - to compute the RLC in circuit +// - to perform keccak lookup table +// +// Naming notes: +// - *_be: Big-Endian bytes +// - *_le: Little-Endian bytes + +#[cfg(any(test, feature = "test-circuits"))] +mod dev; +mod ecdsa; +#[cfg(test)] +mod test; +mod utils; + +use crate::{ + evm_circuit::{util::not, EvmCircuit}, + keccak_circuit::KeccakCircuit, + sig_circuit::{ + ecdsa::ecdsa_verify_no_pubkey_check, + utils::{calc_required_advices, FpChip}, + }, + table::{KeccakTable, SigTable}, + util::{word::WordLoHi, Challenges, Expr, SubCircuit, SubCircuitConfig}, +}; +use eth_types::{ + self, + sign_types::{pk_bytes_le, pk_bytes_swap_endianness, SignData}, + Field, +}; +use halo2_base::{ + gates::{range::RangeConfig, GateInstructions, RangeInstructions}, + utils::modulus, + AssignedValue, Context, QuantumCell, SKIP_FIRST_PASS, +}; +use halo2_ecc::{ + bigint::CRTInteger, + ecc::EccChip, + fields::{ + fp::{FpConfig, FpStrategy}, + FieldChip, + }, +}; +pub(crate) use utils::*; + +use halo2_proofs::{ + circuit::{Layouter, Value}, + halo2curves::secp256k1::{Fp, Fq, Secp256k1Affine}, + plonk::{Advice, Column, ConstraintSystem, Error, Expression, Selector}, + poly::Rotation, +}; + +use ethers_core::utils::keccak256; +use itertools::Itertools; +use log::error; +use std::{iter, marker::PhantomData}; + +/// Circuit configuration arguments +pub struct SigCircuitConfigArgs { + /// KeccakTable + pub _keccak_table: KeccakTable, + /// SigTable + pub sig_table: SigTable, + /// Challenges + pub challenges: Challenges>, +} + +/// SignVerify Configuration +#[derive(Debug, Clone)] +pub struct SigCircuitConfig +where + F: Field + halo2_base::utils::ScalarField, +{ + /// ECDSA + ecdsa_config: FpChip, + // ecdsa_config: FpConfig, + /// An advice column to store RLC witnesses + rlc_column: Column, + /// selector for keccak lookup table + q_keccak: Selector, + /// Used to lookup pk->pk_hash(addr) + _keccak_table: KeccakTable, + /// The exposed table to be used by tx circuit and ecrecover + sig_table: SigTable, +} + +impl SubCircuitConfig for SigCircuitConfig +where + F: Field + halo2_base::utils::ScalarField, +{ + type ConfigArgs = SigCircuitConfigArgs; + + /// Return a new SigConfig + fn new( + meta: &mut ConstraintSystem, + Self::ConfigArgs { + _keccak_table, + sig_table, + challenges: _, + }: Self::ConfigArgs, + ) -> Self { + // need an additional phase 2 column/basic gate to hold the witnesses during RLC + // computations + let num_advice = [calc_required_advices(MAX_NUM_SIG), 1]; + let num_lookup_advice = [calc_required_lookup_advices(MAX_NUM_SIG)]; + log::info!("configuring ECDSA chip with multiple phases"); + + let ecdsa_config = FpConfig::configure( + meta, + FpStrategy::Simple, + &num_advice, + &num_lookup_advice, + 1, + LOG_TOTAL_NUM_ROWS - 1, + LIMB_BITS, + NUM_LIMBS, + modulus::(), + 0, + LOG_TOTAL_NUM_ROWS, // maximum k of the chip + ); + + // we need one phase 2 column to store RLC results + let rlc_column = meta.advice_column_in(halo2_proofs::plonk::SecondPhase); + + meta.enable_equality(rlc_column); + + meta.enable_equality(sig_table.recovered_addr); + meta.enable_equality(sig_table.sig_r.lo()); + meta.enable_equality(sig_table.sig_r.hi()); + meta.enable_equality(sig_table.sig_s.lo()); + meta.enable_equality(sig_table.sig_s.hi()); + meta.enable_equality(sig_table.sig_v); + meta.enable_equality(sig_table.is_valid); + meta.enable_equality(sig_table.msg_hash.lo()); + meta.enable_equality(sig_table.msg_hash.hi()); + + // Ref. spec SignVerifyChip 1. Verify that keccak(pub_key_bytes) = pub_key_hash + // by keccak table lookup, where pub_key_bytes is built from the pub_key + // in the ecdsa_chip. + let q_keccak = meta.complex_selector(); + + meta.lookup_any("keccak lookup table", |meta| { + // When address is 0, we disable the signature verification by using a dummy pk, + // msg_hash and signature which is not constrained to match msg_hash_rlc nor + // the address. + // Layout: + // | q_keccak | rlc | + // | -------- | --------------- | + // | 1 | is_address_zero | + // | | pk_rlc | + // | | pk_hash_lo | + // | | pk_hash_hi | + let q_keccak = meta.query_selector(q_keccak); + let is_address_zero = meta.query_advice(rlc_column, Rotation::cur()); + let is_enable = q_keccak * not::expr(is_address_zero); + + let input = [ + is_enable.clone(), + is_enable.clone() * meta.query_advice(rlc_column, Rotation(1)), + is_enable.clone() * 64usize.expr(), + is_enable.clone() * meta.query_advice(rlc_column, Rotation(2)), + is_enable * meta.query_advice(rlc_column, Rotation(3)), + ]; + let table = [ + meta.query_advice(_keccak_table.is_enabled, Rotation::cur()), + meta.query_advice(_keccak_table.input_rlc, Rotation::cur()), + meta.query_advice(_keccak_table.input_len, Rotation::cur()), + meta.query_advice(_keccak_table.output.lo(), Rotation::cur()), + meta.query_advice(_keccak_table.output.hi(), Rotation::cur()), + ]; + + input.into_iter().zip(table).collect() + }); + + Self { + ecdsa_config, + _keccak_table, + sig_table, + q_keccak, + rlc_column, + } + } +} + +/// Verify a message hash is signed by the public +/// key corresponding to an Ethereum Address. +#[derive(Clone, Debug, Default)] +pub struct SigCircuit { + /// Max number of verifications + pub max_verif: usize, + /// Without padding + pub signatures: Vec, + /// Marker + pub _marker: PhantomData, +} + +impl SubCircuit for SigCircuit { + type Config = SigCircuitConfig; + + fn new_from_block(block: &crate::witness::Block) -> Self { + assert!(block.circuits_params.max_txs <= MAX_NUM_SIG); + + SigCircuit { + max_verif: MAX_NUM_SIG, + signatures: block.get_sign_data(true), + _marker: Default::default(), + } + } + + /// Returns number of unusable rows of the SubCircuit, which should be + /// `meta.blinding_factors() + 1`. + fn unusable_rows() -> usize { + [ + KeccakCircuit::::unusable_rows(), + EvmCircuit::::unusable_rows(), + // may include additional subcircuits here + ] + .into_iter() + .max() + .unwrap() + } + + fn synthesize_sub( + &self, + config: &Self::Config, + challenges: &Challenges>, + layouter: &mut impl Layouter, + ) -> Result<(), Error> { + config.ecdsa_config.range.load_lookup_table(layouter)?; + self.assign(config, layouter, &self.signatures, challenges)?; + Ok(()) + } + + // Since sig circuit / halo2-lib use veticle cell assignment, + // so the returned pair is consisted of same values + fn min_num_rows_block(block: &crate::witness::Block) -> (usize, usize) { + let row_num = if block.circuits_params.max_vertical_circuit_rows == 0 { + Self::min_num_rows() + } else { + block.circuits_params.max_vertical_circuit_rows + }; + + let ecdsa_verif_count = + block.txs.len() + block.precompile_events.get_ecrecover_events().len(); + // Reserve one ecdsa verification for padding tx such that the bad case in which some tx + // calls MAX_NUM_SIG - 1 ecrecover precompile won't happen. If that case happens, the sig + // circuit won't have more space for the padding tx's ECDSA verification. Then the + // prover won't be able to produce any valid proof. + let max_num_verif = MAX_NUM_SIG - 1; + + // Instead of showing actual minimum row usage, + // halo2-lib based circuits use min_row_num to represent a percentage of total-used capacity + // This functionality allows l2geth to decide if additional ops can be added. + let min_row_num = (row_num / max_num_verif) * ecdsa_verif_count; + + (min_row_num, row_num) + } +} + +impl SigCircuit { + /// Return a new SigCircuit + pub fn new(max_verif: usize) -> Self { + Self { + max_verif, + signatures: Vec::new(), + _marker: PhantomData, + } + } + + /// Return the minimum number of rows required to prove an input of a + /// particular size. + pub fn min_num_rows() -> usize { + // SigCircuit can't determine usable rows independently. + // Instead, the blinding area is determined by other advise columns with most counts of + // rotation queries. This value is typically determined by either the Keccak or EVM + // circuit. + + // the cells are allocated vertically, i.e., given a TOTAL_NUM_ROWS * NUM_ADVICE + // matrix, the allocator will try to use all the cells in the first column, then + // the second column, etc. + + let max_blinding_factor = Self::unusable_rows() - 1; + + // same formula as halo2-lib's FlexGate + (1 << LOG_TOTAL_NUM_ROWS) - (max_blinding_factor + 3) + } +} + +impl SigCircuit { + /// Verifies the ecdsa relationship. I.e., prove that the signature + /// is (in)valid or not under the given public key and the message hash in + /// the circuit. Does not enforce the signature is valid. + /// + /// Returns the cells for + /// - public keys + /// - message hashes + /// - a boolean whether the signature is correct or not + /// + /// WARNING: this circuit does not enforce the returned value to be true + /// make sure the caller checks this result! + fn assign_ecdsa( + &self, + ctx: &mut Context, + ecdsa_chip: &FpChip, + sign_data: &SignData, + ) -> Result>, Error> { + let gate = ecdsa_chip.gate(); + let zero = gate.load_zero(ctx); + + let SignData { + signature, + pk, + msg: _, + msg_hash, + } = sign_data; + let (sig_r, sig_s, v) = signature; + + // build ecc chip from Fp chip + let ecc_chip = EccChip::>::construct(ecdsa_chip.clone()); + let pk_assigned = ecc_chip.load_private(ctx, (Value::known(pk.x), Value::known(pk.y))); + let pk_is_valid = ecc_chip.is_on_curve_or_infinity::(ctx, &pk_assigned); + gate.assert_is_const(ctx, &pk_is_valid, F::ONE); + + // build Fq chip from Fp chip + let fq_chip = FqChip::construct(ecdsa_chip.range.clone(), 88, 3, modulus::()); + let integer_r = + fq_chip.load_private(ctx, FqChip::::fe_to_witness(&Value::known(*sig_r))); + let integer_s = + fq_chip.load_private(ctx, FqChip::::fe_to_witness(&Value::known(*sig_s))); + let msg_hash = + fq_chip.load_private(ctx, FqChip::::fe_to_witness(&Value::known(*msg_hash))); + + // returns the verification result of ecdsa signature + // + // WARNING: this circuit does not enforce the returned value to be true + // make sure the caller checks this result! + let (sig_is_valid, pk_is_zero, y_coord, y_coord_is_zero) = + ecdsa_verify_no_pubkey_check::( + &ecc_chip.field_chip, + ctx, + &pk_assigned, + &integer_r, + &integer_s, + &msg_hash, + 4, + 4, + ); + + // ======================================= + // constrains v == y.is_oddness() + // ======================================= + assert!(*v == 0 || *v == 1, "v is not boolean"); + + // we constrain: + // - v + 2*tmp = y where y is already range checked (88 bits) + // - v is a binary + // - tmp is also < 88 bits (this is crucial otherwise tmp may wrap around and break + // soundness) + + let assigned_y_is_odd = gate.load_witness(ctx, Value::known(F::from(*v as u64))); + gate.assert_bit(ctx, assigned_y_is_odd); + + // the last 88 bits of y + let assigned_y_limb = &y_coord.limbs()[0]; + let mut y_value = F::ZERO; + assigned_y_limb.value().map(|&x| y_value = x); + + // y_tmp = (y_value - y_last_bit)/2 + let y_tmp = (y_value - F::from(*v as u64)) * F::TWO_INV; + let assigned_y_tmp = gate.load_witness(ctx, Value::known(y_tmp)); + + // y_tmp_double = (y_value - y_last_bit) + let y_tmp_double = gate.mul( + ctx, + QuantumCell::Existing(assigned_y_tmp), + QuantumCell::Constant(F::from(2)), + ); + let y_rec = gate.add( + ctx, + QuantumCell::Existing(y_tmp_double), + QuantumCell::Existing(assigned_y_is_odd), + ); + let y_is_ok = gate.is_equal( + ctx, + QuantumCell::Existing(*assigned_y_limb), + QuantumCell::Existing(y_rec), + ); + + // last step we want to constrain assigned_y_tmp is 87 bits + let assigned_y_tmp = gate.select( + ctx, + QuantumCell::Existing(zero), + QuantumCell::Existing(assigned_y_tmp), + QuantumCell::Existing(y_coord_is_zero), + ); + ecc_chip + .field_chip + .range + .range_check(ctx, &assigned_y_tmp, 87); + + let y_coord_not_zero = gate.not(ctx, QuantumCell::Existing(y_coord_is_zero)); + let sig_is_valid = gate.and_many( + ctx, + vec![ + QuantumCell::Existing(sig_is_valid), + QuantumCell::Existing(y_is_ok), + QuantumCell::Existing(y_coord_not_zero), + ], + ); + + Ok(AssignedECDSA { + _pk: pk_assigned, + pk_is_zero, + msg_hash, + integer_r, + integer_s, + v: assigned_y_is_odd, + sig_is_valid, + }) + } + + fn enable_keccak_lookup( + &self, + config: &SigCircuitConfig, + ctx: &mut Context, + offset: usize, + is_address_zero: &AssignedValue, + pk_rlc: &AssignedValue, + pk_hash: &WordLoHi>, + ) -> Result<(), Error> { + log::trace!("keccak lookup"); + + // Layout: + // | q_keccak | rlc | + // | -------- | --------------- | + // | 1 | is_address_zero | + // | | pk_rlc | + // | | pk_hash_lo | + // | | pk_hash_hi | + config.q_keccak.enable(&mut ctx.region, offset)?; + + // is_address_zero + let tmp_cell = ctx.region.assign_advice( + || "is_address_zero", + config.rlc_column, + offset, + || is_address_zero.value, + )?; + ctx.region + .constrain_equal(is_address_zero.cell, tmp_cell.cell())?; + + // pk_rlc + let tmp_cell = ctx.region.assign_advice( + || "pk_rlc", + config.rlc_column, + offset + 1, + || pk_rlc.value, + )?; + ctx.region.constrain_equal(pk_rlc.cell, tmp_cell.cell())?; + + // pk_hash + let pk_cell_lo = ctx.region.assign_advice( + || "pk_hash_lo", + config.rlc_column, + offset + 2, + || pk_hash.lo().value, + )?; + ctx.region + .constrain_equal(pk_hash.lo().cell, pk_cell_lo.cell())?; + let pk_cell_hi = ctx.region.assign_advice( + || "pk_hash_hi", + config.rlc_column, + offset + 3, + || pk_hash.hi().value, + )?; + ctx.region + .constrain_equal(pk_hash.hi().cell, pk_cell_hi.cell())?; + + log::trace!("finished keccak lookup"); + Ok(()) + } + + /// Input the signature data, + /// Output the cells for byte decomposition of the keys and messages + fn sign_data_decomposition( + &self, + ctx: &mut Context, + ecdsa_chip: &FpChip, + sign_data: &SignData, + assigned_data: &AssignedECDSA>, + ) -> Result, Error> { + // build ecc chip from Fp chip + let ecc_chip = EccChip::>::construct(ecdsa_chip.clone()); + + let zero = ecdsa_chip.range.gate.load_zero(ctx); + + // ================================================ + // step 0. powers of aux parameters + // ================================================ + let word_lo_hi_powers = + iter::successors(Some(F::ONE), |coeff| Some(F::from(256) * coeff)).take(32); + let powers_of_256_cells = word_lo_hi_powers + .map(|x| QuantumCell::Constant(x)) + .collect_vec(); + + // ================================================ + // pk hash cells + // ================================================ + let pk_le = pk_bytes_le(&sign_data.pk); + let pk_be = pk_bytes_swap_endianness(&pk_le); + let pk_hash = keccak256(pk_be).map(|byte| Value::known(F::from(byte as u64))); + + log::trace!("pk hash {:0x?}", pk_hash); + let pk_hash_cells = pk_hash + .iter() + .map(|&x| QuantumCell::Witness(x)) + .rev() + .collect_vec(); + + // address is the random linear combination of the public key + // it is fine to use a phase 1 gate here + let address = ecdsa_chip.range.gate.inner_product( + ctx, + powers_of_256_cells[..20].to_vec(), + pk_hash_cells[..20].to_vec(), + ); + let address = ecdsa_chip.range.gate.select( + ctx, + QuantumCell::Existing(zero), + QuantumCell::Existing(address), + QuantumCell::Existing(assigned_data.pk_is_zero), + ); + let is_address_zero = ecdsa_chip.range.gate.is_equal( + ctx, + QuantumCell::Existing(address), + QuantumCell::Existing(zero), + ); + log::trace!("address: {:?}", address.value()); + + // ================================================ + // message hash cells + // ================================================ + + let assert_crt = |ctx: &mut Context, + bytes: [u8; 32], + crt_integer: &CRTInteger| + -> Result<_, Error> { + let byte_cells: Vec> = bytes + .iter() + .map(|&x| QuantumCell::Witness(Value::known(F::from(x as u64)))) + .collect_vec(); + self.assert_crt_int_byte_repr( + ctx, + &ecdsa_chip.range, + crt_integer, + &byte_cells, + &powers_of_256_cells, + )?; + Ok(byte_cells) + }; + + // assert the assigned_msg_hash_le is the right decomposition of msg_hash + // msg_hash is an overflowing integer with 3 limbs, of sizes 88, 88, and 80 + let assigned_msg_hash_le = + assert_crt(ctx, sign_data.msg_hash.to_bytes(), &assigned_data.msg_hash)?; + + // ================================================ + // pk cells + // ================================================ + let pk_x_le = sign_data + .pk + .x + .to_bytes() + .iter() + .map(|&x| QuantumCell::Witness(Value::known(F::from_u128(x as u128)))) + .collect_vec(); + let pk_y_le = sign_data + .pk + .y + .to_bytes() + .iter() + .map(|&y| QuantumCell::Witness(Value::known(F::from_u128(y as u128)))) + .collect_vec(); + let pk_assigned = ecc_chip.load_private( + ctx, + (Value::known(sign_data.pk.x), Value::known(sign_data.pk.y)), + ); + + self.assert_crt_int_byte_repr( + ctx, + &ecdsa_chip.range, + &pk_assigned.x, + &pk_x_le, + &powers_of_256_cells, + )?; + self.assert_crt_int_byte_repr( + ctx, + &ecdsa_chip.range, + &pk_assigned.y, + &pk_y_le, + &powers_of_256_cells, + )?; + + let assigned_pk_le_selected = [pk_y_le, pk_x_le].concat(); + log::trace!("finished data decomposition"); + + let r_cells = assert_crt( + ctx, + sign_data.signature.0.to_bytes(), + &assigned_data.integer_r, + )?; + let s_cells = assert_crt( + ctx, + sign_data.signature.1.to_bytes(), + &assigned_data.integer_s, + )?; + + Ok(SignDataDecomposed { + pk_hash_cells, + msg_hash_cells: assigned_msg_hash_le, + pk_cells: assigned_pk_le_selected, + address, + is_address_zero, + r_cells, + s_cells, + }) + } + + #[allow(clippy::too_many_arguments)] + fn assign_sig_verify( + &self, + ctx: &mut Context, + rlc_chip: &RangeConfig, + sign_data_decomposed: &SignDataDecomposed, + challenges: &Challenges>, + assigned_ecdsa: &AssignedECDSA>, + ) -> Result<([AssignedValue; 4], AssignedSignatureVerify), Error> { + // ================================================ + // step 0. powers of aux parameters + // ================================================ + let word_lo_hi_powers = iter::successors(Some(Value::known(F::ONE)), |coeff| { + Some(Value::known(F::from(256)) * coeff) + }) + .take(16) + .map(|x| QuantumCell::Witness(x)) + .collect_vec(); + + let keccak_challenge_powers = iter::successors(Some(Value::known(F::ONE)), |coeff| { + Some(challenges.keccak_input() * coeff) + }) + .take(64) + .map(|x| QuantumCell::Witness(x)) + .collect_vec(); + + // ================================================ + // step 1 message hash + // ================================================ + // Ref. spec SignVerifyChip 3. Verify that the signed message in the ecdsa_chip + // corresponds to msg_hash + let msg_hash_cells = { + let msg_hash_lo_cell_bytes = &sign_data_decomposed.msg_hash_cells[..16]; + let msg_hash_hi_cell_bytes = &sign_data_decomposed.msg_hash_cells[16..]; + + let msg_hash_cell_lo = rlc_chip.gate.inner_product( + ctx, + msg_hash_lo_cell_bytes.iter().cloned().collect_vec(), + word_lo_hi_powers.clone(), + ); + let msg_hash_cell_hi = rlc_chip.gate.inner_product( + ctx, + msg_hash_hi_cell_bytes.iter().cloned().collect_vec(), + word_lo_hi_powers.clone(), + ); + + WordLoHi::new([msg_hash_cell_lo, msg_hash_cell_hi]) + }; + + log::trace!( + "assigned msg hash: ({:?}, {:?})", + msg_hash_cells.lo().value(), + msg_hash_cells.hi().value() + ); + + // ================================================ + // step 2 random linear combination of pk + // ================================================ + let pk_rlc = rlc_chip.gate.inner_product( + ctx, + sign_data_decomposed.pk_cells.clone(), + keccak_challenge_powers, + ); + log::trace!("pk rlc: {:?}", pk_rlc.value()); + + // ================================================ + // step 3 pk_hash + // ================================================ + let pk_hash_cells = { + let pk_hash_lo_cell_bytes = &sign_data_decomposed.pk_hash_cells[..16]; + let pk_hash_hi_cell_bytes = &sign_data_decomposed.pk_hash_cells[16..]; + + let pk_hash_cell_lo = rlc_chip.gate.inner_product( + ctx, + pk_hash_lo_cell_bytes.iter().cloned().collect_vec(), + word_lo_hi_powers.clone(), + ); + let pk_hash_cell_hi = rlc_chip.gate.inner_product( + ctx, + pk_hash_hi_cell_bytes.iter().cloned().collect_vec(), + word_lo_hi_powers.clone(), + ); + + WordLoHi::new([pk_hash_cell_lo, pk_hash_cell_hi]) + }; + + // step 4: r,s + let r_cells = { + let r_lo_cell_bytes = &sign_data_decomposed.r_cells[..16]; + let r_hi_cell_bytes = &sign_data_decomposed.r_cells[16..]; + + let r_cell_lo = rlc_chip.gate.inner_product( + ctx, + r_lo_cell_bytes.iter().cloned().collect_vec(), + word_lo_hi_powers.clone(), + ); + let r_cell_hi = rlc_chip.gate.inner_product( + ctx, + r_hi_cell_bytes.iter().cloned().collect_vec(), + word_lo_hi_powers.clone(), + ); + + WordLoHi::new([r_cell_lo, r_cell_hi]) + }; + let s_cells = { + let s_lo_cell_bytes = &sign_data_decomposed.s_cells[..16]; + let s_hi_cell_bytes = &sign_data_decomposed.s_cells[16..]; + + let s_cell_lo = rlc_chip.gate.inner_product( + ctx, + s_lo_cell_bytes.iter().cloned().collect_vec(), + word_lo_hi_powers.clone(), + ); + let s_cell_hi = rlc_chip.gate.inner_product( + ctx, + s_hi_cell_bytes.iter().cloned().collect_vec(), + word_lo_hi_powers, + ); + + WordLoHi::new([s_cell_lo, s_cell_hi]) + }; + + log::trace!( + "pk hash halo2ecc: ({:?}, {:?})", + pk_hash_cells.lo().value(), + pk_hash_cells.lo().value() + ); + log::trace!("finished sign verify"); + + let to_be_keccak_checked = [ + sign_data_decomposed.is_address_zero, + pk_rlc, + pk_hash_cells.lo(), + pk_hash_cells.hi(), + ]; + let assigned_sig_verif = AssignedSignatureVerify { + address: sign_data_decomposed.address, + // msg_len: sign_data.msg.len(), + // msg_rlc: challenges + // .keccak_input() + // .map(|r| rlc::value(sign_data.msg.iter().rev(), r)), + msg_hash: msg_hash_cells, + sig_is_valid: assigned_ecdsa.sig_is_valid, + r: r_cells, + s: s_cells, + v: assigned_ecdsa.v, + }; + Ok((to_be_keccak_checked, assigned_sig_verif)) + } + + /// Assign witness data to the sig circuit. + pub(crate) fn assign( + &self, + config: &SigCircuitConfig, + layouter: &mut impl Layouter, + signatures: &[SignData], + challenges: &Challenges>, + ) -> Result>, Error> { + if signatures.len() > self.max_verif { + error!( + "signatures.len() = {} > max_verif = {}", + signatures.len(), + self.max_verif + ); + return Err(Error::Synthesis); + } + let mut first_pass = SKIP_FIRST_PASS; + let ecdsa_chip = &config.ecdsa_config; + + let assigned_sig_verifs = layouter.assign_region( + || "ecdsa chip verification", + |region| { + if first_pass { + first_pass = false; + return Ok(vec![]); + } + + let mut ctx = ecdsa_chip.new_context(region); + + // ================================================ + // step 1: assert the signature is valid in circuit + // ================================================ + let assigned_ecdsas = signatures + .iter() + .chain(std::iter::repeat(&SignData::default())) + .take(self.max_verif) + .map(|sign_data| self.assign_ecdsa(&mut ctx, ecdsa_chip, sign_data)) + .collect::>>, Error>>()?; + + // ================================================ + // step 2: decompose the keys and messages + // ================================================ + let sign_data_decomposed = signatures + .iter() + .chain(std::iter::repeat(&SignData::default())) + .take(self.max_verif) + .zip_eq(assigned_ecdsas.iter()) + .map(|(sign_data, assigned_ecdsa)| { + self.sign_data_decomposition( + &mut ctx, + ecdsa_chip, + sign_data, + assigned_ecdsa, + ) + }) + .collect::>, Error>>()?; + + // IMPORTANT: Move to Phase2 before RLC + log::info!("before proceeding to the next phase"); + + // finalize the current lookup table before moving to next phase + ecdsa_chip.finalize(&mut ctx); + ctx.print_stats(&["ECDSA context"]); + ctx.next_phase(); + + // ================================================ + // step 3: compute RLC of keys and messages + // ================================================ + let (assigned_keccak_values, assigned_sig_values): ( + Vec<[AssignedValue; 4]>, + Vec>, + ) = signatures + .iter() + .chain(std::iter::repeat(&SignData::default())) + .take(self.max_verif) + .zip_eq(assigned_ecdsas.iter()) + .zip_eq(sign_data_decomposed.iter()) + .map(|((_, assigned_ecdsa), sign_data_decomp)| { + self.assign_sig_verify( + &mut ctx, + &ecdsa_chip.range, + sign_data_decomp, + challenges, + assigned_ecdsa, + ) + }) + .collect::; 4], AssignedSignatureVerify)>, + Error, + >>()? + .into_iter() + .unzip(); + + // ================================================ + // step 4: deferred keccak checks + // ================================================ + for (i, [is_address_zero, pk_rlc, pk_hash_lo, pk_hash_hi]) in + assigned_keccak_values.iter().enumerate() + { + let offset = i * 4; + self.enable_keccak_lookup( + config, + &mut ctx, + offset, + is_address_zero, + pk_rlc, + &WordLoHi::new([*pk_hash_lo, *pk_hash_hi]), + )?; + } + + // IMPORTANT: this assigns all constants to the fixed columns + // IMPORTANT: this copies cells to the lookup advice column to perform range + // check lookups + // This is not optional. + let lookup_cells = ecdsa_chip.finalize(&mut ctx); + log::info!("total number of lookup cells: {}", lookup_cells); + + ctx.print_stats(&["ECDSA context"]); + Ok(assigned_sig_values) + }, + )?; + + layouter.assign_region( + || "expose sig table", + |mut region| { + // step 5: export as a lookup table + for (idx, assigned_sig_verif) in assigned_sig_verifs.iter().enumerate() { + region.assign_fixed( + || "assign sig_table selector", + config.sig_table.q_enable, + idx, + || Value::known(F::ONE), + )?; + + assigned_sig_verif + .v + .copy_advice(&mut region, config.sig_table.sig_v, idx); + + assigned_sig_verif.r.lo().copy_advice( + &mut region, + config.sig_table.sig_r.lo(), + idx, + ); + assigned_sig_verif.r.hi().copy_advice( + &mut region, + config.sig_table.sig_r.hi(), + idx, + ); + + assigned_sig_verif.s.lo().copy_advice( + &mut region, + config.sig_table.sig_s.lo(), + idx, + ); + assigned_sig_verif.s.hi().copy_advice( + &mut region, + config.sig_table.sig_s.hi(), + idx, + ); + + assigned_sig_verif.address.copy_advice( + &mut region, + config.sig_table.recovered_addr, + idx, + ); + + assigned_sig_verif.sig_is_valid.copy_advice( + &mut region, + config.sig_table.is_valid, + idx, + ); + + assigned_sig_verif.msg_hash.lo().copy_advice( + &mut region, + config.sig_table.msg_hash.lo(), + idx, + ); + assigned_sig_verif.msg_hash.hi().copy_advice( + &mut region, + config.sig_table.msg_hash.hi(), + idx, + ); + } + Ok(()) + }, + )?; + + Ok(assigned_sig_verifs) + } + + /// Assert an CRTInteger's byte representation is correct. + /// inputs + /// - crt_int with 3 limbs [88, 88, 80] + /// - byte representation of the integer + /// - a sequence of [1, 2^8, 2^16, ...] + /// - a overriding flag that sets output to 0 if set + fn assert_crt_int_byte_repr( + &self, + ctx: &mut Context, + range_chip: &RangeConfig, + crt_int: &CRTInteger, + byte_repr: &[QuantumCell], + word_lo_hi_powers: &[QuantumCell], + ) -> Result<(), Error> { + // length of byte representation is 32 + assert_eq!(byte_repr.len(), 32); + // need to support decomposition of up to 88 bits + assert!(word_lo_hi_powers.len() >= 11); + + let flex_gate_chip = &range_chip.gate; + + // apply the overriding flag + let limb1_value = crt_int.truncation.limbs[0]; + let limb2_value = crt_int.truncation.limbs[1]; + let limb3_value = crt_int.truncation.limbs[2]; + + // assert the byte_repr is the right decomposition of overflow_int + // overflow_int is an overflowing integer with 3 limbs, of sizes 88, 88, and 80 + // we reconstruct the three limbs from the bytes repr, and + // then enforce equality with the CRT integer + let limb1_recover = flex_gate_chip.inner_product( + ctx, + byte_repr[0..11].to_vec(), + word_lo_hi_powers[0..11].to_vec(), + ); + let limb2_recover = flex_gate_chip.inner_product( + ctx, + byte_repr[11..22].to_vec(), + word_lo_hi_powers[0..11].to_vec(), + ); + let limb3_recover = flex_gate_chip.inner_product( + ctx, + byte_repr[22..].to_vec(), + word_lo_hi_powers[0..10].to_vec(), + ); + flex_gate_chip.assert_equal( + ctx, + QuantumCell::Existing(limb1_value), + QuantumCell::Existing(limb1_recover), + ); + flex_gate_chip.assert_equal( + ctx, + QuantumCell::Existing(limb2_value), + QuantumCell::Existing(limb2_recover), + ); + flex_gate_chip.assert_equal( + ctx, + QuantumCell::Existing(limb3_value), + QuantumCell::Existing(limb3_recover), + ); + log::trace!( + "limb 1 \ninput {:?}\nreconstructed {:?}", + limb1_value.value(), + limb1_recover.value() + ); + log::trace!( + "limb 2 \ninput {:?}\nreconstructed {:?}", + limb2_value.value(), + limb2_recover.value() + ); + log::trace!( + "limb 3 \ninput {:?}\nreconstructed {:?}", + limb3_value.value(), + limb3_recover.value() + ); + + Ok(()) + } +} diff --git a/zkevm-circuits/src/sig_circuit/dev.rs b/zkevm-circuits/src/sig_circuit/dev.rs new file mode 100644 index 0000000000..684385b45b --- /dev/null +++ b/zkevm-circuits/src/sig_circuit/dev.rs @@ -0,0 +1,64 @@ +use super::*; + +use crate::util::Challenges; + +use bus_mapping::circuit_input_builder::keccak_inputs_sign_verify; +use halo2_proofs::{circuit::SimpleFloorPlanner, plonk::Circuit}; + +/// SigCircuitTesterConfig +#[derive(Clone, Debug)] +pub struct SigCircuitTesterConfig { + sign_verify: SigCircuitConfig, + challenges: crate::util::Challenges, +} + +impl SigCircuitTesterConfig { + pub(crate) fn new(meta: &mut ConstraintSystem) -> Self { + let keccak_table = KeccakTable::construct(meta); + let sig_table = SigTable::construct(meta); + let challenges = Challenges::construct(meta); + let challenges_expr = challenges.exprs(meta); + let sign_verify = SigCircuitConfig::new( + meta, + SigCircuitConfigArgs { + _keccak_table: keccak_table, + challenges: challenges_expr, + sig_table, + }, + ); + + SigCircuitTesterConfig { + sign_verify, + challenges, + } + } +} + +impl Circuit for SigCircuit { + type Config = SigCircuitTesterConfig; + type FloorPlanner = SimpleFloorPlanner; + type Params = (); + + fn without_witnesses(&self) -> Self { + Self::default() + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + SigCircuitTesterConfig::new(meta) + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + let challenges = config.challenges.values(&mut layouter); + self.synthesize_sub(&config.sign_verify, &challenges, &mut layouter)?; + config.sign_verify._keccak_table.dev_load( + &mut layouter, + &keccak_inputs_sign_verify(&self.signatures), + &challenges, + )?; + Ok(()) + } +} diff --git a/zkevm-circuits/src/sig_circuit/ecdsa.rs b/zkevm-circuits/src/sig_circuit/ecdsa.rs new file mode 100644 index 0000000000..7105066123 --- /dev/null +++ b/zkevm-circuits/src/sig_circuit/ecdsa.rs @@ -0,0 +1,226 @@ +//! This module implements the ECDSA circuit. Modified from +//! + +use halo2_base::{ + gates::{GateInstructions, RangeInstructions}, + utils::{fe_to_biguint, modulus, CurveAffineExt}, + AssignedValue, Context, + QuantumCell::Existing, +}; +use halo2_ecc::{ + bigint::{big_less_than, CRTInteger}, + ecc::{ec_add_unequal, ec_sub_unequal, fixed_base, scalar_multiply, EcPoint, EccChip}, + fields::{fp::FpConfig, FieldChip, PrimeField, Selectable}, +}; + +// CF is the coordinate field of GA +// SF is the scalar field of GA +// p = coordinate field modulus +// n = scalar field modulus +// Only valid when p is very close to n in size (e.g. for Secp256k1) +// returns +// - if the signature is valid +// - the y coordinate for rG (will be used for ECRecovery later) +#[allow(clippy::too_many_arguments)] +pub(crate) fn ecdsa_verify_no_pubkey_check( + base_chip: &FpConfig, + ctx: &mut Context, + pubkey: &EcPoint as FieldChip>::FieldPoint>, + r: &CRTInteger, + s: &CRTInteger, + msghash: &CRTInteger, + var_window_bits: usize, + fixed_window_bits: usize, +) -> ( + AssignedValue, + AssignedValue, + CRTInteger, + AssignedValue, +) +where + GA: CurveAffineExt, +{ + let ecc_chip = EccChip::>::construct(base_chip.clone()); + let scalar_chip = FpConfig::::construct( + base_chip.range.clone(), + base_chip.limb_bits, + base_chip.num_limbs, + modulus::(), + ); + let n = scalar_chip.load_constant(ctx, scalar_chip.p.to_biguint().unwrap()); + + // check whether the pubkey is (0, 0), i.e. in the case of ecrecover, no pubkey could be + // recovered. + let (is_pubkey_zero, is_pubkey_not_zero) = { + let is_pubkey_x_zero = ecc_chip.field_chip().is_zero(ctx, &pubkey.x); + let is_pubkey_y_zero = ecc_chip.field_chip().is_zero(ctx, &pubkey.y); + let is_pubkey_zero = ecc_chip.field_chip().range().gate().and( + ctx, + Existing(is_pubkey_x_zero), + Existing(is_pubkey_y_zero), + ); + ( + is_pubkey_zero, + ecc_chip + .field_chip() + .range() + .gate() + .not(ctx, Existing(is_pubkey_zero)), + ) + }; + + // check r,s are in [1, n - 1] + let r_is_zero = scalar_chip.is_soft_zero(ctx, r); + let r_in_range = scalar_chip.is_soft_nonzero(ctx, r); + let r_is_valid = base_chip + .range() + .gate() + .or(ctx, Existing(r_is_zero), Existing(r_in_range)); + let s_is_zero = scalar_chip.is_soft_zero(ctx, s); + let s_in_range = scalar_chip.is_soft_nonzero(ctx, s); + let s_is_valid = base_chip + .range() + .gate() + .or(ctx, Existing(s_is_zero), Existing(s_in_range)); + + // load required constants + let zero = scalar_chip.load_constant(ctx, FpConfig::::fe_to_constant(SF::ZERO)); + let one = scalar_chip.load_constant(ctx, FpConfig::::fe_to_constant(SF::ONE)); + let neg_one = scalar_chip.load_constant(ctx, FpConfig::::fe_to_constant(-SF::ONE)); + + // compute u1 = m * s^{-1} mod n + let s2 = scalar_chip.select(ctx, &one, s, &s_is_zero); + let u1 = scalar_chip.divide(ctx, msghash, &s2); + let u1 = scalar_chip.select(ctx, &zero, &u1, &s_is_zero); + let u1_is_one = { + let diff = scalar_chip.sub_no_carry(ctx, &u1, &one); + let diff = scalar_chip.carry_mod(ctx, &diff); + scalar_chip.is_zero(ctx, &diff) + }; + + // compute u2 = r * s^{-1} mod n + let u2 = scalar_chip.divide(ctx, r, &s2); + let u2 = scalar_chip.select(ctx, &zero, &u2, &s_is_zero); + + // u3 = 1 if u1 == 1 + // u3 = -1 if u1 != 1 + // this ensures u1 + u3 != 0 + let u3 = scalar_chip.select(ctx, &one, &neg_one, &u1_is_one); + + let u1_plus_u3 = scalar_chip.add_no_carry(ctx, &u1, &u3); + let u1_plus_u3 = scalar_chip.carry_mod(ctx, &u1_plus_u3); + + // compute (u1+u3) * G + let u1u3_mul = fixed_base::scalar_multiply::( + base_chip, + ctx, + &GA::generator(), + &u1_plus_u3.truncation.limbs, + base_chip.limb_bits, + fixed_window_bits, + ); + // compute u2 * pubkey + let u2_prime = scalar_chip.select(ctx, &one, &u2, &s_is_zero); + let pubkey_prime = ecc_chip.load_random_point::(ctx); + let pubkey_prime = ecc_chip.select(ctx, &pubkey_prime, pubkey, &is_pubkey_zero); + let u2_mul = scalar_multiply::( + base_chip, + ctx, + &pubkey_prime, + &u2_prime.truncation.limbs, + base_chip.limb_bits, + var_window_bits, + ); + let point_at_infinity = EcPoint::construct( + ecc_chip + .field_chip() + .load_constant(ctx, fe_to_biguint(&CF::ZERO)), + ecc_chip + .field_chip() + .load_constant(ctx, fe_to_biguint(&CF::ZERO)), + ); + let u2_is_zero = + base_chip + .range() + .gate() + .or(ctx, Existing(s_is_zero), Existing(is_pubkey_zero)); + let u2_mul = ecc_chip.select(ctx, &point_at_infinity, &u2_mul, &u2_is_zero); + // compute u3*G this is directly assigned for G so no scalar_multiply is required + let u3_mul = { + let generator = GA::generator(); + let neg_generator = -generator; + let generator = ecc_chip.assign_constant_point(ctx, generator); + let neg_generator = ecc_chip.assign_constant_point(ctx, neg_generator); + ecc_chip.select(ctx, &generator, &neg_generator, &u1_is_one) + }; + + // compute u2 * pubkey + u3 * G + base_chip.enforce_less_than_p(ctx, u2_mul.x()); + base_chip.enforce_less_than_p(ctx, u3_mul.x()); + let u2_pk_u3_g = ec_add_unequal(base_chip, ctx, &u2_mul, &u3_mul, false); + + // check + // - (u1 + u3) * G + // - u2 * pubkey + u3 * G + // are not equal + let u1_u2_x_eq = ecc_chip.is_equal(ctx, &u1u3_mul, &u2_pk_u3_g); + let u1_u2_not_eq = base_chip.range.gate().not(ctx, Existing(u1_u2_x_eq)); + + // compute (x1, y1) = u1 * G + u2 * pubkey and check (r mod n) == x1 as integers + // which is basically u1u3_mul + u2_mul - u3_mul + // WARNING: For optimization reasons, does not reduce x1 mod n, which is + // invalid unless p is very close to n in size. + // + // WARNING: this may be trigger errors if: + // - u1u3_mul == u2_mul + // + // if r is sampled truly from random then this will not happen + // to completely ensure the correctness we may need to sample u3 from random, but it is quite + // costly. + let sum = ec_add_unequal(base_chip, ctx, &u1u3_mul, &u2_mul, false); + + // safe: we have already checked u1.G + u2.pk != 0 + // so u1.G + u3.G + u2.pk != u3.G + let sum = ec_sub_unequal(base_chip, ctx, &sum, &u3_mul, false); + let equal_check = base_chip.is_equal(ctx, &sum.x, r); + + // TODO: maybe the big_less_than is optional? + let u1_small = big_less_than::assign::( + base_chip.range(), + ctx, + &u1.truncation, + &n.truncation, + base_chip.limb_bits, + base_chip.limb_bases[1], + ); + let u2_small = big_less_than::assign::( + base_chip.range(), + ctx, + &u2.truncation, + &n.truncation, + base_chip.limb_bits, + base_chip.limb_bases[1], + ); + + // check + // - (r in [0, n - 1]) + // - (s in [0, n - 1]) + // - (u1_mul != - u2_mul) + // - (r == x1 mod n) + // - pk != (0, 0) + let res = base_chip.range().gate().and_many( + ctx, + vec![ + Existing(r_is_valid), + Existing(s_is_valid), + Existing(u1_small), + Existing(u2_small), + Existing(u1_u2_not_eq), + Existing(equal_check), + Existing(is_pubkey_not_zero), + ], + ); + + let y_is_zero = scalar_chip.is_soft_zero(ctx, &sum.y); + (res, is_pubkey_zero, sum.y, y_is_zero) +} diff --git a/zkevm-circuits/src/sig_circuit/test.rs b/zkevm-circuits/src/sig_circuit/test.rs new file mode 100644 index 0000000000..5d845ae43b --- /dev/null +++ b/zkevm-circuits/src/sig_circuit/test.rs @@ -0,0 +1,267 @@ +use super::*; +use eth_types::sign_types::{sign, SignData}; +use halo2_proofs::{ + arithmetic::Field as HaloField, + dev::MockProver, + halo2curves::{ + bn256::Fr, + group::Curve, + secp256k1::{self, Secp256k1Affine}, + }, +}; +use rand::{Rng, RngCore}; +use std::marker::PhantomData; + +use super::utils::LOG_TOTAL_NUM_ROWS; +use crate::sig_circuit::utils::MAX_NUM_SIG; +use eth_types::{ + sign_types::{biguint_to_32bytes_le, recover_pk, SECP256K1_Q}, + word, ToBigEndian, ToLittleEndian, Word, +}; +use ethers_core::k256::elliptic_curve::PrimeField; +use halo2_proofs::halo2curves::secp256k1::Fq; +use num::{BigUint, Integer}; +use rand::SeedableRng; +use rand_xorshift::XorShiftRng; +use sha3::{Digest, Keccak256}; +use snark_verifier::util::arithmetic::PrimeCurveAffine; + +#[test] +fn test_edge_cases() { + let mut rng = XorShiftRng::seed_from_u64(1); + + // helper + let to_sig = |(r, s, v): (Word, Word, u8)| -> (Fq, Fq, u8) { + ( + Fq::from_bytes(&r.to_le_bytes()).unwrap(), + Fq::from_bytes(&s.to_le_bytes()).unwrap(), + v, + ) + }; + + // Vec<(msg_hash, r, s, v)> + // + // good data for ecrecover is (big-endian): + // - msg_hash: 0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3 + // - r: 0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608 + // - s: 0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada + // - v: 28, i.e. v == 1 for sig circuit + let good_ecrecover_data = ( + word!("0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3"), + word!("0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608"), + word!("0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada"), + 1u8, + ); + let ecrecover_data = vec![ + // 1. good data + good_ecrecover_data, + // 2. msg_hash == 0 + ( + Word::zero(), + good_ecrecover_data.1, + good_ecrecover_data.2, + good_ecrecover_data.3, + ), + // 3. r == 0 + ( + good_ecrecover_data.0, + Word::zero(), + good_ecrecover_data.2, + good_ecrecover_data.3, + ), + // 4. s == 0 + ( + good_ecrecover_data.0, + good_ecrecover_data.1, + Word::zero(), + good_ecrecover_data.3, + ), + // 5. r == 0 and s == 0 + ( + good_ecrecover_data.0, + Word::zero(), + Word::zero(), + good_ecrecover_data.3, + ), + // 6. random r and s for random msg hash + { + let mut bytes = [0u8; 32]; + rng.fill(&mut bytes[..]); + let msg_hash = Word::from_big_endian(&bytes); + rng.fill(&mut bytes[..]); + let r = Word::from_big_endian(&bytes); + rng.fill(&mut bytes[..]); + let s = Word::from_big_endian(&bytes); + (msg_hash, r, s, 0u8) + }, + // 7. v == 0 when v should be 1 + ( + good_ecrecover_data.0, + good_ecrecover_data.1, + good_ecrecover_data.2, + 1 - good_ecrecover_data.3, + ), + // 8. msg_hash outside FQ::MODULUS + ( + Word::MAX, + good_ecrecover_data.1, + good_ecrecover_data.2, + good_ecrecover_data.3, + ), + ]; + let signatures = ecrecover_data + .iter() + .map(|&(msg_hash, r, s, v)| SignData { + signature: to_sig((r, s, v)), + pk: recover_pk(v, &r, &s, &msg_hash.to_be_bytes()) + .unwrap_or(Secp256k1Affine::identity()), + msg_hash: { + let msg_hash = BigUint::from_bytes_be(&msg_hash.to_be_bytes()); + let msg_hash = msg_hash.mod_floor(&*SECP256K1_Q); + let msg_hash_le = biguint_to_32bytes_le(msg_hash); + secp256k1::Fq::from_repr(msg_hash_le).unwrap() + }, + ..Default::default() + }) + .collect(); + log::debug!("signatures="); + log::debug!("{:#?}", signatures); + + run(LOG_TOTAL_NUM_ROWS as u32, 8, signatures); +} + +#[test] +fn sign_verify_zero_msg_hash() { + let mut rng = XorShiftRng::seed_from_u64(1); + + log::debug!("testing for msg_hash = 0"); + let mut signatures = Vec::new(); + + let (sk, pk) = gen_key_pair(&mut rng); + let msg = gen_msg(&mut rng); + let msg_hash = secp256k1::Fq::zero(); + let (r, s, v) = sign_with_rng(&mut rng, sk, msg_hash); + signatures.push(SignData { + signature: (r, s, v), + pk, + msg: msg.into(), + msg_hash, + }); + + let k = LOG_TOTAL_NUM_ROWS as u32; + run(k, 1, signatures); + + log::debug!("end of testing for msg_hash = 0"); +} + +#[test] +fn sign_verify_nonzero_msg_hash() { + let mut rng = XorShiftRng::seed_from_u64(1); + + log::debug!("testing for msg_hash = 1"); + let mut signatures = Vec::new(); + + let (sk, pk) = gen_key_pair(&mut rng); + let msg = gen_msg(&mut rng); + let msg_hash = secp256k1::Fq::one(); + let (r, s, v) = sign_with_rng(&mut rng, sk, msg_hash); + signatures.push(SignData { + signature: (r, s, v), + pk, + msg: msg.into(), + msg_hash, + }); + + let k = LOG_TOTAL_NUM_ROWS as u32; + run(k, 1, signatures); + + log::debug!("end of testing for msg_hash = 1"); +} + +#[test] +fn test_sign_verify() { + let max_sigs = [1, 4]; + for max_sig in max_sigs { + sign_verify(max_sig as usize); + } +} + +#[test] +#[ignore] +fn test_sign_verify_max_num_sig() { + sign_verify(MAX_NUM_SIG); +} + +fn sign_verify(max_sig: usize) { + let mut rng = XorShiftRng::seed_from_u64(1); + + // random msg_hash + log::debug!("testing for {} signatures", max_sig); + let mut signatures = Vec::new(); + for _ in 0..max_sig { + let (sk, pk) = gen_key_pair(&mut rng); + let msg = gen_msg(&mut rng); + let msg_hash: [u8; 32] = Keccak256::digest(&msg) + .as_slice() + .to_vec() + .try_into() + .expect("hash length isn't 32 bytes"); + let msg_hash = secp256k1::Fq::from_bytes(&msg_hash).unwrap(); + let (r, s, v) = sign_with_rng(&mut rng, sk, msg_hash); + signatures.push(SignData { + signature: (r, s, v), + pk, + msg: msg.into(), + msg_hash, + }); + } + + let k = LOG_TOTAL_NUM_ROWS as u32; + run(k, max_sig, signatures); + + log::debug!("end of testing for {} signatures", max_sig); +} + +// Generate a test key pair +fn gen_key_pair(rng: impl RngCore) -> (secp256k1::Fq, Secp256k1Affine) { + // generate a valid signature + let generator = Secp256k1Affine::generator(); + let sk = secp256k1::Fq::random(rng); + let pk = generator * sk; + let pk = pk.to_affine(); + + (sk, pk) +} + +// Generate a test message. +fn gen_msg(mut rng: impl RngCore) -> Vec { + let msg_len: usize = rng.gen_range(0..128); + let mut msg = vec![0; msg_len]; + rng.fill_bytes(&mut msg); + msg +} + +// Returns (r, s, v) +fn sign_with_rng( + rng: impl RngCore, + sk: secp256k1::Fq, + msg_hash: secp256k1::Fq, +) -> (secp256k1::Fq, secp256k1::Fq, u8) { + let randomness = secp256k1::Fq::random(rng); + sign(randomness, sk, msg_hash) +} + +fn run(k: u32, max_verif: usize, signatures: Vec) { + // SignVerifyChip -> ECDSAChip -> MainGate instance column + let circuit = SigCircuit:: { + max_verif, + signatures, + _marker: PhantomData, + }; + + let prover = match MockProver::run(k, &circuit, vec![]) { + Ok(prover) => prover, + Err(e) => panic!("{e:#?}"), + }; + assert_eq!(prover.verify(), Ok(())); +} diff --git a/zkevm-circuits/src/sig_circuit/utils.rs b/zkevm-circuits/src/sig_circuit/utils.rs new file mode 100644 index 0000000000..b7aacf8aed --- /dev/null +++ b/zkevm-circuits/src/sig_circuit/utils.rs @@ -0,0 +1,115 @@ +use eth_types::Field; +use halo2_base::{AssignedValue, QuantumCell}; +use halo2_ecc::{ + bigint::CRTInteger, + ecc::EcPoint, + fields::{fp::FpConfig, FieldChip}, +}; +use halo2_proofs::halo2curves::secp256k1::{Fp, Fq}; + +use crate::util::word::WordLoHi; + +// Hard coded parameters. +// TODO: allow for a configurable param. +pub(super) const MAX_NUM_SIG: usize = 128; +/// Each ecdsa signature requires 461540 cells +pub(super) const CELLS_PER_SIG: usize = 461540; +/// Each ecdsa signature requires 63489 lookup cells +pub(super) const LOOKUP_CELLS_PER_SIG: usize = 63489; +/// Total number of rows allocated for ecdsa chip +pub(super) const LOG_TOTAL_NUM_ROWS: usize = 20; +/// Max number of columns allowed +pub(super) const COLUMN_NUM_LIMIT: usize = 58; +/// Max number of lookup columns allowed +pub(super) const LOOKUP_COLUMN_NUM_LIMIT: usize = 9; + +// halo2-ecc's ECDSA config +// +// get the following parameters by running +// `cargo test --release --package zkevm-circuits --lib sig_circuit::test::sign_verify -- +// --nocapture` +// - num_advice: 56 +// - num_lookup_advice: 8 +// - num_fixed: 1 +// - lookup_bits: 19 +// - limb_bits: 88 +// - num_limbs: 3 +// +/// Number of bits of a limb +pub(super) const LIMB_BITS: usize = 88; +/// Number of limbs +pub(super) const NUM_LIMBS: usize = 3; + +pub(super) fn calc_required_advices(num_verif: usize) -> usize { + let mut num_adv = 1; + let total_cells = num_verif * CELLS_PER_SIG; + let row_num = 1 << LOG_TOTAL_NUM_ROWS; + while num_adv < COLUMN_NUM_LIMIT { + if num_adv * row_num > total_cells { + log::debug!( + "ecdsa chip uses {} advice columns for {} signatures", + num_adv, + num_verif + ); + return num_adv; + } + num_adv += 1; + } + panic!("the required advice columns exceeds {COLUMN_NUM_LIMIT} for {num_verif} signatures"); +} + +pub(super) fn calc_required_lookup_advices(num_verif: usize) -> usize { + let mut num_adv = 1; + let total_cells = num_verif * LOOKUP_CELLS_PER_SIG; + let row_num = 1 << LOG_TOTAL_NUM_ROWS; + while num_adv < LOOKUP_COLUMN_NUM_LIMIT { + if num_adv * row_num > total_cells { + log::debug!( + "ecdsa chip uses {} lookup advice columns for {} signatures", + num_adv, + num_verif + ); + return num_adv; + } + num_adv += 1; + } + panic!("the required lookup advice columns exceeds {LOOKUP_COLUMN_NUM_LIMIT} for {num_verif} signatures"); +} + +/// Chip to handle overflow integers of ECDSA::Fq, the scalar field +pub(super) type FqChip = FpConfig; +/// Chip to handle ECDSA::Fp, the base field +pub(super) type FpChip = FpConfig; + +pub(crate) struct AssignedECDSA> { + pub(super) _pk: EcPoint, + pub(super) pk_is_zero: AssignedValue, + pub(super) msg_hash: CRTInteger, + pub(super) integer_r: CRTInteger, + pub(super) integer_s: CRTInteger, + pub(super) v: AssignedValue, + pub(super) sig_is_valid: AssignedValue, +} + +#[derive(Debug, Clone)] +pub(crate) struct AssignedSignatureVerify { + pub(crate) address: AssignedValue, + // pub(crate) msg_len: usize, + // pub(crate) msg_rlc: Value, + pub(crate) msg_hash: WordLoHi>, + pub(crate) r: WordLoHi>, + pub(crate) s: WordLoHi>, + pub(crate) v: AssignedValue, + pub(crate) sig_is_valid: AssignedValue, +} + +pub(super) struct SignDataDecomposed { + pub(super) pk_hash_cells: Vec>, + pub(super) msg_hash_cells: Vec>, + pub(super) pk_cells: Vec>, + pub(super) address: AssignedValue, + pub(super) is_address_zero: AssignedValue, + pub(super) r_cells: Vec>, + pub(super) s_cells: Vec>, + // v: AssignedValue<'v, F>, // bool +} diff --git a/zkevm-circuits/src/super_circuit.rs b/zkevm-circuits/src/super_circuit.rs index 786d235fed..786493e399 100644 --- a/zkevm-circuits/src/super_circuit.rs +++ b/zkevm-circuits/src/super_circuit.rs @@ -62,8 +62,8 @@ use crate::{ pi_circuit::{PiCircuit, PiCircuitConfig, PiCircuitConfigArgs}, state_circuit::{StateCircuit, StateCircuitConfig, StateCircuitConfigArgs}, table::{ - BlockTable, BytecodeTable, CopyTable, ExpTable, KeccakTable, MptTable, RwTable, TxTable, - UXTable, WdTable, + BlockTable, BytecodeTable, CopyTable, ExpTable, KeccakTable, MptTable, RwTable, SigTable, + TxTable, UXTable, WdTable, }, tx_circuit::{TxCircuit, TxCircuitConfig, TxCircuitConfigArgs}, util::{log2_ceil, Challenges, SubCircuit, SubCircuitConfig}, @@ -136,6 +136,7 @@ impl SubCircuitConfig for SuperCircuitConfig { power_of_randomness[0].clone(), power_of_randomness[0].clone(), ); + let sig_table = SigTable::construct(meta); let keccak_circuit = KeccakCircuitConfig::new( meta, @@ -210,6 +211,7 @@ impl SubCircuitConfig for SuperCircuitConfig { exp_table, u8_table, u16_table, + sig_table, feature_config, }, ); diff --git a/zkevm-circuits/src/super_circuit/test.rs b/zkevm-circuits/src/super_circuit/test.rs index ab6213611b..741fbdad8b 100644 --- a/zkevm-circuits/src/super_circuit/test.rs +++ b/zkevm-circuits/src/super_circuit/test.rs @@ -140,6 +140,7 @@ fn serial_test_super_circuit_1tx_1max_tx() { max_bytecode: 512, max_evm_rows: 0, max_keccak_rows: 0, + max_vertical_circuit_rows: 0, }; test_super_circuit(block, circuits_params, Fr::from(TEST_MOCK_RANDOMNESS)); } @@ -157,6 +158,7 @@ fn serial_test_super_circuit_1tx_2max_tx() { max_bytecode: 512, max_evm_rows: 0, max_keccak_rows: 0, + max_vertical_circuit_rows: 0, }; test_super_circuit(block, circuits_params, Fr::from(TEST_MOCK_RANDOMNESS)); } @@ -174,6 +176,7 @@ fn serial_test_super_circuit_2tx_2max_tx() { max_bytecode: 512, max_evm_rows: 0, max_keccak_rows: 0, + max_vertical_circuit_rows: 0, }; test_super_circuit(block, circuits_params, Fr::from(TEST_MOCK_RANDOMNESS)); } diff --git a/zkevm-circuits/src/table.rs b/zkevm-circuits/src/table.rs index fde4ef38e1..fb0be4aefd 100644 --- a/zkevm-circuits/src/table.rs +++ b/zkevm-circuits/src/table.rs @@ -37,6 +37,8 @@ pub(crate) mod keccak_table; pub mod mpt_table; /// rw table pub(crate) mod rw_table; +/// signature table +pub(crate) mod sig_table; /// tx table pub(crate) mod tx_table; /// ux table @@ -53,6 +55,7 @@ pub(crate) use ux_table::UXTable; pub use mpt_table::{MPTProofType, MptTable}; pub(crate) use rw_table::RwTable; +pub(crate) use sig_table::SigTable; pub(crate) use tx_table::{ TxContextFieldTag, TxFieldTag, TxLogFieldTag, TxReceiptFieldTag, TxTable, }; diff --git a/zkevm-circuits/src/table/sig_table.rs b/zkevm-circuits/src/table/sig_table.rs new file mode 100644 index 0000000000..037b0c9cfe --- /dev/null +++ b/zkevm-circuits/src/table/sig_table.rs @@ -0,0 +1,131 @@ +use super::*; + +use eth_types::sign_types::SignData; + +/// The sig table is used to verify signatures, used in tx circuit and ecrecover precompile. +#[derive(Clone, Copy, Debug)] +pub struct SigTable { + /// Indicates whether or not the gates are enabled on the current row. + pub q_enable: Column, + /// Keccak256 hash of the message that's signed. + pub msg_hash: WordLoHi>, + /// signature's `r` component. + pub sig_r: WordLoHi>, + /// signature's `s` component. + pub sig_s: WordLoHi>, + /// should be in range [0, 1] + pub sig_v: Column, + /// The recovered address, i.e. the 20-bytes address that must have signed the message. + pub recovered_addr: Column, + /// Indicates whether or not the signature is valid or not upon signature verification. + pub is_valid: Column, +} + +impl SigTable { + /// Construct the SigTable. + pub fn construct(meta: &mut ConstraintSystem) -> Self { + Self { + q_enable: meta.fixed_column(), + msg_hash: WordLoHi::new([meta.advice_column(), meta.advice_column()]), + sig_r: WordLoHi::new([meta.advice_column(), meta.advice_column()]), + sig_s: WordLoHi::new([meta.advice_column(), meta.advice_column()]), + sig_v: meta.advice_column(), + recovered_addr: meta.advice_column(), + is_valid: meta.advice_column(), + } + } + + /// Assign witness data from a block to the verification table. + pub fn dev_load( + &self, + layouter: &mut impl Layouter, + block: &Block, + ) -> Result<(), Error> { + layouter.assign_region( + || "sig table (dev load)", + |mut region| { + let signatures: Vec = block.get_sign_data(false); + + for (offset, sign_data) in signatures.iter().enumerate() { + let msg_hash = + WordLoHi::from(U256::from(sign_data.msg_hash.to_bytes())).into_value(); + let sig_r = + WordLoHi::from(U256::from(sign_data.signature.0.to_bytes())).into_value(); + let sig_s = + WordLoHi::from(U256::from(sign_data.signature.1.to_bytes())).into_value(); + let sig_v = Value::known(F::from(sign_data.signature.2 as u64)); + let recovered_addr = Value::known(sign_data.get_addr().to_scalar().unwrap()); + region.assign_fixed( + || format!("sig table q_enable {offset}"), + self.q_enable, + offset, + || Value::known(F::ONE), + )?; + for (column, value) in [ + (self.sig_v, sig_v), + (self.recovered_addr, recovered_addr), + ( + self.is_valid, + Value::known(F::from(!sign_data.get_addr().is_zero() as u64)), + ), + ] { + region.assign_advice( + || "assign sign data on sig table", + column, + offset, + || value, + )?; + } + for (column, value) in [ + (self.msg_hash, msg_hash), + (self.sig_r, sig_r), + (self.sig_s, sig_s), + ] { + value.assign_advice( + &mut region, + || "assign sign data on sig table", + column, + offset, + )?; + } + } + + Ok(()) + }, + )?; + + Ok(()) + } +} + +impl LookupTable for SigTable { + fn columns(&self) -> Vec> { + vec![ + self.q_enable.into(), + self.msg_hash.lo().into(), + self.msg_hash.hi().into(), + self.sig_v.into(), + self.sig_r.lo().into(), + self.sig_r.hi().into(), + self.sig_s.lo().into(), + self.sig_s.hi().into(), + self.recovered_addr.into(), + self.is_valid.into(), + ] + } + + fn annotations(&self) -> Vec { + vec![ + String::from("q_enable"), + String::from("msg_hash_lo"), + String::from("msg_hash_hi"), + String::from("sig_v"), + String::from("sig_r_lo"), + String::from("sig_r_hi"), + String::from("sig_s_lo"), + String::from("sig_s_hi"), + String::from("recovered_addr"), + String::from("is_valid"), + ] + } +} diff --git a/zkevm-circuits/src/tx_circuit/sign_verify.rs b/zkevm-circuits/src/tx_circuit/sign_verify.rs index b99f2a46a2..15587021c6 100644 --- a/zkevm-circuits/src/tx_circuit/sign_verify.rs +++ b/zkevm-circuits/src/tx_circuit/sign_verify.rs @@ -350,9 +350,10 @@ impl SignVerifyChip { let SignData { signature, pk, + msg: _, msg_hash, } = sign_data; - let (sig_r, sig_s) = signature; + let (sig_r, sig_s, _) = signature; let ChipsRef { main_gate: _, @@ -717,7 +718,7 @@ mod sign_verify_tests { use super::*; use crate::util::Challenges; use bus_mapping::circuit_input_builder::keccak_inputs_sign_verify; - use eth_types::sign_types::sign; + use eth_types::{sign_types::sign, Bytes}; use halo2_proofs::{ arithmetic::Field as HaloField, circuit::SimpleFloorPlanner, @@ -841,7 +842,7 @@ mod sign_verify_tests { rng: impl RngCore, sk: secp256k1::Fq, msg_hash: secp256k1::Fq, - ) -> (secp256k1::Fq, secp256k1::Fq) { + ) -> (secp256k1::Fq, secp256k1::Fq, u8) { let randomness = secp256k1::Fq::random(rng); sign(randomness, sk, msg_hash) } @@ -868,6 +869,7 @@ mod sign_verify_tests { signature: sig, pk, msg_hash, + msg: Bytes::new(), }); } diff --git a/zkevm-circuits/src/witness/block.rs b/zkevm-circuits/src/witness/block.rs index 1628534093..507e8e370c 100644 --- a/zkevm-circuits/src/witness/block.rs +++ b/zkevm-circuits/src/witness/block.rs @@ -7,11 +7,13 @@ use crate::{ util::{log2_ceil, word::WordLoHi, SubCircuit}, }; use bus_mapping::{ - circuit_input_builder::{self, CopyEvent, ExpEvent, FeatureConfig, FixedCParams, Withdrawal}, + circuit_input_builder::{ + self, CopyEvent, ExpEvent, FeatureConfig, FixedCParams, PrecompileEvents, Withdrawal, + }, state_db::CodeDB, Error, }; -use eth_types::{Address, Field, ToScalar, Word, H256}; +use eth_types::{sign_types::SignData, Address, Field, ToScalar, Word, H256}; use halo2_proofs::circuit::Value; use itertools::Itertools; @@ -51,6 +53,8 @@ pub struct Block { pub prev_state_root: Word, // TODO: Make this H256 /// Keccak inputs pub keccak_inputs: Vec>, + /// IO to/from the precompiled contract calls. + pub precompile_events: PrecompileEvents, /// Original Block from geth pub eth_block: eth_types::Block, } @@ -73,6 +77,26 @@ impl Block { } } + /// Get signature (witness) from the block for tx signatures and ecRecover calls. + pub(crate) fn get_sign_data(&self, padding: bool) -> Vec { + let mut signatures: Vec = self + .txs + .iter() + .map(|tx| tx.tx.sign_data(self.context.chain_id.as_u64())) + .filter_map(|res| res.ok()) + .collect::>(); + signatures.extend_from_slice(&self.precompile_events.get_ecrecover_events()); + if padding && self.txs.len() < self.circuits_params.max_txs { + // padding tx's sign data + signatures.push( + Transaction::dummy() + .sign_data(self.context.chain_id.as_u64()) + .unwrap(), + ); + } + signatures + } + /// Get a read-write record pub(crate) fn get_rws(&self, step: &ExecStep, index: usize) -> Rw { self.rws[step.rw_index(index)] @@ -294,6 +318,7 @@ pub fn block_convert( exp_circuit_pad_to: ::default(), prev_state_root: block.prev_state_root, keccak_inputs: circuit_input_builder::keccak_inputs(block, code_db)?, + precompile_events: block.precompile_events.clone(), eth_block: block.eth_block.clone(), }; let public_data = public_data_convert(&block);