From c85976c1e17ac2ef141080a3eb7a24580e3ca06b Mon Sep 17 00:00:00 2001 From: johntaiko Date: Fri, 23 Jun 2023 00:50:30 +0800 Subject: [PATCH] feat: add anchor transaction circuit (#105) * feat: add anchor transaction circuit * chore: improve comment * fix: clippy complain * feat: add tests * chore: update method signature * fix: clippy * feat: support meta hash * chore: improve the comment * chore: use LookupTable for readable * improve: readable * fix: query columns cross different regions * fix: sign verify test case * fix: add r == GX2 test case * chore: clippy complain * Some misc small improvements * improve: code clean * fix(IsEqual): alloc value_inv with closure * fix: clippy complain * Revert some formatting only changes * Simplify byte lookup --------- Co-authored-by: Brechtpd --- Cargo.lock | 5 +- gadgets/src/is_equal.rs | 252 ++++++++++ gadgets/src/lib.rs | 1 + zkevm-circuits/Cargo.toml | 3 +- zkevm-circuits/src/anchor_tx_circuit.rs | 458 +++++++++++++++++ zkevm-circuits/src/anchor_tx_circuit/dev.rs | 85 ++++ .../src/anchor_tx_circuit/sign_verify.rs | 465 ++++++++++++++++++ zkevm-circuits/src/anchor_tx_circuit/test.rs | 234 +++++++++ zkevm-circuits/src/lib.rs | 1 + zkevm-circuits/src/pi_circuit2.rs | 54 +- zkevm-circuits/src/table.rs | 5 + zkevm-circuits/src/table/byte_table.rs | 48 ++ zkevm-circuits/src/table/pi_table.rs | 70 +++ zkevm-circuits/src/table/tx_table.rs | 120 +++-- zkevm-circuits/src/tx_circuit.rs | 17 +- zkevm-circuits/src/util.rs | 8 + zkevm-circuits/src/witness.rs | 2 +- zkevm-circuits/src/witness/block.rs | 7 +- zkevm-circuits/src/witness/taiko.rs | 132 ++++- zkevm-circuits/src/witness/tx.rs | 59 ++- 20 files changed, 1925 insertions(+), 101 deletions(-) create mode 100644 gadgets/src/is_equal.rs create mode 100644 zkevm-circuits/src/anchor_tx_circuit.rs create mode 100644 zkevm-circuits/src/anchor_tx_circuit/dev.rs create mode 100644 zkevm-circuits/src/anchor_tx_circuit/sign_verify.rs create mode 100644 zkevm-circuits/src/anchor_tx_circuit/test.rs create mode 100644 zkevm-circuits/src/table/byte_table.rs create mode 100644 zkevm-circuits/src/table/pi_table.rs diff --git a/Cargo.lock b/Cargo.lock index 32193ce582..6299cbba9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3029,9 +3029,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" [[package]] name = "opaque-debug" @@ -5328,6 +5328,7 @@ dependencies = [ "mock", "num", "num-bigint", + "once_cell", "pretty_assertions", "rand", "rand_chacha", diff --git a/gadgets/src/is_equal.rs b/gadgets/src/is_equal.rs new file mode 100644 index 0000000000..839d015fad --- /dev/null +++ b/gadgets/src/is_equal.rs @@ -0,0 +1,252 @@ +//! IsEqual chip can be used to check equality of two expressions. + +use eth_types::Field; +use halo2_proofs::{ + circuit::{Chip, Region, Value}, + plonk::{Advice, Column, ConstraintSystem, Error, Expression, VirtualCells}, +}; + +use super::is_zero::{IsZeroChip, IsZeroInstruction}; + +/// Instruction that the IsEqual chip needs to implement. +pub trait IsEqualInstruction { + /// Assign lhs and rhs witnesses to the IsEqual chip's region. + fn assign( + &self, + region: &mut Region<'_, F>, + offset: usize, + lhs: Value, + rhs: Value, + ) -> Result<(), Error>; +} + +/// Config for the IsEqual chip. +#[derive(Clone, Debug)] +pub struct IsEqualConfig { + /// Stores an IsZero chip. + pub is_zero_chip: IsZeroChip, + /// Expression that denotes whether the chip evaluated to equal or not. + pub is_equal_expression: Expression, +} + +/// Chip that compares equality between two expressions. +#[derive(Clone, Debug)] +pub struct IsEqualChip { + /// Config for the IsEqual chip. + pub(crate) config: IsEqualConfig, +} + +impl IsEqualChip { + /// Configure the IsEqual chip. + pub fn configure( + meta: &mut ConstraintSystem, + value_inv: impl FnOnce(&mut ConstraintSystem) -> Column, + q_enable: impl FnOnce(&mut VirtualCells<'_, F>) -> Expression, + lhs: impl FnOnce(&mut VirtualCells<'_, F>) -> Expression, + rhs: impl FnOnce(&mut VirtualCells<'_, F>) -> Expression, + ) -> IsEqualConfig { + let value = |meta: &mut VirtualCells| lhs(meta) - rhs(meta); + let value_inv = value_inv(meta); + let is_zero_config = IsZeroChip::configure(meta, q_enable, value, value_inv); + let is_equal_expression = is_zero_config.is_zero_expression.clone(); + + IsEqualConfig { + is_zero_chip: IsZeroChip::construct(is_zero_config), + is_equal_expression, + } + } + + /// Construct an IsEqual chip given a config. + pub fn construct(config: IsEqualConfig) -> Self { + Self { config } + } +} + +impl IsEqualInstruction for IsEqualChip { + fn assign( + &self, + region: &mut Region<'_, F>, + offset: usize, + lhs: Value, + rhs: Value, + ) -> Result<(), Error> { + self.config.is_zero_chip.assign(region, offset, lhs - rhs)?; + + Ok(()) + } +} + +impl Chip for IsEqualChip { + type Config = IsEqualConfig; + type Loaded = (); + + fn config(&self) -> &Self::Config { + &self.config + } + + fn loaded(&self) -> &Self::Loaded { + &() + } +} + +#[cfg(test)] +mod tests { + use std::marker::PhantomData; + + use eth_types::Field; + use halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner, Value}, + dev::MockProver, + halo2curves::bn256::Fr as Fp, + plonk::{Advice, Circuit, Column, ConstraintSystem, Error, Selector, VirtualCells}, + poly::Rotation, + }; + use rand::Rng; + + use super::{IsEqualChip, IsEqualConfig, IsEqualInstruction}; + use crate::util::Expr; + + #[derive(Clone, Debug)] + struct TestCircuitConfig { + q_enable: Selector, + value: Column, + check: Column, + is_equal: IsEqualConfig, + } + + #[derive(Default)] + struct TestCircuit { + values: Vec, + checks: Vec, + _marker: PhantomData, + } + + impl Circuit for TestCircuit { + type Config = TestCircuitConfig; + type FloorPlanner = SimpleFloorPlanner; + type Params = (); + + fn without_witnesses(&self) -> Self { + Self::default() + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let q_enable = meta.complex_selector(); + let value = meta.advice_column(); + let check = meta.advice_column(); + + let lhs = |meta: &mut VirtualCells| meta.query_advice(value, Rotation::cur()); + let rhs = |_meta: &mut VirtualCells| RHS.expr(); + + let is_equal = IsEqualChip::configure( + meta, + |meta| meta.advice_column(), + |meta| meta.query_selector(q_enable), + lhs, + rhs, + ); + + let config = Self::Config { + q_enable, + value, + check, + is_equal, + }; + + meta.create_gate("check is_equal", |meta| { + let q_enable = meta.query_selector(q_enable); + + let check = meta.query_advice(check, Rotation::cur()); + + vec![q_enable * (config.is_equal.is_equal_expression.clone() - check)] + }); + + config + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + let chip = IsEqualChip::construct(config.is_equal.clone()); + + layouter.assign_region( + || "witness", + |mut region| { + let checks = self.checks.clone(); + + for (idx, (value, check)) in self.values.iter().cloned().zip(checks).enumerate() + { + region.assign_advice( + || "value", + config.value, + idx + 1, + || Value::known(F::from(value)), + )?; + region.assign_advice( + || "check", + config.check, + idx + 1, + || Value::known(F::from(check as u64)), + )?; + config.q_enable.enable(&mut region, idx + 1)?; + chip.assign( + &mut region, + idx + 1, + Value::known(F::from(value)), + Value::known(F::from(RHS)), + )?; + } + + Ok(()) + }, + ) + } + } + + macro_rules! try_test { + ($values:expr, $checks:expr, $rhs:expr, $is_ok_or_err:ident) => { + let k = usize::BITS - $values.len().leading_zeros() + 2; + let circuit = TestCircuit:: { + values: $values, + checks: $checks, + _marker: PhantomData, + }; + let prover = MockProver::::run(k, &circuit, vec![]).unwrap(); + assert!(prover.verify().$is_ok_or_err()); + }; + } + + fn random() -> u64 { + rand::thread_rng().gen::() + } + + #[test] + fn is_equal_gadget() { + try_test!( + vec![random(), 123, random(), 123, 123, random()], + vec![false, true, false, true, true, false], + 123, + is_ok + ); + try_test!( + vec![random(), 321321, 321321, random()], + vec![false, true, true, false], + 321321, + is_ok + ); + try_test!( + vec![random(), random(), random(), 1846123], + vec![false, false, false, true], + 1846123, + is_ok + ); + try_test!( + vec![123, 234, 345, 456], + vec![true, true, false, false], + 234, + is_err + ); + } +} diff --git a/gadgets/src/lib.rs b/gadgets/src/lib.rs index a7150262ee..5f2a85b922 100644 --- a/gadgets/src/lib.rs +++ b/gadgets/src/lib.rs @@ -13,6 +13,7 @@ pub mod batched_is_zero; pub mod binary_number; +pub mod is_equal; pub mod is_zero; pub mod less_than; pub mod mul_add; diff --git a/zkevm-circuits/Cargo.toml b/zkevm-circuits/Cargo.toml index cc1ca23fd6..263f9b377b 100644 --- a/zkevm-circuits/Cargo.toml +++ b/zkevm-circuits/Cargo.toml @@ -36,6 +36,7 @@ num-bigint = { version = "0.4" } rand_chacha = "0.3" snark-verifier = { git = "https://github.com/privacy-scaling-explorations/snark-verifier", tag = "v2023_04_20", default-features = false, features = ["loader_halo2", "system_halo2"] } cli-table = { version = "0.4", optional = true } +once_cell = "1.17.1" [dev-dependencies] bus-mapping = { path = "../bus-mapping", features = ["test"] } @@ -57,4 +58,4 @@ stats = ["warn-unimplemented", "dep:cli-table"] [[bin]] name = "stats" -required-features = ["stats"] +required-features = ["stats"] \ No newline at end of file diff --git a/zkevm-circuits/src/anchor_tx_circuit.rs b/zkevm-circuits/src/anchor_tx_circuit.rs new file mode 100644 index 0000000000..b2ecbe578c --- /dev/null +++ b/zkevm-circuits/src/anchor_tx_circuit.rs @@ -0,0 +1,458 @@ +//! Anchor circuit implementation. + +#[cfg(any(feature = "test", test, feature = "test-circuits"))] +mod dev; +#[cfg(any(feature = "test", test, feature = "test-circuits"))] +pub use dev::TestAnchorTxCircuit; +mod sign_verify; +#[cfg(any(feature = "test", test))] +mod test; + +use crate::{ + evm_circuit::util::constraint_builder::{BaseConstraintBuilder, ConstrainBuilderCommon}, + table::{byte_table::ByteTable, LookupTable, PiFieldTag, PiTable, TxFieldTag, TxTable}, + tx_circuit::TX_LEN, + util::{Challenges, SubCircuit, SubCircuitConfig}, + witness::{self, Taiko, Transaction}, +}; +use eth_types::{Field, ToScalar}; +use gadgets::util::{select, Expr}; +use halo2_proofs::{ + circuit::{Layouter, Region, Value}, + plonk::{Advice, Column, ConstraintSystem, Error, Expression, Fixed, SecondPhase, Selector}, + poly::Rotation, +}; +use sign_verify::SignVerifyConfig; +use std::marker::PhantomData; + +use self::sign_verify::GOLDEN_TOUCH_ADDRESS; + +// The anchor tx is the first tx +const ANCHOR_TX_ID: usize = 1; +const ANCHOR_TX_VALUE: u64 = 0; +const ANCHOR_TX_IS_CREATE: bool = false; +const ANCHOR_TX_GAS_PRICE: u64 = 0; +const MAX_DEGREE: usize = 9; +const BYTE_POW_BASE: u64 = 1 << 8; + +// function anchor( +// bytes32 l1Hash, +// bytes32 l1SignalRoot, +// uint64 l1Height, +// uint64 parentGasUsed +// ) +// anchor(bytes32,bytes32,uint64,uint64) = +// method_signature(4B)+l1Hash(32B)+l1SignalRoot(32B)+l1Height(8B)+parentGasUsed(8B) +const ANCHOR_CALL_DATA_LEN: usize = 84; + +struct CallData { + start: usize, + end: usize, +} + +/// Config for AnchorTxCircuit +#[derive(Clone, Debug)] +pub struct AnchorTxCircuitConfig { + tx_table: TxTable, + pi_table: PiTable, + byte_table: ByteTable, + + q_tag: Selector, + // the anchor transaction fixed fields + // Gas, GasPrice, CallerAddress, CalleeAddress, IsCreate, Value, CallDataLength, + // 2 rows: 0, tag, 0, value + tag: Column, + use_rlc: Column, + + // check: method_signature, l1Hash, l1SignalRoot, l1Height, parentGasUsed + q_call_data_part_start: Selector, + q_call_data_part_step: Selector, + q_call_data_part_end: Selector, + call_data_part_rlc_acc: Column, + call_data_part_tag: Column, + + sign_verify: SignVerifyConfig, +} + +/// Circuit configuration arguments +pub struct AnchorTxCircuitConfigArgs { + /// TxTable + pub tx_table: TxTable, + /// PiTable + pub pi_table: PiTable, + /// ByteTable + pub byte_table: ByteTable, + /// Challenges + pub challenges: Challenges>, +} + +impl SubCircuitConfig for AnchorTxCircuitConfig { + type ConfigArgs = AnchorTxCircuitConfigArgs; + + /// Return a new TxCircuitConfig + fn new( + meta: &mut ConstraintSystem, + Self::ConfigArgs { + tx_table, + pi_table, + byte_table, + challenges, + }: Self::ConfigArgs, + ) -> Self { + let q_tag = meta.complex_selector(); + let tag = meta.fixed_column(); + let use_rlc = meta.fixed_column(); + + let q_call_data_part_start = meta.complex_selector(); + let q_call_data_part_step = meta.complex_selector(); + let q_call_data_part_end = meta.complex_selector(); + let call_data_part_rlc_acc = meta.advice_column_in(SecondPhase); + let call_data_part_tag = meta.fixed_column(); + let sign_verify = + SignVerifyConfig::configure(meta, tx_table.clone(), byte_table.clone(), &challenges); + + // Verify the constant values of the anchor tx in the tx table. + // The tag and its corresponding constant value are stored next to each other vertically. + // (if the tag is at row i, its value is at row i + 1). + // Because of this, this lookup is enabled every other row. + meta.lookup_any("anchor fixed fields", |meta| { + let q_tag = meta.query_selector(q_tag); + [ + ANCHOR_TX_ID.expr(), + meta.query_fixed(tag, Rotation::cur()), + 0.expr(), + meta.query_fixed(tag, Rotation::next()), + ] + .into_iter() + .zip(tx_table.table_exprs(meta).into_iter()) + .map(|(arg, table)| (q_tag.expr() * arg, table)) + .collect() + }); + + // RLC/decode the calldata (per part) of the anchor tx (all bytes except the first one) + meta.create_gate( + "call_data_rlc_acc[i+1] = call_data_rlc_acc[i] * t + call_data[i+1]", + |meta| { + let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); + + let q_call_data_step = meta.query_selector(q_call_data_part_step); + let call_data_rlc_acc_next = + meta.query_advice(call_data_part_rlc_acc, Rotation::next()); + let call_data_rlc_acc = meta.query_advice(call_data_part_rlc_acc, Rotation::cur()); + let call_data_next = meta.query_advice(tx_table.value, Rotation::next()); + let use_rlc = meta.query_fixed(use_rlc, Rotation::cur()); + let randomness = challenges.lookup_input(); + let t = select::expr(use_rlc, randomness, BYTE_POW_BASE.expr()); + cb.require_equal( + "call_data_rlc_acc[i+1] = call_data_rlc_acc[i] * t + call_data[i+1]", + call_data_rlc_acc_next, + call_data_rlc_acc * t + call_data_next, + ); + cb.gate(q_call_data_step) + }, + ); + // RLC/decode the calldata (per part) of the anchor tx (first byte) + meta.create_gate("call_data_rlc_acc[0] = call_data[0]", |meta| { + let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); + + let q_call_data_start = meta.query_selector(q_call_data_part_start); + let call_data_rlc_acc = meta.query_advice(call_data_part_rlc_acc, Rotation::cur()); + let call_data = meta.query_advice(tx_table.value, Rotation::cur()); + + cb.require_equal( + "call_data_rlc_acc[0] = call_data[0]", + call_data_rlc_acc, + call_data, + ); + cb.gate(q_call_data_start) + }); + + // After RLC/decode of an input in the calldata, verify that the value matches the expected + // value in the public input table. + meta.lookup_any("call data in pi_table", |meta| { + let q_call_data_end = meta.query_selector(q_call_data_part_end); + let call_data_rlc_acc = meta.query_advice(call_data_part_rlc_acc, Rotation::cur()); + let call_data_tag = meta.query_fixed(call_data_part_tag, Rotation::cur()); + + [call_data_tag, call_data_rlc_acc] + .into_iter() + .zip(pi_table.table_exprs(meta).into_iter()) + .map(|(arg, table)| (q_call_data_end.expr() * arg, table)) + .collect::>() + }); + + Self { + tx_table, + pi_table, + byte_table, + + q_tag, + tag, + use_rlc, + + q_call_data_part_start, + q_call_data_part_step, + q_call_data_part_end, + call_data_part_rlc_acc, + call_data_part_tag, + sign_verify, + } + } +} + +impl AnchorTxCircuitConfig { + fn assign_anchor_tx_values( + &self, + region: &mut Region<'_, F>, + _anchor_tx: &Transaction, + taiko: &Taiko, + _challenges: &Challenges>, + ) -> Result<(), Error> { + // Gas, GasPrice, CallerAddress, CalleeAddress, IsCreate, Value, CallDataLength, + let mut offset = 0; + for (tag, value) in [ + ( + TxFieldTag::Gas, + Value::known(F::from(taiko.anchor_gas_cost)), + ), + ( + TxFieldTag::GasPrice, + Value::known(F::from(ANCHOR_TX_GAS_PRICE)), + ), + ( + TxFieldTag::CallerAddress, + Value::known( + GOLDEN_TOUCH_ADDRESS + .to_scalar() + .expect("anchor_tx.from too big"), + ), + ), + ( + TxFieldTag::CalleeAddress, + Value::known(taiko.l2_contract.to_scalar().expect("anchor_tx.to too big")), + ), + ( + TxFieldTag::IsCreate, + Value::known(F::from(ANCHOR_TX_IS_CREATE as u64)), + ), + (TxFieldTag::Value, Value::known(F::from(ANCHOR_TX_VALUE))), + ( + TxFieldTag::CallDataLength, + Value::known(F::from(ANCHOR_CALL_DATA_LEN as u64)), + ), + ] { + self.q_tag.enable(region, offset)?; + region.assign_fixed( + || "tag", + self.tag, + offset, + || Value::known(F::from(tag as u64)), + )?; + offset += 1; + region.assign_fixed(|| "anchor", self.tag, offset, || value)?; + offset += 1; + } + Ok(()) + } + + fn assign_call_data( + &self, + region: &mut Region<'_, F>, + anchor_tx: &Transaction, + call_data: &CallData, + challenges: &Challenges>, + ) -> Result<(), Error> { + let mut offset = call_data.start; + for (annotation, value, tag) in [ + ( + "method_signature", + &anchor_tx.call_data[..4], + PiFieldTag::MethodSign, + ), + ("l1_hash", &anchor_tx.call_data[4..36], PiFieldTag::L1Hash), + ( + "l1_signal_root", + &anchor_tx.call_data[36..68], + PiFieldTag::L1SignalRoot, + ), + ( + "l1_height", + &anchor_tx.call_data[68..76], + PiFieldTag::L1Height, + ), + ( + "parent_gas_used", + &anchor_tx.call_data[76..84], + PiFieldTag::ParentGasUsed, + ), + ] { + let mut rlc_acc = Value::known(F::ZERO); + // Use RLC encoding if the input doesn't fit within the field + let (use_rlc, t) = if value.len() * 8 > F::CAPACITY as usize { + (Value::known(F::ONE), challenges.evm_word()) + } else { + (Value::known(F::ZERO), Value::known(F::from(BYTE_POW_BASE))) + }; + for (idx, byte) in value.iter().enumerate() { + let row_offset = offset + idx; + + // RLC/Decode bytes + region.assign_fixed(|| annotation, self.use_rlc, row_offset, || use_rlc)?; + rlc_acc = rlc_acc * t + Value::known(F::from(*byte as u64)); + region.assign_advice( + || annotation, + self.call_data_part_rlc_acc, + row_offset, + || rlc_acc, + )?; + + // Set the tag for this input + region.assign_fixed( + || annotation, + self.call_data_part_tag, + row_offset, + || Value::known(F::from(tag as u64)), + )?; + + // Always enable the `start` selector at the first byte + if idx == 0 { + self.q_call_data_part_start.enable(region, row_offset)?; + } + // If we're at the last byte, enable the `end` selector. + // Otherwise enable the `step` selector. + if idx == value.len() - 1 { + self.q_call_data_part_end.enable(region, row_offset)?; + } else { + self.q_call_data_part_step.enable(region, row_offset)?; + } + } + offset += value.len(); + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn assign( + &self, + layouter: &mut impl Layouter, + anchor_tx: &Transaction, + txs: &[Transaction], + max_txs: usize, + max_calldata: usize, + taiko: &Taiko, + call_data: &CallData, + challenges: &Challenges>, + ) -> Result<(), Error> { + layouter.assign_region( + || "anchor transaction", + |ref mut region| { + // halo2 doesn't support create gates between different regions, + // so we need to load TxTable in the same region in order to create + // gate with TxTable's column + self.tx_table + .load_with_region(region, txs, max_txs, max_calldata, challenges)?; + self.assign_anchor_tx_values(region, anchor_tx, taiko, challenges)?; + self.assign_call_data(region, anchor_tx, call_data, challenges)?; + Ok(()) + }, + )?; + self.sign_verify.assign(layouter, anchor_tx, challenges) + } +} + +/// Anchor Transaction Circuit for verifying anchor transaction +#[derive(Clone, Default, Debug)] +pub struct AnchorTxCircuit { + max_txs: usize, + max_calldata: usize, + anchor_tx: Transaction, + txs: Vec, + taiko: Taiko, + _marker: PhantomData, +} + +impl AnchorTxCircuit { + /// Return a new TxCircuit + pub fn new( + max_txs: usize, + max_calldata: usize, + anchor_tx: Transaction, + txs: Vec, + taiko: Taiko, + ) -> Self { + AnchorTxCircuit { + max_txs, + max_calldata, + anchor_tx, + txs, + taiko, + _marker: PhantomData, + } + } + + /// Return the minimum number of rows required to prove an input of a + /// particular size. + pub(crate) fn min_num_rows(max_txs: usize) -> usize { + let rows_sign_verify = SignVerifyConfig::::min_num_rows(); + std::cmp::max(Self::call_data_end(max_txs), rows_sign_verify) + } + + fn call_data_start(max_txs: usize) -> usize { + max_txs * TX_LEN + 1 // empty row + } + + fn call_data_end(max_txs: usize) -> usize { + Self::call_data_start(max_txs) + ANCHOR_CALL_DATA_LEN + } +} + +impl SubCircuit for AnchorTxCircuit { + type Config = AnchorTxCircuitConfig; + + fn unusable_rows() -> usize { + // No column queried at more than 7 distinct rotations, so returns 10 as + // minimum unusable row. + 10 + } + + fn new_from_block(block: &witness::Block) -> Self { + Self::new( + block.circuits_params.max_txs, + block.circuits_params.max_calldata, + block.txs.first().unwrap().clone(), + block.txs.clone(), + block.taiko.clone(), + ) + } + + /// Make the assignments to the TxCircuit + fn synthesize_sub( + &self, + config: &Self::Config, + challenges: &Challenges>, + layouter: &mut impl Layouter, + ) -> Result<(), Error> { + let call_data = CallData { + start: Self::call_data_start(self.max_txs), + end: Self::call_data_end(self.max_txs), + }; + // the first transaction is the anchor transaction + config.assign( + layouter, + &self.anchor_tx, + &self.txs, + self.max_txs, + self.max_calldata, + &self.taiko, + &call_data, + challenges, + ) + } + + fn min_num_rows_block(block: &witness::Block) -> (usize, usize) { + ( + Self::min_num_rows(block.circuits_params.max_txs), + Self::min_num_rows(block.circuits_params.max_txs), + ) + } +} diff --git a/zkevm-circuits/src/anchor_tx_circuit/dev.rs b/zkevm-circuits/src/anchor_tx_circuit/dev.rs new file mode 100644 index 0000000000..4e72967e4c --- /dev/null +++ b/zkevm-circuits/src/anchor_tx_circuit/dev.rs @@ -0,0 +1,85 @@ +pub use super::AnchorTxCircuit; +use crate::{ + anchor_tx_circuit::{AnchorTxCircuitConfig, AnchorTxCircuitConfigArgs}, + table::{byte_table::ByteTable, PiTable, TxTable}, + util::{Challenges, SubCircuit, SubCircuitConfig}, + witness::{self, Taiko}, +}; +use eth_types::{Field, H256}; +use halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner}, + plonk::{Circuit, ConstraintSystem, Error}, +}; + +/// Test circuit for the anchor tx circuit. +#[derive(Clone, Debug, Default)] +pub struct TestAnchorTxCircuit { + txs: Vec, + taiko: Taiko, + max_calldata: usize, + circuit: AnchorTxCircuit, +} + +impl TestAnchorTxCircuit { + /// Create a new test circuit from a block. + pub fn new_from_block(block: &witness::Block) -> Self { + TestAnchorTxCircuit { + txs: block.txs.clone(), + taiko: block.taiko.clone(), + max_calldata: block.circuits_params.max_calldata, + circuit: AnchorTxCircuit::new_from_block(block), + } + } + + /// Modify the sign hash for test + pub fn sign_hash(&mut self, hash: H256) { + self.circuit.anchor_tx.tx_sign_hash = hash; + self.circuit.txs[0].tx_sign_hash = hash; + } +} + +impl Circuit for TestAnchorTxCircuit { + type Config = (AnchorTxCircuitConfig, Challenges); + type FloorPlanner = SimpleFloorPlanner; + type Params = (); + + fn without_witnesses(&self) -> Self { + Self::default() + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let tx_table = TxTable::construct(meta); + let pi_table = PiTable::construct(meta); + let byte_table = ByteTable::construct(meta); + let challenges = Challenges::construct(meta); + + let config = { + let challenges = challenges.exprs(meta); + AnchorTxCircuitConfig::new( + meta, + AnchorTxCircuitConfigArgs { + tx_table, + pi_table, + byte_table, + challenges, + }, + ) + }; + + (config, challenges) + } + + fn synthesize( + &self, + (config, challenges): Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + let challenges = challenges.values(&mut layouter); + config + .pi_table + .load(&mut layouter, &self.taiko, &challenges)?; + config.byte_table.load(&mut layouter)?; + self.circuit + .synthesize_sub(&config, &challenges, &mut layouter) + } +} diff --git a/zkevm-circuits/src/anchor_tx_circuit/sign_verify.rs b/zkevm-circuits/src/anchor_tx_circuit/sign_verify.rs new file mode 100644 index 0000000000..5db43201ef --- /dev/null +++ b/zkevm-circuits/src/anchor_tx_circuit/sign_verify.rs @@ -0,0 +1,465 @@ +//! # How to check the signature +//! +//! 1. IF r == GX1 OR r == GX2 +//! 2. IF r == GX2 THEN MUST CHECK IF s == 0 when r = GX1 +//! 3. IF s == 0 THEN GX1_MUL_PRIVATEKEY + msg_hash == N +//! +//! So, IF r == GX2 THEN GX1_MUL_PRIVATEKEY + msg_hash == N +//! +//! ## Why we only need to prove the equation: GX1_MUL_PRIVATEKEY + msg_hash == N +//! +//! based on the algorithm of [taiko-mono](https://github.com/taikoxyz/taiko-mono/blob/ad26803e5bcbcc76b812084b7bd08f45992e59dd/packages/protocol/contracts/libs/LibAnchorSignature.sol#L68) +//! +//! ### The formula of signature with K = 1 +//! +//! ``` +//! s = (GX1 * GOLDEN_TOUCH_PRIVATEKEY + msg_hash) (mod N) (K = 1) +//! ``` +//! +//! #### Formula deformation +//! +//! ``` +//! s = (GX1 * GOLDEN_TOUCH_PRIVATEKEY (mod N) + msg_hash (mod N)) (mod N) +//! ``` +//! +//! - Our `GX1_MUL_PRIVATEKEY` is equal to `GX1 * GOLDEN_TOUCH_PRIVATEKEY (mod N)` +//! - Our `msg_hash` has already been (mod N) in [zkevm-circuit](https://github.com/taikoxyz/zkevm-circuits/blob/839152c04ab3ddd1b8ce32632a407e5e7ef823a8/eth-types/src/geth_types.rs#L236) +//! +//! ```rust +//! let msg_hash = msg_hash.mod_floor(&*SECP256K1_Q); +//! ``` +//! +//! ### Summary +//! +//! ``` +//! because: 0 < GX1_MUL_PRIVATEKEY + msg_hash < 2N +//! need prove: (GX1_MUL_PRIVATEKEY + msg_hash) (mod N) == 0 +//! so: GX1_MUL_PRIVATEKEY + msg_hash == N +//! ``` + +use crate::{ + evm_circuit::util::{ + constraint_builder::{BaseConstraintBuilder, ConstrainBuilderCommon}, + rlc, split_u256_limb64, + }, + table::{byte_table::ByteTable, LookupTable, TxFieldTag, TxTable}, + util::Challenges, + witness::Transaction, +}; +use eth_types::{address, word, Address, Field, ToBigEndian, ToLittleEndian, Word, U256}; +use ethers_signers::LocalWallet; +use gadgets::{ + is_equal::{IsEqualChip, IsEqualConfig, IsEqualInstruction}, + mul_add::{MulAddChip, MulAddConfig}, + util::{split_u256, Expr}, +}; +use halo2_proofs::{ + circuit::{Layouter, Region, Value}, + plonk::{Advice, Column, ConstraintSystem, Error, Expression, Fixed, SecondPhase, Selector}, + poly::Rotation, +}; +use once_cell::sync::Lazy; +use std::str::FromStr; + +const MAX_DEGREE: usize = 9; +const BYTE_POW_BASE: u64 = 1 << 8; + +pub(crate) static GOLDEN_TOUCH_ADDRESS: Lazy
= + Lazy::new(|| address!("0x0000777735367b36bC9B61C50022d9D0700dB4Ec")); + +// 0x92954368afd3caa1f3ce3ead0069c1af414054aefe1ef9aeacc1bf426222ce38 +pub(crate) static GOLDEN_TOUCH_PRIVATEKEY: Lazy = + Lazy::new(|| word!("0x92954368afd3caa1f3ce3ead0069c1af414054aefe1ef9aeacc1bf426222ce38")); + +pub(crate) static GOLDEN_TOUCH_WALLET: Lazy = Lazy::new(|| { + LocalWallet::from_str("0x92954368afd3caa1f3ce3ead0069c1af414054aefe1ef9aeacc1bf426222ce38") + .unwrap() +}); + +// 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 +pub(crate) static GX1: Lazy = + Lazy::new(|| word!("0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798")); +static GX1_LO_HI: Lazy<(U256, U256)> = Lazy::new(|| split_u256(&GX1)); + +// 0xc6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 +pub(crate) static GX2: Lazy = + Lazy::new(|| word!("0xc6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5")); +static GX2_LO_HI: Lazy<(U256, U256)> = Lazy::new(|| split_u256(&GX2)); + +// 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 +pub(crate) static N: Lazy = + Lazy::new(|| word!("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141")); +static N_LO_HI: Lazy<(U256, U256)> = Lazy::new(|| split_u256(&N)); + +// private key 0x92954368afd3caa1f3ce3ead0069c1af414054aefe1ef9aeacc1bf426222ce38 +// GX1 * PRIVATEKEY(mod N) = 0x4341adf5a780b4a87939938fd7a032f6e6664c7da553c121d3b4947429639122 +pub(crate) static GX1_MUL_PRIVATEKEY: Lazy = + Lazy::new(|| word!("0x4341adf5a780b4a87939938fd7a032f6e6664c7da553c121d3b4947429639122")); +static GX1_MUL_PRIVATEKEY_LO_HI: Lazy<(U256, U256)> = Lazy::new(|| split_u256(&GX1_MUL_PRIVATEKEY)); +static GX1_MUL_PRIVATEKEY_LIMB64: Lazy<[U256; 4]> = + Lazy::new(|| split_u256_limb64(&GX1_MUL_PRIVATEKEY)); + +// GX2 * PRIVATEKEY(mod N) = 0x4a43b192ca74cab200d6c086df90fb729abca9e52d38b8fa0beb4eafe70956de +static GX2_MUL_PRIVATEKEY: Lazy = + Lazy::new(|| word!("0x4a43b192ca74cab200d6c086df90fb729abca9e52d38b8fa0beb4eafe70956de")); +static GX2_MUL_PRIVATEKEY_LO_HI: Lazy<(U256, U256)> = Lazy::new(|| split_u256(&GX2_MUL_PRIVATEKEY)); + +// # The circuit layout +// - msg_hash (c) +// - SigR +// +// We don't have to verify `s` and `v` of the signature, because the signature needs to be valid and +// and those values are fixed if `r` is fixed. +// `msg_hash` is calculated and verified in the tx circuit like any other transaction. + +#[derive(Debug, Clone)] +pub(crate) struct SignVerifyConfig { + tx_table: TxTable, + + q_sig_start: Selector, + q_sig_step: Selector, + q_sig_end: Selector, + tag: Column, + sig: Column, + sig_rlc_acc: Column, + + // split u256 into 4 64bit limbs + q_u64_start: Selector, + q_u64_step: Selector, + q_u64_end: Selector, + sig_u64_acc: Column, + + q_check: Selector, + mul_add: MulAddConfig, + is_equal_gx2: IsEqualConfig, +} + +impl SignVerifyConfig { + pub(crate) fn configure( + meta: &mut ConstraintSystem, + tx_table: TxTable, + byte_table: ByteTable, + challenges: &Challenges>, + ) -> Self { + let q_sig_start = meta.complex_selector(); + let q_sig_step = meta.complex_selector(); + let q_sig_end = meta.complex_selector(); + let tag = meta.fixed_column(); + let sig = meta.advice_column(); + let sig_rlc_acc = meta.advice_column_in(SecondPhase); + + let q_u64_start = meta.complex_selector(); + let q_u64_step = meta.complex_selector(); + let q_u64_end = meta.complex_selector(); + let sig_u64_acc = meta.advice_column(); + + // RLC of GX1 bytes + let gx1_rlc = rlc::expr( + GX1.to_le_bytes() + .map(|v| Expression::Constant(F::from(v as u64))) + .as_ref(), + challenges.evm_word(), + ); + // RLC of GX2 bytes + let gx2_rlc = rlc::expr( + GX2.to_le_bytes() + .map(|v| Expression::Constant(F::from(v as u64))) + .as_ref(), + challenges.evm_word(), + ); + + // Check if R == GX2 + let q_check = meta.complex_selector(); + let is_equal_gx2 = IsEqualChip::configure( + meta, + |meta| meta.advice_column_in(SecondPhase), + |meta| meta.query_selector(q_check), + |meta| meta.query_advice(sig_rlc_acc, Rotation(63)), // SigR == GX2 + |_| gx2_rlc.expr(), + ); + // Only enable the mul_add constraints when r == GX2 + let mul_add = MulAddChip::configure(meta, |meta| { + is_equal_gx2.is_equal_expression.expr() * meta.query_selector(q_check) + }); + + // RLC the signature data (per part) (all bytes except the first one) + meta.create_gate( + "sig_rlc_acc[i+1] = sig_rlc_acc[i] * randomness + sig[i+1]", + |meta| { + let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); + + let q_sig_step = meta.query_selector(q_sig_step); + let sig_rlc_acc_next = meta.query_advice(sig_rlc_acc, Rotation::next()); + let sig_rlc_acc = meta.query_advice(sig_rlc_acc, Rotation::cur()); + let sign = meta.query_advice(sig, Rotation::next()); + let randomness = challenges.evm_word(); + cb.require_equal( + "sig_rlc_acc[i+1] = sig_rlc_acc[i] * randomness + sign[i+1]", + sig_rlc_acc_next, + sig_rlc_acc * randomness + sign, + ); + cb.gate(q_sig_step) + }, + ); + // RLC the signature data (per part) (first byte) + meta.create_gate("sig_rlc_acc[0] = sign[0]", |meta| { + let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); + + let q_sig_start = meta.query_selector(q_sig_start); + let sig_rlc_acc = meta.query_advice(sig_rlc_acc, Rotation::cur()); + let sign = meta.query_advice(sig, Rotation::cur()); + + cb.require_equal("sig_rlc_acc[0] = sign[0]", sig_rlc_acc, sign); + cb.gate(q_sig_start) + }); + + // Make sure that `sig_r` and `msg_hash` have the correct value by looking up their RLCd + // representations in the tx table. + meta.lookup_any("sig_r or msg_hash in tx_table", |meta| { + let q_sig_end = meta.query_selector(q_sig_end); + + let tx_id = super::ANCHOR_TX_ID.expr(); + let tag = meta.query_fixed(tag, Rotation::cur()); + let index = 0.expr(); + let value = meta.query_advice(sig_rlc_acc, Rotation::cur()); + + [tx_id, tag, index, value] + .into_iter() + .zip(tx_table.table_exprs(meta).into_iter()) + .map(|(arg, table)| (q_sig_end.expr() * arg, table)) + .collect::>() + }); + + // Decode the 4 64bit limbs of msg_hash and R (all bytes except the first one) + meta.create_gate( + "sig_u64_acc[i+1] = sig_u64_acc[i] * BYTE_POW_BASE + sig[i+1]", + |meta| { + let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); + + let q_u64_step = meta.query_selector(q_u64_step); + let sig_u64_acc_next = meta.query_advice(sig_u64_acc, Rotation::next()); + let sig_u64_acc = meta.query_advice(sig_u64_acc, Rotation::cur()); + let sig_next = meta.query_advice(sig, Rotation::next()); + cb.require_equal( + "sig_u64_acc[i+1] = sig_u64_acc[i] * BYTE_POW_BASE + sig[i+1]", + sig_u64_acc_next, + sig_u64_acc * BYTE_POW_BASE.expr() + sig_next, + ); + cb.gate(q_u64_step) + }, + ); + // Decode the 4 64bit limbs of msg_hash and R (first byte) + meta.create_gate("sig_u64_acc[start] = sig[start]", |meta| { + let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); + + let q_u64_start = meta.query_selector(q_u64_start); + let sig_u64_acc = meta.query_advice(sig_u64_acc, Rotation::cur()); + let sig = meta.query_advice(sig, Rotation::cur()); + + cb.require_equal("sig_u64_acc[start] = sig[start]", sig_u64_acc, sig); + cb.gate(q_u64_start) + }); + + // Check that R has the expected value + meta.create_gate( + "IF r == GX2 THEN a(msg_hash) * b(1) + c(GX1_MUL_PRIVATEKEY) == d(N)", + |meta| { + let mut cb = BaseConstraintBuilder::new(MAX_DEGREE); + + // R needs to be either GX1 or Gx2 + let sig_rlc_acc = meta.query_advice(sig_rlc_acc, Rotation(63)); + cb.require_in_set("r in (GX1, GX2)", sig_rlc_acc, vec![gx1_rlc, gx2_rlc]); + + // In mul_add chip, we have a * b + c == d + // => a == msg_hash + // => b == 1 + // => c == GX1_MUL_PRIVATEKEY + // => d == N + // Check that the inputs a, b, and c into mul_add are correct + // a == msg_hash + let a = mul_add.a_limbs_cur(meta); + cb.require_equal("a.0", a.0, meta.query_advice(sig_u64_acc, Rotation(31))); + cb.require_equal("a.1", a.1, meta.query_advice(sig_u64_acc, Rotation(23))); + cb.require_equal("a.2", a.2, meta.query_advice(sig_u64_acc, Rotation(15))); + cb.require_equal("a.3", a.3, meta.query_advice(sig_u64_acc, Rotation(7))); + // b == 1 + let b = mul_add.b_limbs_cur(meta); + let one = split_u256_limb64(&U256::one()) + .map(|v| Expression::Constant(F::from(v.as_u64()))); + cb.require_equal("b.0", b.0, one[0].expr()); + cb.require_equal("b.1", b.1, one[1].expr()); + cb.require_equal("b.2", b.2, one[2].expr()); + cb.require_equal("b.3", b.3, one[3].expr()); + // c == GX1_MUL_PRIVATEKEY + let gx1_mul_privatekey_0 = + Expression::Constant(F::from_u128(GX1_MUL_PRIVATEKEY_LO_HI.0.as_u128())); + let gx1_mul_privatekey_1 = + Expression::Constant(F::from_u128(GX1_MUL_PRIVATEKEY_LO_HI.1.as_u128())); + let c = mul_add.c_lo_hi_cur(meta); + cb.require_equal("c.0", c.0, gx1_mul_privatekey_0); + cb.require_equal("c.1", c.1, gx1_mul_privatekey_1); + + // Now finally check that d == N + let n_0 = Expression::Constant(F::from_u128(N_LO_HI.0.as_u128())); + let n_1 = Expression::Constant(F::from_u128(N_LO_HI.1.as_u128())); + let d = mul_add.d_lo_hi_cur(meta); + cb.require_equal("d.0", d.0, n_0); + cb.require_equal("d.1", d.1, n_1); + + cb.gate(meta.query_selector(q_check)) + }, + ); + + // Range constraint all bytes + meta.lookup_any("ensure all bytes are actually byte values", |meta| { + let rpi_field_bytes = meta.query_advice(sig, Rotation::cur()); + [rpi_field_bytes] + .into_iter() + .zip(byte_table.table_exprs(meta).into_iter()) + .map(|(arg, table)| (arg, table)) + .collect::>() + }); + + Self { + tx_table, + + q_sig_start, + q_sig_step, + q_sig_end, + tag, + sig, + sig_rlc_acc, + + q_u64_start, + q_u64_step, + q_u64_end, + sig_u64_acc, + + q_check, + mul_add, + is_equal_gx2, + } + } + + #[allow(clippy::too_many_arguments)] + fn assign_field( + &self, + region: &mut Region<'_, F>, + _annotation: &'static str, + offset: &mut usize, + tag: TxFieldTag, + value: [u8; 32], + challenges: &Challenges>, + ) -> Result, Error> { + let mut rlc_acc = Value::known(F::ZERO); + let randomness = challenges.evm_word(); + + let mut assign_u64 = |offset: &mut usize, value: &[u8]| -> Result<(), Error> { + let mut u64_acc = Value::known(F::ZERO); + for (idx, byte) in value.iter().enumerate() { + let row_offset = *offset + idx; + u64_acc = u64_acc * Value::known(F::from(BYTE_POW_BASE)) + + Value::known(F::from(*byte as u64)); + region.assign_advice(|| "sig_u64_acc", self.sig_u64_acc, row_offset, || u64_acc)?; + // setup selector + if idx == 0 { + self.q_u64_start.enable(region, row_offset)?; + } + // the last offset of field + if idx == 7 { + self.q_u64_end.enable(region, row_offset)?; + } else { + self.q_u64_step.enable(region, row_offset)?; + } + } + *offset += 8; + Ok(()) + }; + + let mut assign_u64_offset = *offset; + assign_u64(&mut assign_u64_offset, &value[..8])?; + assign_u64(&mut assign_u64_offset, &value[8..16])?; + assign_u64(&mut assign_u64_offset, &value[16..24])?; + assign_u64(&mut assign_u64_offset, &value[24..])?; + + for (idx, byte) in value.iter().enumerate() { + let row_offset = *offset + idx; + region.assign_advice( + || "sig", + self.sig, + row_offset, + || Value::known(F::from(*byte as u64)), + )?; + region.assign_fixed( + || "tag", + self.tag, + row_offset, + || Value::known(F::from(tag as u64)), + )?; + + rlc_acc = rlc_acc * randomness + Value::known(F::from(*byte as u64)); + region.assign_advice(|| "sig_rlc_acc", self.sig_rlc_acc, row_offset, || rlc_acc)?; + // setup selector + if idx == 0 { + self.q_sig_start.enable(region, row_offset)?; + } + // the last offset of field + if idx == 31 { + self.q_sig_end.enable(region, row_offset)?; + } else { + self.q_sig_step.enable(region, row_offset)?; + } + } + *offset += 32; + Ok(rlc_acc) + } + + fn load_mul_add(&self, region: &mut Region<'_, F>, msg_hash: Word) -> Result<(), Error> { + let chip = MulAddChip::construct(self.mul_add.clone()); + chip.assign(region, 0, [msg_hash, U256::one(), *GX1_MUL_PRIVATEKEY, *N]) + } + + /// Return the minimum number of rows required to prove an input of a + /// particular size. + pub(crate) fn min_num_rows() -> usize { + 64 // msg_hash(32B) + sign_r(32B) + } + + pub(crate) fn assign( + &self, + layouter: &mut impl Layouter, + anchor_tx: &Transaction, + challenges: &Challenges>, + ) -> Result<(), Error> { + layouter.assign_region( + || "anchor sign verify", + |ref mut region| { + self.q_check.enable(region, 0)?; + + let msg_hash = U256::from_little_endian(&anchor_tx.tx_sign_hash.to_fixed_bytes()); + self.load_mul_add(region, msg_hash)?; + let mut offset = 0; + for (annotation, tag, do_check_equal_to_gx2, value) in [ + ( + "msg_hash", + TxFieldTag::TxSignHash, + false, + msg_hash.to_be_bytes(), + ), + ("sig_r", TxFieldTag::SigR, true, anchor_tx.r.to_be_bytes()), + ] { + let rlc_acc = + self.assign_field(region, annotation, &mut offset, tag, value, challenges)?; + if do_check_equal_to_gx2 { + let gx2_rlc = challenges + .evm_word() + .map(|randomness| rlc::value(&GX2.to_le_bytes(), randomness)); + let chip = IsEqualChip::construct(self.is_equal_gx2.clone()); + chip.assign(region, 0, rlc_acc, gx2_rlc)?; + } + } + Ok(()) + }, + ) + } +} diff --git a/zkevm-circuits/src/anchor_tx_circuit/test.rs b/zkevm-circuits/src/anchor_tx_circuit/test.rs new file mode 100644 index 0000000000..bb07cbbc57 --- /dev/null +++ b/zkevm-circuits/src/anchor_tx_circuit/test.rs @@ -0,0 +1,234 @@ +#![allow(unused_imports)] +use std::collections::HashMap; + +use super::{ + sign_verify::{ + GOLDEN_TOUCH_ADDRESS, GOLDEN_TOUCH_PRIVATEKEY, GOLDEN_TOUCH_WALLET, GX1, + GX1_MUL_PRIVATEKEY, GX2, N, + }, + *, +}; +use crate::{ + util::{log2_ceil, unusable_rows}, + witness::{block_convert, Block}, +}; +use bus_mapping::{ + circuit_input_builder::{CircuitInputBuilder, CircuitsParams}, + mock::BlockData, +}; +use eth_types::{ + address, bytecode, + geth_types::{GethData, Transaction}, + sign_types::{biguint_to_32bytes_le, ct_option_ok_or, sign, SignData, SECP256K1_Q}, + word, Address, Field, ToBigEndian, ToLittleEndian, ToWord, Word, H256, U256, +}; +use ethers_core::types::TransactionRequest; +use ethers_signers::{LocalWallet, Signer}; +use gadgets::{ + is_equal::IsEqualChip, + mul_add::{MulAddChip, MulAddConfig}, + util::{split_u256, Expr}, +}; +use halo2_proofs::{ + arithmetic::Field as _, + circuit::{Layouter, Region, Value}, + dev::{MockProver, VerifyFailure}, + halo2curves::{ + bn256::Fr, + ff::PrimeField, + group::Curve, + secp256k1::{self, Secp256k1Affine}, + }, + plonk::{Advice, Column, ConstraintSystem, Error, Expression, Fixed, SecondPhase, Selector}, + poly::Rotation, +}; +use itertools::Itertools; +use log::error; +use mock::{AddrOrWallet, MockTransaction, TestContext, MOCK_CHAIN_ID}; +use num::Integer; +use num_bigint::BigUint; +use once_cell::sync::Lazy; +use sha3::{Digest, Keccak256}; + +pub(crate) fn anchor_sign( + anchor_tx: &Transaction, + chain_id: u64, +) -> Result { + // msg = rlp([nonce, gasPrice, gas, to, value, data, sig_v, r, s]) + let req: TransactionRequest = anchor_tx.into(); + let msg = req.chain_id(chain_id).rlp(); + let msg_hash: [u8; 32] = Keccak256::digest(&msg) + .as_slice() + .to_vec() + .try_into() + .expect("hash length isn't 32 bytes"); + // msg_hash = msg_hash % q + let msg_hash = BigUint::from_bytes_be(msg_hash.as_slice()); + let msg_hash = msg_hash.mod_floor(&*SECP256K1_Q); + let msg_hash_le = biguint_to_32bytes_le(msg_hash); + let msg_hash = ct_option_ok_or( + secp256k1::Fq::from_repr(msg_hash_le), + libsecp256k1::Error::InvalidMessage, + )?; + let k1 = secp256k1::Fq::ONE; + let sk = ct_option_ok_or( + secp256k1::Fq::from_repr(GOLDEN_TOUCH_PRIVATEKEY.to_le_bytes()), + libsecp256k1::Error::InvalidSecretKey, + )?; + let generator = Secp256k1Affine::generator(); + let pk = generator * sk; + let pk = pk.to_affine(); + let (mut sig_r, mut sig_s) = sign(k1, sk, msg_hash); + let gx1 = ct_option_ok_or( + secp256k1::Fq::from_repr(GX1.to_le_bytes()), + libsecp256k1::Error::InvalidSignature, + )?; + assert!(sig_r == gx1); + if sig_s == secp256k1::Fq::ZERO { + let k2 = secp256k1::Fq::ONE + secp256k1::Fq::ONE; + (sig_r, sig_s) = sign(k2, sk, msg_hash); + let gx2 = ct_option_ok_or( + secp256k1::Fq::from_repr(GX2.to_le_bytes()), + libsecp256k1::Error::InvalidSignature, + )?; + assert!(sig_r == gx2); + } + Ok(SignData { + signature: (sig_r, sig_s), + pk, + msg_hash, + }) +} + +fn run(block: &Block, sign_hash: Option) -> Result<(), Vec> { + let k = log2_ceil( + AnchorTxCircuit::::unusable_rows() + + AnchorTxCircuit::::min_num_rows(block.circuits_params.max_txs), + ); + let mut circuit = TestAnchorTxCircuit::::new_from_block(block); + if let Some(sign_hash) = sign_hash { + circuit.sign_hash(sign_hash); + } + let prover = match MockProver::run(k + 3, &circuit, vec![]) { + Ok(prover) => prover, + Err(e) => panic!("{:#?}", e), + }; + prover.verify() +} + +fn gen_block( + max_txs: usize, + max_calldata: usize, + taiko: Taiko, + extra_func_tx: fn(&mut MockTransaction), +) -> Block { + let chain_id = (*MOCK_CHAIN_ID).as_u64(); + let mut wallets = HashMap::new(); + wallets.insert( + *GOLDEN_TOUCH_ADDRESS, + GOLDEN_TOUCH_WALLET.clone().with_chain_id(chain_id), + ); + + let code = bytecode! { + PUSH1(0x01) // value + PUSH1(0x02) // key + SSTORE + + PUSH3(0xbb) + }; + + let block: GethData = TestContext::<2, NUM_TXS>::new( + None, + |accs| { + accs[0] + .address(*GOLDEN_TOUCH_ADDRESS) + .balance(Word::from(1u64 << 20)); + accs[1].address(taiko.l2_contract).code(code); + }, + |mut txs, _accs| { + txs[0] + .gas(taiko.anchor_gas_cost.to_word()) + .gas_price(ANCHOR_TX_GAS_PRICE.to_word()) + .from(*GOLDEN_TOUCH_ADDRESS) + .to(taiko.l2_contract) + .input(taiko.anchor_call()) + .nonce(0) + .value(ANCHOR_TX_VALUE.to_word()); + extra_func_tx(txs[0]); + }, + |block, _tx| block, + ) + .unwrap() + .into(); + let circuits_params = CircuitsParams { + max_txs, + max_calldata, + ..Default::default() + }; + let mut builder = BlockData::new_from_geth_data_with_params(block.clone(), circuits_params) + .new_circuit_input_builder(); + builder + .handle_block(&block.eth_block, &block.geth_traces) + .unwrap(); + let mut block = block_convert::(&builder.block, &builder.code_db).unwrap(); + block.taiko = taiko; + block +} + +fn sign_tx(tx: &mut MockTransaction) { + let chain_id = (*MOCK_CHAIN_ID).as_u64(); + let _tx: Transaction = tx.to_owned().into(); + let sig_data = anchor_sign(&_tx, chain_id).unwrap(); + let sig_r = U256::from_little_endian(sig_data.signature.0.to_bytes().as_slice()); + let sig_s = U256::from_little_endian(sig_data.signature.1.to_bytes().as_slice()); + tx.sig_data((2712, sig_r, sig_s)); +} + +fn sign_tx_r_is_gx2(tx: &mut MockTransaction) { + let msg_hash = *N - *GX1_MUL_PRIVATEKEY; + let msg_hash = ct_option_ok_or( + secp256k1::Fq::from_repr(msg_hash.to_le_bytes()), + libsecp256k1::Error::InvalidMessage, + ) + .unwrap(); + let k2 = secp256k1::Fq::ONE + secp256k1::Fq::ONE; + let sk = ct_option_ok_or( + secp256k1::Fq::from_repr(GOLDEN_TOUCH_PRIVATEKEY.to_le_bytes()), + libsecp256k1::Error::InvalidSecretKey, + ) + .unwrap(); + let (sig_r, sig_s) = sign(k2, sk, msg_hash); + let sig_r = U256::from_little_endian(sig_r.to_bytes().as_slice()); + let sig_s = U256::from_little_endian(sig_s.to_bytes().as_slice()); + tx.sig_data((2712, sig_r, sig_s)); +} + +#[test] +fn anchor_tx_circuit_unusable_rows() { + assert_eq!( + AnchorTxCircuit::::unusable_rows(), + unusable_rows::>(()), + ) +} + +#[test] +fn anchor_test() { + let taiko = Taiko { + anchor_gas_cost: 150000, + ..Default::default() + }; + let block = gen_block::<1>(2, 100, taiko, sign_tx); + assert_eq!(run::(&block, None), Ok(())); +} + +#[test] +fn anchor_test_when_sign_r_is_gx2() { + let taiko = Taiko { + anchor_gas_cost: 150000, + ..Default::default() + }; + let msg_hash = *N - *GX1_MUL_PRIVATEKEY; + let msg_hash = H256::from(msg_hash.to_le_bytes()); + let block = gen_block::<1>(2, 100, taiko, sign_tx_r_is_gx2); + assert_eq!(run::(&block, Some(msg_hash)), Ok(())); +} diff --git a/zkevm-circuits/src/lib.rs b/zkevm-circuits/src/lib.rs index f4f0bb8cd7..23d1b5cff4 100644 --- a/zkevm-circuits/src/lib.rs +++ b/zkevm-circuits/src/lib.rs @@ -32,6 +32,7 @@ pub mod table; #[cfg(any(feature = "test", test))] pub mod test_util; +pub mod anchor_tx_circuit; pub mod tx_circuit; pub mod util; pub mod witness; diff --git a/zkevm-circuits/src/pi_circuit2.rs b/zkevm-circuits/src/pi_circuit2.rs index 0486ee1e79..c0d14a471d 100644 --- a/zkevm-circuits/src/pi_circuit2.rs +++ b/zkevm-circuits/src/pi_circuit2.rs @@ -2,7 +2,7 @@ use crate::{ evm_circuit::util::constraint_builder::{BaseConstraintBuilder, ConstrainBuilderCommon}, - table::{BlockTable, KeccakTable}, + table::{byte_table::ByteTable, BlockTable, KeccakTable, LookupTable}, util::{random_linear_combine_word as rlc, Challenges, SubCircuit, SubCircuitConfig}, witness, }; @@ -22,7 +22,7 @@ use halo2_proofs::{ }; use std::marker::PhantomData; -const MAX_DEGREE: usize = 10; +const MAX_DEGREE: usize = 9; const MIN_DEGREE: usize = 8; const RPI_CELL_IDX: usize = 0; const RPI_RLC_ACC_CELL_IDX: usize = 1; @@ -135,18 +135,7 @@ impl PublicData { /// create PublicData from block and taiko pub fn new(block: &witness::Block, taiko: &witness::Taiko) -> Self { - // left shift x by n bits - fn left_shift(x: T, n: u32) -> Word { - assert!(n < 256); - if n < 128 { - return x.to_word() * Word::from(2u128.pow(n)); - } - let mut bits = [0; 32]; - bits[..16].copy_from_slice(2u128.pow(n - 128).to_be_bytes().as_ref()); - bits[16..].copy_from_slice(0u128.to_be_bytes().as_ref()); - x.to_word() * Word::from(&bits[..]) - } - + use witness::left_shift; let field9 = left_shift(taiko.prover, 96) + left_shift(taiko.parent_gas_used as u64, 64) + left_shift(taiko.gas_used as u64, 32); @@ -158,7 +147,7 @@ impl PublicData { l1_signal_service: taiko.l1_signal_service.to_word(), l2_signal_service: taiko.l2_signal_service.to_word(), l2_contract: taiko.l2_contract.to_word(), - meta_hash: taiko.meta_hash.to_word(), + meta_hash: taiko.meta_hash.hash().to_word(), block_hash: taiko.block_hash.to_word(), parent_hash: taiko.parent_hash.to_word(), signal_root: taiko.signal_root.to_word(), @@ -218,7 +207,7 @@ pub struct PiCircuitConfig { q_start: Selector, q_not_end: Selector, - is_byte: Column, + byte_table: ByteTable, pi: Column, // keccak_hi, keccak_lo @@ -237,6 +226,8 @@ pub struct PiCircuitConfigArgs { pub block_table: BlockTable, /// KeccakTable pub keccak_table: KeccakTable, + /// ByteTable + pub byte_table: ByteTable, /// Challenges pub challenges: Challenges>, } @@ -250,6 +241,7 @@ impl SubCircuitConfig for PiCircuitConfig { Self::ConfigArgs { block_table, keccak_table, + byte_table, challenges, }: Self::ConfigArgs, ) -> Self { @@ -261,7 +253,6 @@ impl SubCircuitConfig for PiCircuitConfig { let q_field_end = meta.complex_selector(); let q_start = meta.complex_selector(); let q_not_end = meta.complex_selector(); - let is_byte = meta.fixed_column(); let is_field_rlc = meta.fixed_column(); let pi = meta.instance_column(); @@ -336,9 +327,11 @@ impl SubCircuitConfig for PiCircuitConfig { let q_field_end = meta.query_selector(q_field_end); let is_field = or::expr([q_field_step, q_field_end]); let rpi_field_bytes = meta.query_advice(rpi_field_bytes, Rotation::cur()); - - let is_byte = meta.query_fixed(is_byte, Rotation::cur()); - vec![(is_field * rpi_field_bytes, is_byte)] + [rpi_field_bytes] + .into_iter() + .zip(byte_table.table_exprs(meta).into_iter()) + .map(|(arg, table)| (is_field.expr() * arg, table)) + .collect::>() }); Self { @@ -351,7 +344,7 @@ impl SubCircuitConfig for PiCircuitConfig { q_start, q_not_end, - is_byte, + byte_table, is_field_rlc, pi, // keccak_hi, keccak_lo @@ -654,21 +647,7 @@ impl SubCircuit for PiCircuit { challenges: &Challenges>, layouter: &mut impl Layouter, ) -> Result<(), Error> { - layouter.assign_region( - || "is_byte table", - |mut region| { - for i in 0..(1 << 8) { - region.assign_fixed( - || format!("row_{}", i), - config.is_byte, - i, - || Value::known(F::from(i as u64)), - )?; - } - - Ok(()) - }, - )?; + config.byte_table.load(layouter)?; config.assign(layouter, &self.public_data, challenges) } } @@ -697,6 +676,7 @@ impl Circuit for PiTestCircuit { fn configure(meta: &mut ConstraintSystem) -> Self::Config { let block_table = BlockTable::construct(meta); let keccak_table = KeccakTable::construct(meta); + let byte_table = ByteTable::construct(meta); let challenges = Challenges::construct(meta); let challenge_exprs = challenges.exprs(meta); // let challenge_exprs = Challenges::mock(100.expr(), 100.expr()); @@ -707,6 +687,7 @@ impl Circuit for PiTestCircuit { PiCircuitConfigArgs { block_table, keccak_table, + byte_table, challenges: challenge_exprs, }, ), @@ -728,6 +709,7 @@ impl Circuit for PiTestCircuit { config .keccak_table .dev_load(&mut layouter, vec![&public_data.rpi_bytes()], &challenges)?; + config.byte_table.load(&mut layouter)?; self.0.synthesize_sub(&config, &challenges, &mut layouter) } diff --git a/zkevm-circuits/src/table.rs b/zkevm-circuits/src/table.rs index c9ba47c01a..223fe24d5d 100644 --- a/zkevm-circuits/src/table.rs +++ b/zkevm-circuits/src/table.rs @@ -28,6 +28,8 @@ use strum_macros::{EnumCount, EnumIter}; /// block table pub(crate) mod block_table; +/// byte table +pub(crate) mod byte_table; /// bytecode table pub(crate) mod bytecode_table; /// copy Table @@ -38,6 +40,8 @@ pub(crate) mod exp_table; pub(crate) mod keccak_table; /// mpt table pub(crate) mod mpt_table; +/// pi table +pub(crate) mod pi_table; /// rw table pub(crate) mod rw_table; /// tx table @@ -50,6 +54,7 @@ pub(crate) use exp_table::ExpTable; pub(crate) use keccak_table::KeccakTable; pub(crate) use mpt_table::{MPTProofType, MptTable}; +pub(crate) use pi_table::{PiFieldTag, PiTable}; pub(crate) use rw_table::RwTable; pub(crate) use tx_table::{ TxContextFieldTag, TxFieldTag, TxLogFieldTag, TxReceiptFieldTag, TxTable, diff --git a/zkevm-circuits/src/table/byte_table.rs b/zkevm-circuits/src/table/byte_table.rs new file mode 100644 index 0000000000..801df04235 --- /dev/null +++ b/zkevm-circuits/src/table/byte_table.rs @@ -0,0 +1,48 @@ +use super::*; + +/// byte table stores the value from 0 to 255 which is used to check whether the input is a byte +#[derive(Clone, Debug)] +pub struct ByteTable { + range256: Column, +} + +impl ByteTable { + /// construct the table + pub fn construct(meta: &mut ConstraintSystem) -> Self { + let range256 = meta.fixed_column(); + Self { range256 } + } + + /// load table + pub fn load(&self, layouter: &mut impl Layouter) -> Result<(), Error> { + layouter.assign_region( + || "is_byte table", + |mut region| { + for i in 0..(1 << 8) { + region.assign_fixed( + || format!("row_{}", i), + self.range256, + i, + || Value::known(F::from(i as u64)), + )?; + } + + Ok(()) + }, + ) + } +} + +impl LookupTable for ByteTable { + fn columns(&self) -> Vec> { + vec![self.range256.into()] + } + + fn annotations(&self) -> Vec { + vec![String::from("range256")] + } + + fn table_exprs(&self, meta: &mut VirtualCells) -> Vec> { + vec![meta.query_fixed(self.range256, Rotation::cur())] + } +} diff --git a/zkevm-circuits/src/table/pi_table.rs b/zkevm-circuits/src/table/pi_table.rs new file mode 100644 index 0000000000..ef2d35c387 --- /dev/null +++ b/zkevm-circuits/src/table/pi_table.rs @@ -0,0 +1,70 @@ +use crate::witness::Taiko; + +use super::*; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PiFieldTag { + Null = 0, + MethodSign, + L1Hash, + L1SignalRoot, + L1Height, + ParentGasUsed, +} +impl_expr!(PiFieldTag); + +#[derive(Clone, Debug)] +pub struct PiTable { + pub tag: Column, + pub value: Column, +} + +impl PiTable { + /// Construct a new TxTable + pub fn construct(meta: &mut ConstraintSystem) -> Self { + Self { + tag: meta.fixed_column(), + value: meta.advice_column_in(SecondPhase), + } + } + + /// Assign the `TxTable` from a list of block `Transaction`s, followig the + /// same layout that the Tx Circuit uses. + pub fn load( + &self, + layouter: &mut impl Layouter, + taiko: &Taiko, + challenges: &Challenges>, + ) -> Result<(), Error> { + layouter.assign_region( + || "pi table", + |mut region| { + let randomness = challenges.evm_word(); + for (offset, [tag, value]) in + taiko.table_assignments(randomness).into_iter().enumerate() + { + region.assign_fixed(|| "tag", self.tag, offset, || tag)?; + region.assign_advice(|| "value", self.value, offset, || value)?; + } + Ok(()) + }, + ) + } +} + +impl LookupTable for PiTable { + fn columns(&self) -> Vec> { + vec![self.tag.into(), self.value.into()] + } + + fn annotations(&self) -> Vec { + vec![String::from("tag"), String::from("value")] + } + + fn table_exprs(&self, meta: &mut VirtualCells) -> Vec> { + vec![ + meta.query_fixed(self.tag, Rotation::cur()), + meta.query_advice(self.value, Rotation::cur()), + ] + } +} diff --git a/zkevm-circuits/src/table/tx_table.rs b/zkevm-circuits/src/table/tx_table.rs index 017007a614..9d7cb74c91 100644 --- a/zkevm-circuits/src/table/tx_table.rs +++ b/zkevm-circuits/src/table/tx_table.rs @@ -29,6 +29,12 @@ pub enum TxFieldTag { TxSignHash, /// CallData CallData, + /// Signature field V. + SigV, + /// Signature field R. + SigR, + /// Signature field S. + SigS, } impl_expr!(TxFieldTag); @@ -92,6 +98,23 @@ impl TxTable { max_txs: usize, max_calldata: usize, challenges: &Challenges>, + ) -> Result<(), Error> { + layouter.assign_region( + || "tx table", + |mut region| self.load_with_region(&mut region, txs, max_txs, max_calldata, challenges), + ) + } + + /// Assign the `TxTable` from a list of block `Transaction`s, followig the + /// same layout that the Tx Circuit uses. + /// NOTE: This function is used by passing the region directly + pub fn load_with_region( + &self, + region: &mut Region<'_, F>, + txs: &[Transaction], + max_txs: usize, + max_calldata: usize, + challenges: &Challenges>, ) -> Result<(), Error> { assert!( txs.len() <= max_txs, @@ -132,58 +155,53 @@ impl TxTable { Ok(()) } - layouter.assign_region( - || "tx table", - |mut region| { - let mut offset = 0; - let advice_columns = [self.tx_id, self.index, self.value]; - assign_row( - &mut region, - offset, - &advice_columns, - &self.tag, - &[(); 4].map(|_| Value::known(F::ZERO)), - "all-zero", - )?; - offset += 1; + let mut offset = 0; + let advice_columns = [self.tx_id, self.index, self.value]; + assign_row( + region, + offset, + &advice_columns, + &self.tag, + &[(); 4].map(|_| Value::known(F::ZERO)), + "all-zero", + )?; + offset += 1; - // Tx Table contains an initial region that has a size parametrized by max_txs - // with all the tx data except for calldata, and then a second - // region that has a size parametrized by max_calldata with all - // the tx calldata. This is required to achieve a constant fixed column tag - // regardless of the number of input txs or the calldata size of each tx. - let mut calldata_assignments: Vec<[Value; 4]> = Vec::new(); - // Assign Tx data (all tx fields except for calldata) - let padding_txs: Vec<_> = (txs.len()..max_txs) - .map(|i| Transaction { - id: i + 1, - ..Default::default() - }) - .collect(); - for tx in txs.iter().chain(padding_txs.iter()) { - let [tx_data, tx_calldata] = tx.table_assignments(*challenges); - for row in tx_data { - assign_row(&mut region, offset, &advice_columns, &self.tag, &row, "")?; - offset += 1; - } - calldata_assignments.extend(tx_calldata.iter()); - } - // Assign Tx calldata - let padding_calldata = (sum_txs_calldata..max_calldata).map(|_| { - [ - Value::known(F::ZERO), - Value::known(F::from(TxContextFieldTag::CallData as u64)), - Value::known(F::ZERO), - Value::known(F::ZERO), - ] - }); - for row in calldata_assignments.into_iter().chain(padding_calldata) { - assign_row(&mut region, offset, &advice_columns, &self.tag, &row, "")?; - offset += 1; - } - Ok(()) - }, - ) + // Tx Table contains an initial region that has a size parametrized by max_txs + // with all the tx data except for calldata, and then a second + // region that has a size parametrized by max_calldata with all + // the tx calldata. This is required to achieve a constant fixed column tag + // regardless of the number of input txs or the calldata size of each tx. + let mut calldata_assignments: Vec<[Value; 4]> = Vec::new(); + // Assign Tx data (all tx fields except for calldata) + let padding_txs: Vec<_> = (txs.len()..max_txs) + .map(|i| Transaction { + id: i + 1, + ..Default::default() + }) + .collect(); + for tx in txs.iter().chain(padding_txs.iter()) { + let [tx_data, tx_calldata] = tx.table_assignments(*challenges); + for row in tx_data { + assign_row(region, offset, &advice_columns, &self.tag, &row, "")?; + offset += 1; + } + calldata_assignments.extend(tx_calldata.iter()); + } + // Assign Tx calldata + let padding_calldata = (sum_txs_calldata..max_calldata).map(|_| { + [ + Value::known(F::ZERO), + Value::known(F::from(TxContextFieldTag::CallData as u64)), + Value::known(F::ZERO), + Value::known(F::ZERO), + ] + }); + for row in calldata_assignments.into_iter().chain(padding_calldata) { + assign_row(region, offset, &advice_columns, &self.tag, &row, "")?; + offset += 1; + } + Ok(()) } } diff --git a/zkevm-circuits/src/tx_circuit.rs b/zkevm-circuits/src/tx_circuit.rs index ffd60993aa..fa90e9108a 100644 --- a/zkevm-circuits/src/tx_circuit.rs +++ b/zkevm-circuits/src/tx_circuit.rs @@ -30,10 +30,10 @@ use std::marker::PhantomData; /// Number of static fields per tx: [nonce, gas, gas_price, /// caller_address, callee_address, is_create, value, call_data_length, -/// call_data_gas_cost, tx_sign_hash]. +/// call_data_gas_cost, tx_sign_hash, r, s, v]. /// Note that call data bytes are layed out in the TxTable after all the static /// fields arranged by txs. -pub(crate) const TX_LEN: usize = 10; +pub(crate) const TX_LEN: usize = 13; /// Config for TxCircuit #[derive(Clone, Debug)] @@ -243,6 +243,19 @@ impl TxCircuit { TxFieldTag::TxSignHash, assigned_sig_verif.msg_hash_rlc.value().copied(), ), + (TxFieldTag::SigV, Value::known(F::from(tx.v))), + ( + TxFieldTag::SigR, + challenges + .evm_word() + .map(|challenge| rlc(tx.r.to_le_bytes(), challenge)), + ), + ( + TxFieldTag::SigS, + challenges + .evm_word() + .map(|challenge| rlc(tx.s.to_le_bytes(), challenge)), + ), ] { let assigned_cell = config.assign_row(&mut region, offset, i + 1, tag, 0, value)?; diff --git a/zkevm-circuits/src/util.rs b/zkevm-circuits/src/util.rs index 2a20e42e34..894beaedb7 100644 --- a/zkevm-circuits/src/util.rs +++ b/zkevm-circuits/src/util.rs @@ -30,6 +30,14 @@ pub(crate) fn random_linear_combine_word(bytes: [u8; 32], randomness: rlc::value(&bytes, randomness) } +pub(crate) fn rlc_be_bytes(bytes: &[u8], rand: Value) -> Value { + rand.map(|rand| { + bytes + .iter() + .fold(F::ZERO, |acc, byte| acc * rand + F::from(*byte as u64)) + }) +} + /// All challenges used in `SuperCircuit`. #[derive(Default, Clone, Copy, Debug)] pub struct Challenges { diff --git a/zkevm-circuits/src/witness.rs b/zkevm-circuits/src/witness.rs index edea9c3c7f..e6c57370db 100644 --- a/zkevm-circuits/src/witness.rs +++ b/zkevm-circuits/src/witness.rs @@ -15,4 +15,4 @@ mod tx; pub use bus_mapping::circuit_input_builder::Call; pub use tx::Transaction; mod taiko; -pub use taiko::Taiko; +pub use taiko::{left_shift, MetaHash, Taiko}; diff --git a/zkevm-circuits/src/witness/block.rs b/zkevm-circuits/src/witness/block.rs index 920cd909b0..e9fef72bc6 100644 --- a/zkevm-circuits/src/witness/block.rs +++ b/zkevm-circuits/src/witness/block.rs @@ -13,7 +13,7 @@ use bus_mapping::{ use eth_types::{Address, Field, ToLittleEndian, ToScalar, Word}; use halo2_proofs::circuit::Value; -use super::{tx::tx_convert, Bytecode, ExecStep, Rw, RwMap, Transaction}; +use super::{taiko::Taiko, tx::tx_convert, Bytecode, ExecStep, Rw, RwMap, Transaction}; // TODO: Remove fields that are duplicated in`eth_block` /// Block is the struct used by all circuits, which contains all the needed @@ -51,6 +51,8 @@ pub struct Block { pub keccak_inputs: Vec>, /// Original Block from geth pub eth_block: eth_types::Block, + /// Taiko witness + pub taiko: Taiko, } impl Block { @@ -231,7 +233,7 @@ pub fn block_convert( .txs() .iter() .enumerate() - .map(|(idx, tx)| tx_convert(tx, idx + 1)) + .map(|(idx, tx)| tx_convert(tx, block.chain_id.as_u64(), idx + 1)) .collect(), end_block_not_last: block.block_steps.end_block_not_last.clone(), end_block_last: block.block_steps.end_block_last.clone(), @@ -251,5 +253,6 @@ pub fn block_convert( prev_state_root: block.prev_state_root, keccak_inputs: circuit_input_builder::keccak_inputs(block, code_db)?, eth_block: block.eth_block.clone(), + taiko: Taiko::default(), }) } diff --git a/zkevm-circuits/src/witness/taiko.rs b/zkevm-circuits/src/witness/taiko.rs index c394870d8e..ec419aa684 100644 --- a/zkevm-circuits/src/witness/taiko.rs +++ b/zkevm-circuits/src/witness/taiko.rs @@ -1,9 +1,17 @@ //! extra witness for taiko circuits -use eth_types::{Address, Hash, H256}; +use std::iter; + +use crate::{table::PiFieldTag, util::rlc_be_bytes}; +use eth_types::{Address, Bytes, Field, Hash, ToBigEndian, ToWord, Word, H256}; +use halo2_proofs::circuit::Value; +use keccak256::plain::Keccak; + +// hash(anchor) +const ANCHOR_TX_METHOD_SIGNATURE: u32 = 0x3d384a4b; /// Taiko witness -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct Taiko { /// l1 signal service address pub l1_signal_service: Address, @@ -12,7 +20,7 @@ pub struct Taiko { /// l2 contract address pub l2_contract: Address, /// meta hash - pub meta_hash: Hash, + pub meta_hash: MetaHash, /// block hash value pub block_hash: Hash, /// the parent block hash @@ -33,4 +41,122 @@ pub struct Taiko { pub max_transactions_per_block: u64, /// maxBytesPerTxList pub max_bytes_per_tx_list: u64, + + /// anchor gas cost + pub anchor_gas_cost: u64, +} + +/// l1 meta hash +#[derive(Debug, Default, Clone)] +pub struct MetaHash { + /// meta id + pub id: u64, + /// meta timestamp + pub timestamp: u64, + /// l1 block height + pub l1_height: u64, + /// l1 block hash + pub l1_hash: Hash, + /// l1 block mix hash + pub l1_mix_hash: Hash, + /// deposits processed + pub deposits_processed: Hash, + /// tx list hash + pub tx_list_hash: Hash, + /// tx list byte start + pub tx_list_byte_start: u32, // u24 + /// tx list byte end + pub tx_list_byte_end: u32, // u24 + /// gas limit + pub gas_limit: u32, + /// beneficiary + pub beneficiary: Address, + /// treasury + pub treasury: Address, +} + +/// left shift x by n bits +pub fn left_shift(x: T, n: u32) -> Word { + assert!(n < 256); + if n < 128 { + return x.to_word() * Word::from(2u128.pow(n)); + } + let mut bits = [0; 32]; + bits[..16].copy_from_slice(2u128.pow(n - 128).to_be_bytes().as_ref()); + bits[16..].copy_from_slice(0u128.to_be_bytes().as_ref()); + x.to_word() * Word::from(&bits[..]) +} + +impl MetaHash { + /// get the hash of meta hash + pub fn hash(&self) -> Hash { + let field0 = left_shift(self.id, 192) + + left_shift(self.timestamp, 128) + + left_shift(self.l1_height, 64); + + let field5 = left_shift(self.tx_list_byte_start as u64, 232) + + left_shift(self.tx_list_byte_end as u64, 208) + + left_shift(self.gas_limit as u64, 176) + + left_shift(self.beneficiary, 16); + + let field6 = left_shift(self.treasury, 96); + + let input: Vec = iter::empty() + .chain(field0.to_be_bytes()) + .chain(self.l1_hash.to_fixed_bytes()) + .chain(self.l1_mix_hash.to_fixed_bytes()) + .chain(self.deposits_processed.to_fixed_bytes()) + .chain(self.tx_list_hash.to_fixed_bytes()) + .chain(field5.to_be_bytes()) + .chain(field6.to_be_bytes()) + .collect(); + let mut keccak = Keccak::default(); + keccak.update(&input); + let output = keccak.digest(); + Hash::from_slice(&output) + } +} + +impl Taiko { + /// gen anchor call + // anchor(l1_hash,signal_root,l1_height,parent_gas_used) + pub fn anchor_call(&self) -> Bytes { + let mut result = Vec::new(); + result.extend_from_slice(&ANCHOR_TX_METHOD_SIGNATURE.to_be_bytes()); + result.extend_from_slice(&self.meta_hash.l1_hash.to_fixed_bytes()); + result.extend_from_slice(&self.signal_root.to_fixed_bytes()); + result.extend_from_slice(&self.meta_hash.l1_height.to_be_bytes()); + result.extend_from_slice(&(self.parent_gas_used as u64).to_be_bytes()); + result.into() + } + + /// Assignments for pi table + pub fn table_assignments(&self, randomness: Value) -> [[Value; 2]; 6] { + [ + [ + Value::known(F::from(PiFieldTag::Null as u64)), + Value::known(F::ZERO), + ], + [ + Value::known(F::from(PiFieldTag::MethodSign as u64)), + Value::known(F::from(ANCHOR_TX_METHOD_SIGNATURE as u64)), + ], + [ + Value::known(F::from(PiFieldTag::L1Hash as u64)), + rlc_be_bytes(&self.meta_hash.l1_hash.to_fixed_bytes(), randomness), + ], + [ + Value::known(F::from(PiFieldTag::L1SignalRoot as u64)), + rlc_be_bytes(&self.signal_root.to_fixed_bytes(), randomness), + ], + [ + Value::known(F::from(PiFieldTag::L1Height as u64)), + Value::known(F::from(self.meta_hash.l1_height)), + ], + [ + Value::known(F::from(PiFieldTag::ParentGasUsed as u64)), + Value::known(F::from(self.parent_gas_used as u64)), + ], + ] + } } diff --git a/zkevm-circuits/src/witness/tx.rs b/zkevm-circuits/src/witness/tx.rs index 38be201d3f..3df92c3edb 100644 --- a/zkevm-circuits/src/witness/tx.rs +++ b/zkevm-circuits/src/witness/tx.rs @@ -1,8 +1,14 @@ use bus_mapping::circuit_input_builder; -use eth_types::{Address, Field, ToLittleEndian, ToScalar, Word}; +use eth_types::{ + sign_types::SignData, Address, Field, ToBigEndian, ToLittleEndian, ToScalar, Word, H256, +}; use halo2_proofs::circuit::Value; -use crate::{evm_circuit::util::rlc, table::TxContextFieldTag, util::Challenges}; +use crate::{ + evm_circuit::util::rlc, + table::TxContextFieldTag, + util::{rlc_be_bytes, Challenges}, +}; use super::{Call, ExecStep}; @@ -35,6 +41,14 @@ pub struct Transaction { pub calls: Vec, /// The steps executioned in the transaction pub steps: Vec, + /// "v" value of the transaction signature + pub v: u64, + /// "r" value of the transaction signature + pub r: Word, + /// "s" value of the transaction signature + pub s: Word, + /// tx sign hash + pub tx_sign_hash: H256, } impl Transaction { @@ -103,6 +117,32 @@ impl Transaction { Value::known(F::ZERO), Value::known(F::from(self.call_data_gas_cost)), ], + [ + Value::known(F::from(self.id as u64)), + Value::known(F::from(TxContextFieldTag::TxSignHash as u64)), + Value::known(F::ZERO), + challenges + .evm_word() + .map(|challenge| rlc::value(&self.tx_sign_hash.to_fixed_bytes(), challenge)), + ], + [ + Value::known(F::from(self.id as u64)), + Value::known(F::from(TxContextFieldTag::SigV as u64)), + Value::known(F::ZERO), + Value::known(F::from(self.v)), + ], + [ + Value::known(F::from(self.id as u64)), + Value::known(F::from(TxContextFieldTag::SigR as u64)), + Value::known(F::ZERO), + rlc_be_bytes(&self.r.to_be_bytes(), challenges.evm_word()), + ], + [ + Value::known(F::from(self.id as u64)), + Value::known(F::from(TxContextFieldTag::SigS as u64)), + Value::known(F::ZERO), + rlc_be_bytes(&self.s.to_be_bytes(), challenges.evm_word()), + ], ]; let tx_calldata = self .call_data @@ -121,7 +161,16 @@ impl Transaction { } } -pub(super) fn tx_convert(tx: &circuit_input_builder::Transaction, id: usize) -> Transaction { +pub(super) fn tx_convert( + tx: &circuit_input_builder::Transaction, + chain_id: u64, + id: usize, +) -> Transaction { + let sign_data: SignData = tx + .tx + .sign_data(chain_id) + .expect("Error computing tx_sign_hash"); + let tx_sign_hash = H256::from(&sign_data.msg_hash.to_bytes()); Transaction { id, nonce: tx.tx.nonce.as_u64(), @@ -136,5 +185,9 @@ pub(super) fn tx_convert(tx: &circuit_input_builder::Transaction, id: usize) -> call_data_gas_cost: tx.tx.call_data_gas_cost(), calls: tx.calls().to_vec(), steps: tx.steps().to_vec(), + v: tx.tx.v, + r: tx.tx.r, + s: tx.tx.s, + tx_sign_hash, } }