diff --git a/Cargo.lock b/Cargo.lock index 2c1b03daa7..66bc8de4c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,7 @@ dependencies = [ "eth-types", "ethers-core", "halo2_proofs", + "hex", "itertools", "log", "rand", diff --git a/aggregator/Cargo.toml b/aggregator/Cargo.toml index b6cf215506..3af735fa22 100644 --- a/aggregator/Cargo.toml +++ b/aggregator/Cargo.toml @@ -13,6 +13,7 @@ zkevm-circuits = { path = "../zkevm-circuits" } ark-std = "0.3.0" env_logger = "0.10.0" ethers-core = "0.17.0" +hex = "0.4.3" log = "0.4" itertools = "0.10.3" serde = { version = "1.0", features = ["derive"] } diff --git a/aggregator/src/chunk.rs b/aggregator/src/chunk.rs index 99899b3404..c22cd97db1 100644 --- a/aggregator/src/chunk.rs +++ b/aggregator/src/chunk.rs @@ -35,13 +35,35 @@ impl ChunkHash { pub fn from_witness_block(block: &Block, is_padding: bool) -> Self { // + let mut total_l1_popped = block.start_l1_queue_index; + log::debug!("chunk-hash: start_l1_queue_index = {}", total_l1_popped); let data_bytes = iter::empty() + // .chain(block_headers.iter().flat_map(|(&block_num, block)| { .chain(block.context.ctxs.iter().flat_map(|(b_num, b_ctx)| { - let num_txs = block + let num_l2_txs = block .txs .iter() - .filter(|tx| tx.block_number == *b_num) - .count() as u16; + .filter(|tx| !tx.tx_type.is_l1_msg() && tx.block_number == *b_num) + .count() as u64; + let num_l1_msgs = block + .txs + .iter() + .filter(|tx| tx.tx_type.is_l1_msg() && tx.block_number == *b_num) + // tx.nonce alias for queue_index for l1 msg tx + .map(|tx| tx.nonce) + .max() + .map_or(0, |max_queue_index| max_queue_index - total_l1_popped + 1); + total_l1_popped += num_l1_msgs; + + let num_txs = (num_l2_txs + num_l1_msgs) as u16; + log::debug!( + "chunk-hash: [block {}] total_l1_popped = {}, num_l1_msgs = {}, num_l2_txs = {}, num_txs = {}", + b_num, + total_l1_popped, + num_l1_msgs, + num_l2_txs, + num_txs, + ); iter::empty() // Block Values @@ -56,6 +78,10 @@ impl ChunkHash { .collect::>(); let data_hash = H256(keccak256(data_bytes)); + log::debug!( + "chunk-hash: data hash = {}", + hex::encode(data_hash.to_fixed_bytes()) + ); let post_state_root = block .context diff --git a/bus-mapping/src/circuit_input_builder.rs b/bus-mapping/src/circuit_input_builder.rs index d313459fb0..f7010ac5be 100644 --- a/bus-mapping/src/circuit_input_builder.rs +++ b/bus-mapping/src/circuit_input_builder.rs @@ -644,6 +644,7 @@ pub fn keccak_inputs(block: &Block, code_db: &CodeDB) -> Result>, Er // PI circuit keccak_inputs.extend(keccak_inputs_pi_circuit( block.chain_id, + block.start_l1_queue_index, block.prev_state_root, block.withdraw_root, &block.headers, @@ -736,17 +737,41 @@ pub fn get_dummy_tx_hash() -> H256 { fn keccak_inputs_pi_circuit( chain_id: u64, + start_l1_queue_index: u64, prev_state_root: Word, withdraw_trie_root: Word, block_headers: &BTreeMap, transactions: &[Transaction], ) -> Vec> { + let mut total_l1_popped = start_l1_queue_index; + log::debug!( + "start_l1_queue_index in keccak_inputs: {}", + start_l1_queue_index + ); let data_bytes = iter::empty() - .chain(block_headers.iter().flat_map(|(block_num, block)| { - let num_txs = transactions + .chain(block_headers.iter().flat_map(|(&block_num, block)| { + let num_l2_txs = transactions .iter() - .filter(|tx| tx.block_num == *block_num) - .count() as u16; + .filter(|tx| !tx.tx_type.is_l1_msg() && tx.block_num == block_num) + .count() as u64; + let num_l1_msgs = transactions + .iter() + .filter(|tx| tx.tx_type.is_l1_msg() && tx.block_num == block_num) + // tx.nonce alias for queue_index for l1 msg tx + .map(|tx| tx.nonce) + .max() + .map_or(0, |max_queue_index| max_queue_index - total_l1_popped + 1); + total_l1_popped += num_l1_msgs; + + let num_txs = (num_l2_txs + num_l1_msgs) as u16; + log::debug!( + "[block {}] total_l1_popped: {}, num_l1_msgs: {}, num_l2_txs: {}, num_txs: {}", + block_num, + total_l1_popped, + num_l1_msgs, + num_l2_txs, + num_txs, + ); iter::empty() // Block Values @@ -760,6 +785,10 @@ fn keccak_inputs_pi_circuit( .chain(transactions.iter().flat_map(|tx| tx.hash.to_fixed_bytes())) .collect::>(); let data_hash = H256(keccak256(&data_bytes)); + log::debug!( + "chunk data hash: {}", + hex::encode(data_hash.to_fixed_bytes()) + ); let after_state_root = block_headers .last_key_value() .map(|(_, blk)| blk.eth_block.state_root) diff --git a/bus-mapping/src/circuit_input_builder/block.rs b/bus-mapping/src/circuit_input_builder/block.rs index 99f6882845..4f5caf5d42 100644 --- a/bus-mapping/src/circuit_input_builder/block.rs +++ b/bus-mapping/src/circuit_input_builder/block.rs @@ -88,6 +88,8 @@ pub struct BlockHead { pub difficulty: Word, /// base fee pub base_fee: Word, + /// start l1 queue index + pub start_l1_queue_index: u64, /// Original block from geth pub eth_block: eth_types::Block, } @@ -108,6 +110,49 @@ impl BlockHead { Ok(Self { chain_id, history_hashes, + start_l1_queue_index: 0, + coinbase: eth_block + .author + .ok_or(Error::EthTypeError(eth_types::Error::IncompleteBlock))?, + gas_limit: eth_block.gas_limit.low_u64(), + number: eth_block + .number + .ok_or(Error::EthTypeError(eth_types::Error::IncompleteBlock))? + .low_u64() + .into(), + timestamp: eth_block.timestamp, + difficulty: if eth_block.difficulty.is_zero() { + eth_block + .mix_hash + .unwrap_or_default() + .to_fixed_bytes() + .into() + } else { + eth_block.difficulty + }, + base_fee: eth_block.base_fee_per_gas.unwrap_or_default(), + eth_block: eth_block.clone(), + }) + } + + /// Create a new block. + pub fn new_with_l1_queue_index( + chain_id: u64, + start_l1_queue_index: u64, + history_hashes: Vec, + eth_block: ð_types::Block, + ) -> Result { + if eth_block.base_fee_per_gas.is_none() { + // FIXME: resolve this once we have proper EIP-1559 support + log::debug!( + "This does not look like a EIP-1559 block - base_fee_per_gas defaults to zero" + ); + } + + Ok(Self { + chain_id, + history_hashes, + start_l1_queue_index, coinbase: eth_block .author .ok_or(Error::EthTypeError(eth_types::Error::IncompleteBlock))?, @@ -163,6 +208,8 @@ pub struct Block { pub circuits_params: CircuitsParams, /// chain id pub chain_id: u64, + /// start_l1_queue_index + pub start_l1_queue_index: u64, /// IO to/from the precompiled contract calls. pub precompile_events: PrecompileEvents, } @@ -217,6 +264,41 @@ impl Block { Ok(block) } + /// Create a new block. + pub fn new_with_l1_queue_index( + chain_id: u64, + start_l1_queue_index: u64, + history_hashes: Vec, + eth_block: ð_types::Block, + circuits_params: CircuitsParams, + ) -> Result { + let mut block = Self { + block_steps: BlockSteps { + end_block_not_last: ExecStep { + exec_state: ExecState::EndBlock, + ..ExecStep::default() + }, + end_block_last: ExecStep { + exec_state: ExecState::EndBlock, + ..ExecStep::default() + }, + }, + exp_events: Vec::new(), + chain_id, + start_l1_queue_index, + circuits_params, + ..Default::default() + }; + let info = BlockHead::new_with_l1_queue_index( + chain_id, + start_l1_queue_index, + history_hashes, + eth_block, + )?; + block.headers.insert(info.number.as_u64(), info); + Ok(block) + } + /// Return the list of transactions of this block. pub fn txs(&self) -> &[Transaction] { &self.txs diff --git a/zkevm-circuits/src/pi_circuit.rs b/zkevm-circuits/src/pi_circuit.rs index dcd617324d..84c2b65153 100644 --- a/zkevm-circuits/src/pi_circuit.rs +++ b/zkevm-circuits/src/pi_circuit.rs @@ -7,7 +7,7 @@ mod param; #[cfg(any(feature = "test", test, feature = "test-circuits"))] mod test; -use std::{iter, marker::PhantomData, str::FromStr}; +use std::{collections::BTreeMap, iter, marker::PhantomData, str::FromStr}; use crate::{evm_circuit::util::constraint_builder::ConstrainBuilderCommon, table::KeccakTable}; use bus_mapping::circuit_input_builder::get_dummy_tx_hash; @@ -47,8 +47,11 @@ use once_cell::sync::Lazy; use crate::{ evm_circuit::param::{N_BYTES_ACCOUNT_ADDRESS, N_BYTES_U64, N_BYTES_WORD}, pi_circuit::param::{COINBASE_OFFSET, DIFFICULTY_OFFSET}, - table::BlockContextFieldTag::{ - BaseFee, ChainId, Coinbase, CumNumTxs, Difficulty, GasLimit, NumTxs, Number, Timestamp, + table::{ + BlockContextFieldTag, + BlockContextFieldTag::{ + BaseFee, ChainId, Coinbase, CumNumTxs, Difficulty, GasLimit, NumTxs, Number, Timestamp, + }, }, util::rlc_be_bytes, }; @@ -70,6 +73,8 @@ pub(crate) static DIFFICULTY: Lazy = Lazy::new(|| read_env_var("DIFFICULTY pub struct PublicData { /// chain id pub chain_id: u64, + /// Start L1 QueueIndex + pub start_l1_queue_index: u64, /// Block Transactions pub transactions: Vec, /// Block contexts @@ -84,6 +89,7 @@ impl Default for PublicData { fn default() -> Self { PublicData { chain_id: 0, + start_l1_queue_index: 0, transactions: vec![], prev_state_root: H256::zero(), withdraw_trie_root: H256::zero(), @@ -93,16 +99,53 @@ impl Default for PublicData { } impl PublicData { + fn get_num_txs(&self) -> BTreeMap { + let mut num_txs_in_blocks = BTreeMap::new(); + // short for total number of l1 msgs popped before + let mut total_l1_popped = self.start_l1_queue_index; + log::debug!("start_l1_queue_index: {}", total_l1_popped); + for &block_num in self.block_ctxs.ctxs.keys() { + let num_l2_txs = self + .transactions + .iter() + .filter(|tx| !tx.tx_type.is_l1_msg() && tx.block_number == block_num) + .count() as u64; + let num_l1_msgs = self + .transactions + .iter() + .filter(|tx| tx.tx_type.is_l1_msg() && tx.block_number == block_num) + // tx.nonce alias for queue_index for l1 msg tx + .map(|tx| tx.nonce) + .max() + .map_or(0, |max_queue_index| max_queue_index - total_l1_popped + 1); + total_l1_popped += num_l1_msgs; + + let num_txs = num_l2_txs + num_l1_msgs; + num_txs_in_blocks.insert(block_num, num_txs); + + log::trace!( + "[block {}] total_l1_popped: {}, num_l1_msgs: {}, num_l2_txs: {}, num_txs: {}", + block_num, + total_l1_popped, + num_l1_msgs, + num_l2_txs, + num_txs + ); + } + + num_txs_in_blocks + } + /// Compute the bytes for dataHash from the verifier's perspective. fn data_bytes(&self) -> Vec { + let num_txs_in_blocks = self.get_num_txs(); let result = iter::empty() .chain(self.block_ctxs.ctxs.iter().flat_map(|(block_num, block)| { - let num_txs = self - .transactions - .iter() - .filter(|tx| tx.block_number == *block_num) - .count() as u16; - + let num_txs = num_txs_in_blocks + .get(block_num) + .cloned() + .unwrap_or_else(|| panic!("get num_txs in block {block_num}")) + as u16; iter::empty() // Block Values .chain(block.number.as_u64().to_be_bytes()) @@ -153,6 +196,8 @@ impl PublicData { fn get_pi(&self) -> H256 { let data_hash = H256(keccak256(self.data_bytes())); + log::debug!("data hash: {}", hex::encode(data_hash.to_fixed_bytes())); + let pi_bytes = self.pi_bytes(data_hash); let pi_hash = keccak256(pi_bytes); @@ -541,6 +586,16 @@ impl SubCircuitConfig for PiCircuitConfig { meta.query_advice(cum_num_txs, Rotation::cur()) + num_txs, ); + cb.condition(meta.query_fixed(is_block_num_txs, Rotation::cur()), |cb| { + cb.require_equal( + "block_table.value' == cum_num_txs' if block_table.tag == Nums", + meta.query_advice(block_table.value, Rotation::next()), + meta.query_advice(cum_num_txs, Rotation::next()), + ); + }); + + // NOTE: cum_num_txs is already enforced to start with 0 using copy constraint. + cb.gate(meta.query_fixed(q_block_tag, Rotation::cur())) } ); @@ -605,6 +660,7 @@ impl PiCircuitConfig { .iter() .map(|tx| tx.hash) .collect::>(); + let num_txs_in_blocks = public_data.get_num_txs(); let mut offset = 0; let mut block_copy_cells = vec![]; @@ -636,11 +692,9 @@ impl PiCircuitConfig { .enumerate() { let is_rpi_padding = i >= block_values.ctxs.len(); - let num_txs = public_data - .transactions - .iter() - .filter(|tx| tx.block_number == block.number.as_u64()) - .count() as u16; + let block_num = block.number.as_u64(); + let num_txs = num_txs_in_blocks.get(&block_num).cloned().unwrap_or(0) as u16; + log::debug!("num_txs in block {}: {}", block_num, num_txs); // Assign fields in pi columns and connect them to block table let fields = vec![ @@ -668,10 +722,15 @@ impl PiCircuitConfig { false, challenges, )?; - block_copy_cells.push(( - cells[RPI_CELL_IDX].clone(), - block_table_offset + block_offset, - )); + // do not copy num_txs to block table as the meaning of num_txs + // in block table is len(block.txs), and this is different from num_l1_msgs + + // num_l2_txs + if block_offset != NUM_TXS_OFFSET { + block_copy_cells.push(( + cells[RPI_CELL_IDX].clone(), + block_table_offset + block_offset, + )); + } } block_table_offset += BLOCK_LEN; @@ -1274,6 +1333,12 @@ impl PiCircuitConfig { || Value::known(F::zero()), )?; } + region.assign_fixed( + || "tag of first row of block table", + self.block_table.tag, + offset, + || Value::known(F::from(BlockContextFieldTag::Null as u64)), + )?; for column in block_table_columns .iter() .chain(iter::once(&self.cum_num_txs)) @@ -1290,11 +1355,13 @@ impl PiCircuitConfig { let mut cum_num_txs = 0usize; let mut block_value_cells = vec![]; let block_ctxs = &public_data.block_ctxs; + // let num_txs_in_blocks = public_data.get_num_txs(); for block_ctx in block_ctxs.ctxs.values().cloned().chain( (block_ctxs.ctxs.len()..max_inner_blocks) .into_iter() .map(|_| BlockContext::padding(public_data.chain_id)), ) { + // note that let num_txs = public_data .transactions .iter() @@ -1363,10 +1430,12 @@ impl PiCircuitConfig { )?; } if *tag == CumNumTxs { + // only increase cum_num_txs when the block_table.tag = CumNumTxs cum_num_txs_field = F::from(cum_num_txs as u64); } if offset == 1 { assert_eq!(cum_num_txs_field, F::zero()); + // use copy constraint to make sure that cum_num_txs starts with 0 region.assign_advice_from_constant( || "cum_num_txs", self.cum_num_txs, @@ -1414,6 +1483,7 @@ impl PiCircuit { let chain_id = block.chain_id; let public_data = PublicData { chain_id, + start_l1_queue_index: block.start_l1_queue_index, transactions: block.txs.clone(), block_ctxs: block.context.clone(), prev_state_root: H256(block.mpt_updates.old_root().to_be_bytes()), diff --git a/zkevm-circuits/src/witness.rs b/zkevm-circuits/src/witness.rs index 3dfb446485..da94347026 100644 --- a/zkevm-circuits/src/witness.rs +++ b/zkevm-circuits/src/witness.rs @@ -3,7 +3,10 @@ //! used to generate witnesses for circuits. mod block; -pub use block::{block_apply_mpt_state, block_convert, Block, BlockContext, BlockContexts}; +pub use block::{ + block_apply_mpt_state, block_convert, block_convert_with_l1_queue_index, Block, BlockContext, + BlockContexts, +}; mod bytecode; pub use bytecode::Bytecode; diff --git a/zkevm-circuits/src/witness/block.rs b/zkevm-circuits/src/witness/block.rs index 7e6976acdf..f117384fb7 100644 --- a/zkevm-circuits/src/witness/block.rs +++ b/zkevm-circuits/src/witness/block.rs @@ -65,6 +65,8 @@ pub struct Block { pub mpt_updates: MptUpdates, /// Chain ID pub chain_id: u64, + /// StartL1QueueIndex + pub start_l1_queue_index: u64, /// IO to/from precompile calls. pub precompile_events: PrecompileEvents, } @@ -458,10 +460,23 @@ pub fn block_convert( keccak_inputs: circuit_input_builder::keccak_inputs(block, code_db)?, mpt_updates, chain_id, + start_l1_queue_index: 0, precompile_events: block.precompile_events.clone(), }) } +/// Convert a block struct in bus-mapping to a witness block used in circuits +pub fn block_convert_with_l1_queue_index( + block: &circuit_input_builder::Block, + code_db: &bus_mapping::state_db::CodeDB, + start_l1_queue_index: u64, +) -> Result, Error> { + let mut block = block_convert(block, code_db)?; + block.start_l1_queue_index = start_l1_queue_index; + + Ok(block) +} + /// Attach witness block with mpt states pub fn block_apply_mpt_state(block: &mut Block, mpt_state: &MptState) { block.mpt_updates.fill_state_roots(mpt_state);