diff --git a/evm_arithmetization/src/fixed_recursive_verifier.rs b/evm_arithmetization/src/fixed_recursive_verifier.rs index ed62014f5..3fa6e208f 100644 --- a/evm_arithmetization/src/fixed_recursive_verifier.rs +++ b/evm_arithmetization/src/fixed_recursive_verifier.rs @@ -12,7 +12,7 @@ use plonky2::field::extension::Extendable; use plonky2::fri::FriParams; use plonky2::gates::constant::ConstantGate; use plonky2::gates::noop::NoopGate; -use plonky2::hash::hash_types::RichField; +use plonky2::hash::hash_types::{RichField, NUM_HASH_OUT_ELTS}; use plonky2::iop::challenger::RecursiveChallenger; use plonky2::iop::target::{BoolTarget, Target}; use plonky2::iop::witness::{PartialWitness, WitnessWrite}; @@ -20,7 +20,7 @@ use plonky2::plonk::circuit_builder::CircuitBuilder; use plonky2::plonk::circuit_data::{ CircuitConfig, CircuitData, CommonCircuitData, VerifierCircuitData, VerifierCircuitTarget, }; -use plonky2::plonk::config::{AlgebraicHasher, GenericConfig}; +use plonky2::plonk::config::{AlgebraicHasher, GenericConfig, GenericHashOut}; use plonky2::plonk::proof::{ProofWithPublicInputs, ProofWithPublicInputsTarget}; use plonky2::recursion::cyclic_recursion::check_cyclic_proof_verifier_data; use plonky2::recursion::dummy_circuit::cyclic_base_proof; @@ -75,6 +75,9 @@ where /// The block circuit, which verifies an aggregation root proof and an /// optional previous block proof. pub block: BlockCircuitData, + /// The two-to-one block aggregation circuit, which verifies two unrelated + /// block proofs. + pub two_to_one_block: TwoToOneBlockCircuitData, /// Holds chains of circuits for each table and for each initial /// `degree_bits`. pub by_table: [RecursiveCircuitsForTable; NUM_TABLES], @@ -211,25 +214,25 @@ where struct AggregationChildTarget { is_agg: BoolTarget, agg_proof: ProofWithPublicInputsTarget, - evm_proof: ProofWithPublicInputsTarget, + base_proof: ProofWithPublicInputsTarget, } impl AggregationChildTarget { fn to_buffer(&self, buffer: &mut Vec) -> IoResult<()> { buffer.write_target_bool(self.is_agg)?; buffer.write_target_proof_with_public_inputs(&self.agg_proof)?; - buffer.write_target_proof_with_public_inputs(&self.evm_proof)?; + buffer.write_target_proof_with_public_inputs(&self.base_proof)?; Ok(()) } fn from_buffer(buffer: &mut Buffer) -> IoResult { let is_agg = buffer.read_target_bool()?; let agg_proof = buffer.read_target_proof_with_public_inputs()?; - let evm_proof = buffer.read_target_proof_with_public_inputs()?; + let base_proof = buffer.read_target_proof_with_public_inputs()?; Ok(Self { is_agg, agg_proof, - evm_proof, + base_proof, }) } @@ -238,8 +241,20 @@ impl AggregationChildTarget { builder: &mut CircuitBuilder, ) -> PublicValuesTarget { let agg_pv = PublicValuesTarget::from_public_inputs(&self.agg_proof.public_inputs); - let evm_pv = PublicValuesTarget::from_public_inputs(&self.evm_proof.public_inputs); - PublicValuesTarget::select(builder, self.is_agg, agg_pv, evm_pv) + let base_pv = PublicValuesTarget::from_public_inputs(&self.base_proof.public_inputs); + PublicValuesTarget::select(builder, self.is_agg, agg_pv, base_pv) + } + + fn public_inputs>( + &self, + builder: &mut CircuitBuilder, + ) -> Vec { + zip_eq( + &self.agg_proof.public_inputs, + &self.base_proof.public_inputs, + ) + .map(|(&agg_pv, &base_pv)| builder.select(self.is_agg, agg_pv, base_pv)) + .collect() } } @@ -301,6 +316,56 @@ where } } +/// Data for the two-to-one block circuit, which is used to generate a +/// proof of two unrelated proofs. +#[derive(Eq, PartialEq, Debug)] +pub struct TwoToOneBlockCircuitData +where + F: RichField + Extendable, + C: GenericConfig, +{ + pub circuit: CircuitData, + lhs: AggregationChildTarget, + rhs: AggregationChildTarget, + cyclic_vk: VerifierCircuitTarget, +} + +impl TwoToOneBlockCircuitData +where + F: RichField + Extendable, + C: GenericConfig, +{ + fn to_buffer( + &self, + buffer: &mut Vec, + gate_serializer: &dyn GateSerializer, + generator_serializer: &dyn WitnessGeneratorSerializer, + ) -> IoResult<()> { + buffer.write_circuit_data(&self.circuit, gate_serializer, generator_serializer)?; + self.lhs.to_buffer(buffer)?; + self.rhs.to_buffer(buffer)?; + buffer.write_target_verifier_circuit(&self.cyclic_vk)?; + Ok(()) + } + + fn from_buffer( + buffer: &mut Buffer, + gate_serializer: &dyn GateSerializer, + generator_serializer: &dyn WitnessGeneratorSerializer, + ) -> IoResult { + let circuit = buffer.read_circuit_data(gate_serializer, generator_serializer)?; + let lhs = AggregationChildTarget::from_buffer(buffer)?; + let rhs = AggregationChildTarget::from_buffer(buffer)?; + let cyclic_vk = buffer.read_target_verifier_circuit()?; + Ok(Self { + circuit, + lhs, + rhs, + cyclic_vk, + }) + } +} + impl AllRecursiveCircuits where F: RichField + Extendable, @@ -333,6 +398,8 @@ where .to_buffer(&mut buffer, gate_serializer, generator_serializer)?; self.block .to_buffer(&mut buffer, gate_serializer, generator_serializer)?; + self.two_to_one_block + .to_buffer(&mut buffer, gate_serializer, generator_serializer)?; if !skip_tables { for table in &self.by_table { table.to_buffer(&mut buffer, gate_serializer, generator_serializer)?; @@ -370,6 +437,11 @@ where )?; let block = BlockCircuitData::from_buffer(&mut buffer, gate_serializer, generator_serializer)?; + let two_to_one_block = TwoToOneBlockCircuitData::from_buffer( + &mut buffer, + gate_serializer, + generator_serializer, + )?; let by_table = match skip_tables { true => (0..NUM_TABLES) @@ -405,6 +477,7 @@ where root, aggregation, block, + two_to_one_block, by_table, }) } @@ -494,10 +567,13 @@ where let root = Self::create_root_circuit(&by_table, stark_config); let aggregation = Self::create_aggregation_circuit(&root); let block = Self::create_block_circuit(&aggregation); + let two_to_one_block = Self::create_two_to_one_block_circuit(&block); + Self { root, aggregation, block, + two_to_one_block, by_table, } } @@ -662,8 +738,9 @@ where let mut builder = CircuitBuilder::::new(root.circuit.common.config.clone()); let public_values = add_virtual_public_values(&mut builder); let cyclic_vk = builder.add_verifier_data_public_inputs(); - let lhs = Self::add_agg_child(&mut builder, root); - let rhs = Self::add_agg_child(&mut builder, root); + + let lhs = Self::add_agg_child(&mut builder, &root.circuit); + let rhs = Self::add_agg_child(&mut builder, &root.circuit); let lhs_public_values = lhs.public_values(&mut builder); let rhs_public_values = rhs.public_values(&mut builder); @@ -768,24 +845,38 @@ where builder.connect(lhs.gas_used_after, rhs.gas_used_before); } + /// Extend a circuit to verify one of two proofs. + /// + /// # Arguments + /// + /// - `builder`: The circuit builder object. + /// - `base_circuit`: Circuit data describing the circuit of the base proof. + /// + /// # Outputs + /// + /// Returns a [`TwoToOneBlockChildTarget`] object. fn add_agg_child( builder: &mut CircuitBuilder, - root: &RootCircuitData, + base_circuit: &CircuitData, ) -> AggregationChildTarget { - let common = &root.circuit.common; - let root_vk = builder.constant_verifier_data(&root.circuit.verifier_only); + let common = &base_circuit.common; + let base_vk = builder.constant_verifier_data(&base_circuit.verifier_only); let is_agg = builder.add_virtual_bool_target_safe(); let agg_proof = builder.add_virtual_proof_with_pis(common); - let evm_proof = builder.add_virtual_proof_with_pis(common); + let base_proof = builder.add_virtual_proof_with_pis(common); builder .conditionally_verify_cyclic_proof::( - is_agg, &agg_proof, &evm_proof, &root_vk, common, + is_agg, + &agg_proof, + &base_proof, + &base_vk, + common, ) .expect("Failed to build cyclic recursion circuit"); AggregationChildTarget { is_agg, agg_proof, - evm_proof, + base_proof, } } @@ -868,6 +959,87 @@ where } } + /// Create two-to-one block aggregation circuit. + /// + /// # Arguments + /// + /// - `block_circuit`: circuit data for the block circuit, that constitutes + /// the base case for aggregation. + /// + /// # Outputs + /// + /// Returns a [`TwoToOneBlockCircuitData`]. + fn create_two_to_one_block_circuit( + block_circuit: &BlockCircuitData, + ) -> TwoToOneBlockCircuitData + where + F: RichField + Extendable, + C: GenericConfig, + C::Hasher: AlgebraicHasher, + { + let mut builder = CircuitBuilder::::new(block_circuit.circuit.common.config.clone()); + + let mix_hash = builder.add_virtual_hash_public_input(); + + // We need to pad by PIS to match the count of PIS of the `base_proof`. + let mut padding = block_circuit.circuit.common.num_public_inputs; + // The number of PIS that will be added *after* padding by + // [`add_verifier_data_public_inputs()`]. + padding -= verification_key_len(&block_circuit.circuit); + // Account for `mix_pv_hash`. + padding -= builder.num_public_inputs(); + + let zero = builder.zero(); + for _ in 0..padding { + builder.register_public_input(zero); + } + + let cyclic_vk = builder.add_verifier_data_public_inputs(); + + let lhs = Self::add_agg_child(&mut builder, &block_circuit.circuit); + let rhs = Self::add_agg_child(&mut builder, &block_circuit.circuit); + + let lhs_public_inputs = lhs.public_inputs(&mut builder); + let rhs_public_inputs = rhs.public_inputs(&mut builder); + + let lhs_public_values = extract_block_public_values(&lhs_public_inputs); + let rhs_public_values = extract_block_public_values(&rhs_public_inputs); + + let lhs_agg_pv_hash = extract_two_to_one_block_hash(&lhs_public_inputs); + let rhs_agg_pv_hash = extract_two_to_one_block_hash(&rhs_public_inputs); + + let lhs_base_pv_hash = builder + .hash_n_to_hash_no_pad::(lhs_public_values.to_vec()) + .elements; + let rhs_base_pv_hash = builder + .hash_n_to_hash_no_pad::(rhs_public_values.to_vec()) + .elements; + + let lhs_hash: Vec = zip_eq(lhs_agg_pv_hash, lhs_base_pv_hash) + .map(|(&agg_target, base_target)| builder.select(lhs.is_agg, agg_target, base_target)) + .collect(); + + let rhs_hash: Vec = zip_eq(rhs_agg_pv_hash, rhs_base_pv_hash) + .map(|(&agg_target, base_target)| builder.select(rhs.is_agg, agg_target, base_target)) + .collect(); + + let mut mix_vec = vec![]; + mix_vec.extend(&lhs_hash); + mix_vec.extend(&rhs_hash); + + let mix_hash_virtual = builder.hash_n_to_hash_no_pad::(mix_vec); + + builder.connect_hashes(mix_hash, mix_hash_virtual); + + let circuit = builder.build::(); + TwoToOneBlockCircuitData { + circuit, + lhs, + rhs, + cyclic_vk, + } + } + /// Connect the 256 block hashes between two blocks fn connect_block_hashes( builder: &mut CircuitBuilder, @@ -1218,11 +1390,11 @@ where agg_inputs.set_bool_target(self.aggregation.lhs.is_agg, lhs_is_agg); agg_inputs.set_proof_with_pis_target(&self.aggregation.lhs.agg_proof, lhs_proof); - agg_inputs.set_proof_with_pis_target(&self.aggregation.lhs.evm_proof, lhs_proof); + agg_inputs.set_proof_with_pis_target(&self.aggregation.lhs.base_proof, lhs_proof); agg_inputs.set_bool_target(self.aggregation.rhs.is_agg, rhs_is_agg); agg_inputs.set_proof_with_pis_target(&self.aggregation.rhs.agg_proof, rhs_proof); - agg_inputs.set_proof_with_pis_target(&self.aggregation.rhs.evm_proof, rhs_proof); + agg_inputs.set_proof_with_pis_target(&self.aggregation.rhs.base_proof, rhs_proof); agg_inputs.set_verifier_data_target( &self.aggregation.cyclic_vk, @@ -1440,8 +1612,134 @@ where &self.block.circuit.common, ) } -} + /// Aggregates two proofs in manner similar to [`prove_aggregation`]. + /// + /// # Arguments + /// + /// - `lhs`: a proof of either a block or previous aggregation. + /// - `lhs_is_agg`: specify which case `lhs` was. + /// - `rhs`: a proof of either a block or previous aggregation. + /// - `rhs_is_agg`: specify which case `rhs` was. + /// + /// # Outputs + /// + /// Returns a [`ProofWithPublicInputs`]. + pub fn prove_two_to_one_block( + &self, + lhs: &ProofWithPublicInputs, + lhs_is_agg: bool, + rhs: &ProofWithPublicInputs, + rhs_is_agg: bool, + ) -> anyhow::Result> { + let mut witness = PartialWitness::new(); + + Self::set_dummy_if_necessary( + &self.two_to_one_block.lhs, + lhs_is_agg, + &self.two_to_one_block.circuit, + &mut witness, + lhs, + ); + + Self::set_dummy_if_necessary( + &self.two_to_one_block.rhs, + rhs_is_agg, + &self.two_to_one_block.circuit, + &mut witness, + rhs, + ); + + witness.set_verifier_data_target( + &self.two_to_one_block.cyclic_vk, + &self.two_to_one_block.circuit.verifier_only, + ); + + let proof = self.two_to_one_block.circuit.prove(witness)?; + Ok(proof) + } + + /// Verifies an existing block aggregation proof. + /// + /// # Arguments + /// + /// - `proof`: The proof generated with `prove_two_to_one_block`. + /// + /// # Outputs + /// + /// Returns whether the proof was valid or not. + pub fn verify_two_to_one_block( + &self, + proof: &ProofWithPublicInputs, + ) -> anyhow::Result<()> { + self.two_to_one_block.circuit.verify(proof.clone())?; + let verifier_data = &self.two_to_one_block.circuit.verifier_data(); + check_cyclic_proof_verifier_data(proof, &verifier_data.verifier_only, &verifier_data.common) + } + + /// Creates dummy public inputs with correct verifier key at the end. Used + /// by [`set_dummy_if_necessary`]. It cyclic vk to the aggregation circuit + /// values, so that both aggregation and non-aggregation parts of the child + /// share the same vk. This is possible because only the aggregation inner + /// circuit is checked against its vk. + fn set_dummy_proof_with_cyclic_vk_pis( + circuit_agg: &CircuitData, + witness: &mut PartialWitness, + agg_proof_with_pis: &ProofWithPublicInputsTarget, + base_proof_with_pis: &ProofWithPublicInputs, + ) { + let ProofWithPublicInputs { + proof: base_proof, + public_inputs: _, + } = base_proof_with_pis; + let ProofWithPublicInputsTarget { + proof: agg_proof_targets, + public_inputs: agg_pi_targets, + } = agg_proof_with_pis; + + // The proof remains the same. + witness.set_proof_target(agg_proof_targets, base_proof); + + let cyclic_verifying_data = &circuit_agg.verifier_only; + let mut cyclic_vk = cyclic_verifying_data.circuit_digest.to_vec(); + cyclic_vk.append(&mut cyclic_verifying_data.constants_sigmas_cap.flatten()); + + let mut dummy_pis = vec![F::ZERO; circuit_agg.common.num_public_inputs - cyclic_vk.len()]; + dummy_pis.append(&mut cyclic_vk); + + // Set dummy public inputs. + for (&pi_t, pi) in agg_pi_targets.iter().zip_eq(dummy_pis) { + witness.set_target(pi_t, pi); + } + } + + /// If the [`AggregationChild`] is a base proof and not an aggregation + /// proof, we need to manually set the public inputs vector of the otherwise + /// inert `agg_proof`, so that they correspond to the `cyclic_vk` of the + /// aggregation circuit. The cyclic prover expects to find the `cyclic_vk` + /// targets in the very end of the public inputs vector, and so it does not + /// matter what the preceding values are. + fn set_dummy_if_necessary( + agg_child: &AggregationChildTarget, + is_agg: bool, + circuit: &CircuitData, + agg_inputs: &mut PartialWitness, + proof: &ProofWithPublicInputs, + ) { + agg_inputs.set_bool_target(agg_child.is_agg, is_agg); + if is_agg { + agg_inputs.set_proof_with_pis_target(&agg_child.agg_proof, proof); + } else { + Self::set_dummy_proof_with_cyclic_vk_pis( + circuit, + agg_inputs, + &agg_child.agg_proof, + proof, + ); + } + agg_inputs.set_proof_with_pis_target(&agg_child.base_proof, proof); + } +} /// A map between initial degree sizes and their associated shrinking recursion /// circuits. #[derive(Eq, PartialEq, Debug)] @@ -1698,3 +1996,59 @@ fn shrinking_config() -> CircuitConfig { ..CircuitConfig::standard_recursion_config() } } + +/// Extracts the two-to-one block aggregation hash from a public inputs slice. +/// +/// # Arguments +/// +/// - `public_inputs`: A slice of public inputs originating from the aggregation +/// case of a two-to-one block proof. This slice must consist of a hash, +/// either of public values, or of two concatenated hashes. The hash must +/// start at offset zero of the slice and is typically followed by padding and +/// then a verifier key. It is an error to call this on a slice for a base +/// proof. +/// +/// # Outputs +/// +/// - A slice containing exactly the hash. +pub fn extract_two_to_one_block_hash(public_inputs: &[T]) -> &[T; NUM_HASH_OUT_ELTS] { + const PV_HASH_INDEX_START: usize = 0; + const PV_HASH_INDEX_END: usize = PV_HASH_INDEX_START + NUM_HASH_OUT_ELTS; + public_inputs[PV_HASH_INDEX_START..PV_HASH_INDEX_END] + .try_into() + .expect("Public inputs vector was malformed.") +} + +/// Extracts the two-to-one block aggregation public values of the block from +/// a public inputs slice. +/// +/// # Arguments +/// +/// - `public_inputs`: A slice of public inputs originating from the base case +/// of a two-to-one block proof. This slice must consist exactly of public +/// values starting at offset zero and is typically followed by a verifier +/// key. It is an error to call this function on a slice for an aggregation +/// proof. +/// +/// # Outputs +/// +/// - A slice containing exactly the public values. +pub fn extract_block_public_values(public_inputs: &[T]) -> &[T; PublicValuesTarget::SIZE] { + const PV_INDEX_START: usize = 0; + const PV_INDEX_END: usize = PV_INDEX_START + PublicValuesTarget::SIZE; + public_inputs[PV_INDEX_START..PV_INDEX_END] + .try_into() + .expect("Public inputs vector was malformed.") +} + +/// Computes the length added to the public inputs vector by +/// [`CircuitBuilder::add_verifier_data_public_inputs`]. +pub const fn verification_key_len(circuit: &CircuitData) -> usize +where + F: RichField + Extendable, + C: GenericConfig, + C::Hasher: AlgebraicHasher, +{ + circuit.verifier_only.circuit_digest.elements.len() + + (1 << circuit.common.config.fri_config.cap_height) * NUM_HASH_OUT_ELTS +} diff --git a/evm_arithmetization/src/proof.rs b/evm_arithmetization/src/proof.rs index 338eda2fa..380bd318a 100644 --- a/evm_arithmetization/src/proof.rs +++ b/evm_arithmetization/src/proof.rs @@ -59,14 +59,7 @@ impl PublicValues { /// Public values are always the first public inputs added to the circuit, /// so we can start extracting at index 0. pub fn from_public_inputs(pis: &[F]) -> Self { - assert!( - pis.len() - > TrieRootsTarget::SIZE * 2 - + BlockMetadataTarget::SIZE - + BlockHashesTarget::SIZE - + ExtraBlockDataTarget::SIZE - - 1 - ); + assert!(PublicValuesTarget::SIZE <= pis.len()); let trie_roots_before = TrieRoots::from_public_inputs(&pis[0..TrieRootsTarget::SIZE]); let trie_roots_after = @@ -279,6 +272,10 @@ pub struct PublicValuesTarget { } impl PublicValuesTarget { + pub(crate) const SIZE: usize = TrieRootsTarget::SIZE * 2 + + BlockMetadataTarget::SIZE + + BlockHashesTarget::SIZE + + ExtraBlockDataTarget::SIZE; /// Serializes public value targets. pub(crate) fn to_buffer(&self, buffer: &mut Vec) -> IoResult<()> { let TrieRootsTarget { @@ -758,7 +755,7 @@ pub struct ExtraBlockDataTarget { impl ExtraBlockDataTarget { /// Number of `Target`s required for the extra block data. - const SIZE: usize = 12; + pub(crate) const SIZE: usize = 12; /// Extracts the extra block data `Target`s from the public input `Target`s. /// The provided `pis` should start with the extra vblock data. diff --git a/evm_arithmetization/tests/two_to_one_block.rs b/evm_arithmetization/tests/two_to_one_block.rs new file mode 100644 index 000000000..fe387cee2 --- /dev/null +++ b/evm_arithmetization/tests/two_to_one_block.rs @@ -0,0 +1,359 @@ +use std::collections::HashMap; +use std::str::FromStr; + +use env_logger::{try_init_from_env, Env, DEFAULT_FILTER_ENV}; +use ethereum_types::{Address, BigEndianHash, H256, U256}; +use evm_arithmetization::fixed_recursive_verifier::{ + extract_block_public_values, extract_two_to_one_block_hash, +}; +use evm_arithmetization::generation::mpt::{AccountRlp, LegacyReceiptRlp}; +use evm_arithmetization::generation::{GenerationInputs, TrieInputs}; +use evm_arithmetization::proof::{BlockMetadata, PublicValues, TrieRoots}; +use evm_arithmetization::{AllRecursiveCircuits, AllStark, Node, StarkConfig}; +use hex_literal::hex; +use keccak_hash::keccak; +use mpt_trie::nibbles::Nibbles; +use mpt_trie::partial_trie::{HashedPartialTrie, PartialTrie}; +use plonky2::field::goldilocks_field::GoldilocksField; +use plonky2::hash::poseidon::PoseidonHash; +use plonky2::plonk::config::{Hasher, PoseidonGoldilocksConfig}; +use plonky2::plonk::proof::ProofWithPublicInputs; +use plonky2::util::timing::TimingTree; + +type F = GoldilocksField; +const D: usize = 2; +type C = PoseidonGoldilocksConfig; + +fn eth_to_wei(eth: U256) -> U256 { + // 1 ether = 10^18 wei. + eth * U256::from(10).pow(18.into()) +} + +fn init_logger() { + let _ = try_init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); +} + +/// Get `GenerationInputs` for a simple token transfer txn, where the block has +/// the given timestamp. +fn empty_transfer(timestamp: u64) -> anyhow::Result { + init_logger(); + + let beneficiary = hex!("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + let sender = hex!("2c7536e3605d9c16a7a3d7b1898e529396a65c23"); + let to = hex!("a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0"); + + let sender_state_key = keccak(sender); + let to_state_key = keccak(to); + + let sender_nibbles = Nibbles::from_bytes_be(sender_state_key.as_bytes()).unwrap(); + let to_nibbles = Nibbles::from_bytes_be(to_state_key.as_bytes()).unwrap(); + + let sender_account_before = AccountRlp { + nonce: 5.into(), + balance: eth_to_wei(100_000.into()), + storage_root: HashedPartialTrie::from(Node::Empty).hash(), + code_hash: keccak([]), + }; + let to_account_before = AccountRlp::default(); + + let state_trie_before: HashedPartialTrie = Node::Leaf { + nibbles: sender_nibbles, + value: rlp::encode(&sender_account_before).to_vec(), + } + .into(); + let checkpoint_state_trie_root = state_trie_before.hash(); + assert_eq!( + checkpoint_state_trie_root, + hex!("ef46022eafbc33d70e6ea9c6aef1074c1ff7ad36417ffbc64307ad3a8c274b75").into() + ); + + let tries_before = TrieInputs { + state_trie: HashedPartialTrie::from(Node::Empty), + transactions_trie: HashedPartialTrie::from(Node::Empty), + receipts_trie: HashedPartialTrie::from(Node::Empty), + storage_tries: vec![], + }; + + // Generated using a little py-evm script. + let txn = hex!("f861050a8255f094a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0648242421ba02c89eb757d9deeb1f5b3859a9d4d679951ef610ac47ad4608dc142beb1b7e313a05af7e9fbab825455d36c36c7f4cfcafbeafa9a77bdff936b52afb36d4fe4bcdd"); + let value = U256::from(100u32); + + let block_metadata = BlockMetadata { + block_beneficiary: Address::from(beneficiary), + block_timestamp: timestamp.into(), + block_number: 1.into(), + block_difficulty: 0x020000.into(), + block_random: H256::from_uint(&0x020000.into()), + block_gaslimit: 0xff112233u32.into(), + block_chain_id: 1.into(), + block_base_fee: 0xa.into(), + block_gas_used: 0.into(), + block_bloom: [0.into(); 8], + }; + + let contract_code = HashMap::new(); + + let expected_state_trie_after: HashedPartialTrie = { + let txdata_gas = 2 * 16; + let gas_used = 21_000 + txdata_gas; + + let sender_account_after = AccountRlp { + balance: sender_account_before.balance - value - gas_used * 10, + nonce: sender_account_before.nonce + 1, + ..sender_account_before + }; + let to_account_after = AccountRlp { + balance: value, + ..to_account_before + }; + + let mut children = core::array::from_fn(|_| Node::Empty.into()); + children[sender_nibbles.get_nibble(0) as usize] = Node::Leaf { + nibbles: sender_nibbles.truncate_n_nibbles_front(1), + value: rlp::encode(&sender_account_after).to_vec(), + } + .into(); + children[to_nibbles.get_nibble(0) as usize] = Node::Leaf { + nibbles: to_nibbles.truncate_n_nibbles_front(1), + value: rlp::encode(&to_account_after).to_vec(), + } + .into(); + Node::Branch { + children, + value: vec![], + } + .into() + }; + + let receipt_0 = LegacyReceiptRlp { + status: true, + cum_gas_used: 21032.into(), + bloom: vec![0; 256].into(), + logs: vec![], + }; + let mut receipts_trie = HashedPartialTrie::from(Node::Empty); + receipts_trie.insert( + Nibbles::from_str("0x80").unwrap(), + rlp::encode(&receipt_0).to_vec(), + )?; + let transactions_trie: HashedPartialTrie = Node::Leaf { + nibbles: Nibbles::from_str("0x80").unwrap(), + value: txn.to_vec(), + } + .into(); + + let _trie_roots_after = TrieRoots { + state_root: expected_state_trie_after.hash(), + transactions_root: transactions_trie.hash(), + receipts_root: receipts_trie.hash(), + }; + + let trie_roots_after = TrieRoots { + state_root: tries_before.state_trie.hash(), + transactions_root: tries_before.transactions_trie.hash(), + receipts_root: tries_before.receipts_trie.hash(), + }; + let inputs = GenerationInputs { + tries: tries_before.clone(), + trie_roots_after, + contract_code, + checkpoint_state_trie_root: tries_before.state_trie.hash(), + block_metadata, + ..Default::default() + }; + + Ok(inputs) +} + +fn get_test_block_proof( + timestamp: u64, + all_circuits: &AllRecursiveCircuits, + all_stark: &AllStark, + config: &StarkConfig, +) -> anyhow::Result> { + let inputs0 = empty_transfer(timestamp)?; + let inputs = inputs0.clone(); + let dummy0 = GenerationInputs { + txn_number_before: inputs.txn_number_before, + gas_used_before: inputs.gas_used_after, + gas_used_after: inputs.gas_used_after, + signed_txn: None, + withdrawals: vec![], + tries: TrieInputs { + state_trie: HashedPartialTrie::from(Node::Hash(inputs.trie_roots_after.state_root)), + transactions_trie: HashedPartialTrie::from(Node::Hash( + inputs.trie_roots_after.transactions_root, + )), + receipts_trie: HashedPartialTrie::from(Node::Hash( + inputs.trie_roots_after.receipts_root, + )), + storage_tries: vec![], + }, + trie_roots_after: inputs.trie_roots_after, + checkpoint_state_trie_root: inputs.checkpoint_state_trie_root, + contract_code: Default::default(), + block_metadata: inputs.block_metadata.clone(), + block_hashes: inputs.block_hashes.clone(), + }; + + let timing = &mut TimingTree::new(&format!("Blockproof {timestamp}"), log::Level::Info); + let (root_proof0, pv0) = all_circuits.prove_root(all_stark, config, inputs0, timing, None)?; + all_circuits.verify_root(root_proof0.clone())?; + let (dummy_proof0, dummy_pv0) = + all_circuits.prove_root(all_stark, config, dummy0, timing, None)?; + all_circuits.verify_root(dummy_proof0.clone())?; + + let (agg_proof0, pv0) = all_circuits.prove_aggregation( + false, + &root_proof0, + pv0, + false, + &dummy_proof0, + dummy_pv0, + )?; + + all_circuits.verify_aggregation(&agg_proof0)?; + + // Test retrieved public values from the proof public inputs. + let retrieved_public_values0 = PublicValues::from_public_inputs(&agg_proof0.public_inputs); + assert_eq!(retrieved_public_values0, pv0); + assert_eq!( + pv0.trie_roots_before.state_root, + pv0.extra_block_data.checkpoint_state_trie_root + ); + + let (block_proof0, block_public_values) = all_circuits.prove_block( + None, // We don't specify a previous proof, considering block 1 as the new checkpoint. + &agg_proof0, + pv0.clone(), + )?; + + let pv_block = PublicValues::from_public_inputs(&block_proof0.public_inputs); + assert_eq!(block_public_values, pv_block); + + Ok(block_proof0) +} + +#[ignore] +#[test] +fn test_two_to_one_block_aggregation() -> anyhow::Result<()> { + init_logger(); + let some_timestamps = [127, 42, 65, 43]; + + let all_stark = AllStark::::default(); + let config = StarkConfig::standard_fast_config(); + let all_circuits = AllRecursiveCircuits::::new( + &all_stark, + &[16..17, 9..15, 12..18, 14..15, 9..10, 12..13, 17..20], + &config, + ); + + let unrelated_block_proofs = some_timestamps + .iter() + .map(|&ts| get_test_block_proof(ts, &all_circuits, &all_stark, &config)) + .collect::>>>()?; + + unrelated_block_proofs + .iter() + .try_for_each(|bp| all_circuits.verify_block(bp))?; + + let bp = unrelated_block_proofs; + + { + // Aggregate the same proof twice + let aggproof_42_42 = all_circuits.prove_two_to_one_block(&bp[0], false, &bp[0], false)?; + all_circuits.verify_two_to_one_block(&aggproof_42_42)?; + } + + { + // Binary tree reduction + // + // A B C D Blockproofs (base case) + // \ / \ / + // (A, B) (C, D) Two-to-one block aggregation proofs + // \ / + // ((A,B), (C,D)) Two-to-one block aggregation proofs + + let aggproof01 = all_circuits.prove_two_to_one_block(&bp[0], false, &bp[1], false)?; + all_circuits.verify_two_to_one_block(&aggproof01)?; + + let aggproof23 = all_circuits.prove_two_to_one_block(&bp[2], false, &bp[3], false)?; + all_circuits.verify_two_to_one_block(&aggproof23)?; + + let aggproof0123 = + all_circuits.prove_two_to_one_block(&aggproof01, true, &aggproof23, true)?; + all_circuits.verify_two_to_one_block(&aggproof0123)?; + + { + // Compute Merkle root from public inputs of block proofs. + // Leaves + let mut hashes: Vec<_> = bp + .iter() + .map(|block_proof| { + let public_values = extract_block_public_values(&block_proof.public_inputs); + PoseidonHash::hash_no_pad(public_values) + }) + .collect(); + + // Inner nodes + hashes.extend_from_within(0..hashes.len()); + let half = hashes.len() / 2; + for i in 0..half - 1 { + hashes[half + i] = PoseidonHash::two_to_one(hashes[2 * i], hashes[2 * i + 1]); + } + let merkle_root = hashes[hashes.len() - 2].elements; + + assert_eq!( + extract_two_to_one_block_hash(&aggproof0123.public_inputs), + &merkle_root, + "Merkle root of verifier's verification tree did not match merkle root in public inputs." + ); + } + } + + { + // Foldleft + // + // A B C D Blockproofs (base case) + // \ / / / + // (A, B) / / Two-to-one block aggregation proofs + // \ / / + // ((A,B), C) / Two-to-one block aggregation proofs + // \ / + // (((A,B),C),D) Two-to-one block aggregation proofs + + let aggproof01 = all_circuits.prove_two_to_one_block(&bp[0], false, &bp[1], false)?; + all_circuits.verify_two_to_one_block(&aggproof01)?; + + let aggproof012 = all_circuits.prove_two_to_one_block(&aggproof01, true, &bp[2], false)?; + all_circuits.verify_two_to_one_block(&aggproof012)?; + + let aggproof0123 = + all_circuits.prove_two_to_one_block(&aggproof012, true, &bp[3], false)?; + all_circuits.verify_two_to_one_block(&aggproof0123)?; + } + + { + // Foldright + // + // A B C D Blockproofs (base case) + // \ \ \ / + // \ \ (C,D) Two-to-one block aggregation proofs + // \ \ / + // \ (B,(C, D)) Two-to-one block aggregation proofs + // \ / + // (A,(B,(C,D))) Two-to-one block aggregation proofs + + let aggproof23 = all_circuits.prove_two_to_one_block(&bp[2], false, &bp[3], false)?; + all_circuits.verify_two_to_one_block(&aggproof23)?; + + let aggproof123 = all_circuits.prove_two_to_one_block(&bp[1], false, &aggproof23, true)?; + all_circuits.verify_two_to_one_block(&aggproof123)?; + + let aggproof0123 = + all_circuits.prove_two_to_one_block(&bp[0], false, &aggproof123, true)?; + all_circuits.verify_two_to_one_block(&aggproof0123)?; + } + + Ok(()) +}