diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1c5efc9..5a82da88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,53 +2,64 @@ name: Tests on: push: - branches: ["main"] + branches: ["main", "develop"] pull_request: - branches: ["main"] + branches: ["main", "develop", "release*", "release/*", "rc*", "*-audit*", "v0.13", "v0.12"] env: CARGO_TERM_COLOR: always - INFURA_ID: ${{ vars.INFURA_ID }} - JSON_RPC_URL: ${{ vars.JSON_RPC_URL }} jobs: build: - runs-on: ubuntu-latest-m + runs-on: ubuntu-latest-64core-256ram steps: - uses: actions/checkout@v3 + - name: Build - run: cargo build --verbose + run: | + export AXIOM_SKIP_CONSTANT_GEN=1 + cargo build --verbose + - name: Run axiom-eth tests MockProver + working-directory: "axiom-eth" + run: | + export ALCHEMY_KEY=${{ secrets.ALCHEMY_KEY}} + export JSON_RPC_URL=${{ secrets.JSON_RPC_URL }} + cargo t test_keccak + cargo t rlc::tests + cargo t rlp::tests + cargo t keccak::tests + cargo t block_header::tests + cargo t mpt + cargo t storage::tests::test_mock + cargo t transaction::tests + BLOCK_NUM=17000000 cargo test receipt::tests + cargo t solidity::tests::mapping::test_mock + cargo t solidity::tests::nested_mappings::test_mock + cargo t solidity::tests::mapping_storage::test_mock + + - name: Run axiom-eth tests real prover + working-directory: "axiom-eth" + run: | + export ALCHEMY_KEY=${{ secrets.ALCHEMY_KEY }} + export JSON_RPC_URL=${{ secrets.JSON_RPC_URL }} + cargo t --release -- test_one_mainnet_header_prover + cargo t --release -- bench_mpt_inclusion_fixed --ignored + cargo t --release utils::component:: + + - name: Run axiom-core tests + working-directory: "axiom-core" + run: | + export ALCHEMY_KEY=${{ secrets.ALCHEMY_KEY }} + export JSON_RPC_URL=${{ secrets.JSON_RPC_URL }} + cargo t + + - name: Run axiom-query tests + working-directory: "axiom-query" run: | - export INFURA_ID=${{ vars.INFURA_ID }} - export JSON_RPC_URL=${{ vars.JSON_RPC_URL }} - cd axiom-eth - cargo test -- test_mock_account_queries_empty - cargo test -- test_mock_account_queries_genesis - cargo test -- test_mock_account_queries_simple - cargo test -- test_genesis_block - cargo test -- test_mock_block_queries_random - cargo test -- test_mock_row_consistency_nouns - cargo test -- test_default_storage_query - cargo test -- test_mock_storage_queries_empty - cargo test -- test_mock_storage_queries_mapping - cargo test -- test_mock_storage_queries_slot0 - cargo test -- test_mock_storage_queries_uni_v3 - cargo test -- test_multi_goerli_header_mock - cargo test -- test_one_mainnet_header_before_london_mock - cargo test -- test_one_mainnet_header_mock - cargo test -- test_one_mainnet_header_withdrawals_mock - cargo test -- test_keccak - cargo test -- test_mock_mpt_inclusion_fixed - cargo test -- test_mpt_empty_root - cargo test -- test_mpt_noninclusion_branch_fixed - cargo test -- test_mpt_noninclusion_extension_fixed - cargo test -- test_mock_rlc - cargo test -- test_mock_rlp_array - cargo test -- test_mock_rlp_field - cargo test -- test_mock_rlp_literal - cargo test -- test_mock_rlp_long_field - cargo test -- test_mock_rlp_long_long_field - cargo test -- test_mock_rlp_short_field - cargo test -- test_mock_single_eip1186 + export AXIOM_SKIP_CONSTANT_GEN=1 + export ALCHEMY_KEY=${{ secrets.ALCHEMY_KEY }} + export JSON_RPC_URL=${{ secrets.JSON_RPC_URL }} + mkdir -p data/test + cargo t --no-default-features --features "halo2-axiom, jemallocator" diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml new file mode 100644 index 00000000..8172c2ea --- /dev/null +++ b/.github/workflows/lints.yml @@ -0,0 +1,34 @@ +name: Lints + +on: + push: + branches: ["main", "develop"] + pull_request: + branches: ["main", "develop"] + +jobs: + lint: + name: Lint + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + override: false + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v1 + with: + cache-on-failure: true + + - name: Run fmt + run: | + export AXIOM_SKIP_CONSTANT_GEN=1 + cargo fmt --all -- --check + + - name: Run clippy + run: cargo clippy --all -- -D warnings diff --git a/.gitignore b/.gitignore index 65983083..05396974 100644 --- a/.gitignore +++ b/.gitignore @@ -8,10 +8,17 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk -======= -/target +# ======= +target/ +params/ +!params/integration_test *.png /halo2_ecc/src/bn254/data/ /halo2_ecc/src/secp256k1/data/ +**/.env + +*.pk +*.snark +.DS_Store \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 783c4f72..cb2d4ea9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,10 @@ [workspace] -members = [ - "axiom-eth", - "any_circuit_derive" -] +members = ["axiom-eth", "axiom-core", "axiom-query", "axiom-codec"] +resolver = "2" [profile.dev] opt-level = 3 -debug = 1 # change to 0 or 2 for more or less debug info +debug = 2 # change to 0 or 2 for more or less debug info overflow-checks = true incremental = true @@ -16,7 +14,7 @@ inherits = "dev" opt-level = 3 # Set this to 1 or 2 to get more useful backtraces debug = 1 -debug-assertions = true +debug-assertions = true panic = 'unwind' # better recompile times incremental = true @@ -27,9 +25,9 @@ codegen-units = 16 opt-level = 3 debug = false debug-assertions = false -lto = "fat" +lto = "fat" # `codegen-units = 1` can lead to WORSE performance - always bench to find best profile for your machine! -# codegen-units = 1 +codegen-units = 1 panic = "abort" incremental = false diff --git a/README.md b/README.md index b347a4af..53dd5d33 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ -# axiom-eth +# Axiom ZK Circuits -This repository builds on [halo2-lib](https://github.com/axiom-crypto/halo2-lib/releases/tag/v0.3.0) and [snark-verifier](https://github.com/axiom-crypto/snark-verifier/tree/v0.1.1) to create a library of zero-knowledge proof circuits that [Axiom](https://axiom.xyz) uses to read data from the Ethereum virtual machine (EVM). +This repository builds on [halo2-lib](https://github.com/axiom-crypto/halo2-lib/tree/main) and [snark-verifier](https://github.com/axiom-crypto/snark-verifier/) to create a library of zero-knowledge proof circuits that [Axiom](https://axiom.xyz) uses to read data from the Ethereum virtual machine (EVM). + +This monorepo consists of multiple crates: + +## `axiom-eth` + +This is the main library of chips and frameworks for building ZK circuits that prove data from the Ethereum virtual machine (EVM). For more details, see the [README](./axiom-eth/README.md). + +## `axiom-core` + +This contains the ZK circuits that generate proofs for the `AxiomV2Core` smart contract. These circuits read the RLP encoded block headers for a chain of blocks and verify that the block headers form a chain. They output a Merkle Mountain Range of the block hashes of the chain. This crate also contains aggregation circuits to aggregate multiple circuits for the purpose of proving longer chains. For more details, see the [README](./axiom-core/README.md). + +## `axiom-query` + +This contains the ZK circuits that generate proofs for the `AxiomV2Query` smart contract. For more details, see the [README](./axiom-query/README.md). + +### `axiom-codec` + +This crate does not contain any ZK circuits, but it contains Rust types for Axiom queries and specifies how to encode/decode them to field elements for in-circuit use. diff --git a/any_circuit_derive/Cargo.toml b/any_circuit_derive/Cargo.toml deleted file mode 100644 index bde6e6b0..00000000 --- a/any_circuit_derive/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "any_circuit_derive" -version = "0.1.0" -edition = "2021" - -[lib] -proc-macro = true - -[dependencies] -syn = "2.0.15" -quote = "1.0.26" -proc-macro2 = "1.0.56" diff --git a/any_circuit_derive/src/lib.rs b/any_circuit_derive/src/lib.rs deleted file mode 100644 index 7d53d464..00000000 --- a/any_circuit_derive/src/lib.rs +++ /dev/null @@ -1,100 +0,0 @@ -extern crate proc_macro; - -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, Data, DeriveInput}; - -#[proc_macro_derive(AnyCircuit)] -pub fn any_circuit_derive(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let name = &input.ident; - let generics = &input.generics; - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - - let variants = match input.data { - Data::Enum(data_enum) => data_enum.variants, - _ => panic!("AnyCircuit can only be derived for enums"), - }; - - let read_or_create_pk_arms = variants.iter().map(|variant| { - let ident = &variant.ident; - quote! { - Self::#ident(pre_circuit) => pre_circuit.read_or_create_pk(params, pk_path, pinning_path, read_only) - } - }); - - let gen_snark_shplonk_arms = variants.iter().map(|variant| { - let ident = &variant.ident; - quote! { - Self::#ident(pre_circuit) => pre_circuit.gen_snark_shplonk(params, pk, pinning_path, path) - } - }); - - let gen_evm_verifier_shplonk_arms = variants.iter().map(|variant| { - let ident = &variant.ident; - quote! { - Self::#ident(pre_circuit) => pre_circuit.gen_evm_verifier_shplonk(params, pk, yul_path) - } - }); - - let gen_calldata_arms = variants.iter().map(|variant| { - let ident = &variant.ident; - quote! { - Self::#ident(pre_circuit) => pre_circuit.gen_calldata(params, pk, pinning_path, path, deployment_code) - } - }); - - let expanded = quote! { - impl #impl_generics AnyCircuit for #name #ty_generics #where_clause { - fn read_or_create_pk( - self, - params: &ParamsKZG, - pk_path: impl AsRef, - pinning_path: impl AsRef, - read_only: bool, - ) -> ProvingKey { - match self { - #(#read_or_create_pk_arms,)* - } - } - - fn gen_snark_shplonk( - self, - params: &ParamsKZG, - pk: &ProvingKey, - pinning_path: impl AsRef, - path: Option>, - ) -> Snark { - match self { - #(#gen_snark_shplonk_arms,)* - } - } - - fn gen_evm_verifier_shplonk( - self, - params: &ParamsKZG, - pk: &ProvingKey, - yul_path: impl AsRef, - ) -> Vec { - match self { - #(#gen_evm_verifier_shplonk_arms,)* - } - } - - fn gen_calldata( - self, - params: &ParamsKZG, - pk: &ProvingKey, - pinning_path: impl AsRef, - path: impl AsRef, - deployment_code: Option>, - ) -> String { - match self { - #(#gen_calldata_arms,)* - } - } - } - }; - - TokenStream::from(expanded) -} diff --git a/axiom-codec/Cargo.toml b/axiom-codec/Cargo.toml new file mode 100644 index 00000000..60a9d733 --- /dev/null +++ b/axiom-codec/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "axiom-codec" +version = "0.2.0" +authors = ["Intrinsic Technologies"] +license = "MIT" +edition = "2021" +repository = "https://github.com/axiom-crypto/axiom-eth" +readme = "README.md" +description = "This crate contains Rust types for Axiom queries and specifies how to encode/decode them to field elements for in-circuit use." +rust-version = "1.73.0" + +[dependencies] +byteorder = { version = "1.4.3" } +serde = { version = "1.0", default-features = false, features = ["derive"] } +serde_json = { version = "1.0", default-features = false } +serde_repr = "0.1" +base64 = { version = "0.21", optional = true } +serde_with = { version = "2.2", optional = true } +anyhow = "1.0" + +# halo2, features turned on by axiom-eth +axiom-eth = { version = "=0.4.0", path = "../axiom-eth", default-features = false } + +# ethereum +ethers-core = { version = "2.0.10" } + +[dev-dependencies] +hex = "0.4.3" + +[build-dependencies] +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } + +[features] +default = ["halo2-axiom"] +# halo2-pse = ["axiom-eth/halo2-pse"] +halo2-axiom = ["axiom-eth/halo2-axiom"] diff --git a/axiom-codec/src/constants.rs b/axiom-codec/src/constants.rs new file mode 100644 index 00000000..dc16a89f --- /dev/null +++ b/axiom-codec/src/constants.rs @@ -0,0 +1,20 @@ +pub const ENCODED_K_BYTES: usize = 1; +pub const ENCODED_VKEY_LENGTH_BYTES: usize = 1; +pub const FIELD_IDX_BITS: usize = 32; +pub const FIELD_IDX_BYTES: usize = 4; +pub const MAX_SOLIDITY_MAPPING_KEYS: usize = 4; +pub const MAX_SUBQUERY_INPUTS: usize = 13; +pub const MAX_SUBQUERY_OUTPUTS: usize = 2; +pub const NUM_SUBQUERY_TYPES: usize = 7; +pub const SOURCE_CHAIN_ID_BYTES: usize = 8; +pub const SUBQUERY_TYPE_BYTES: usize = 2; +pub const USER_ADVICE_COLS: usize = 4; +pub const USER_FIXED_COLS: usize = 1; +pub const USER_INSTANCE_COLS: usize = 1; +pub const USER_LOOKUP_ADVICE_COLS: usize = 1; +pub const USER_MAX_OUTPUTS: usize = 128; +pub const USER_MAX_SUBQUERIES: usize = 128; +pub const USER_PROOF_LEN_BYTES: usize = 4; +pub const USER_RESULT_BYTES: usize = 32; +pub const USER_RESULT_FIELD_ELEMENTS: usize = 2; +pub const USER_RESULT_LEN_BYTES: usize = 2; diff --git a/axiom-codec/src/decoder/field_elements.rs b/axiom-codec/src/decoder/field_elements.rs new file mode 100644 index 00000000..e1853dc1 --- /dev/null +++ b/axiom-codec/src/decoder/field_elements.rs @@ -0,0 +1,120 @@ +use std::io::{Error, ErrorKind, Result}; + +use crate::{ + constants::MAX_SOLIDITY_MAPPING_KEYS, + encoder::field_elements::{ + NUM_FE_SOLIDITY_NESTED_MAPPING, NUM_FE_SOLIDITY_NESTED_MAPPING_WITHOUT_KEYS, + }, + types::field_elements::{ + FieldAccountSubquery, FieldHeaderSubquery, FieldReceiptSubquery, + FieldSolidityNestedMappingSubquery, FieldStorageSubquery, FieldTxSubquery, + FlattenedSubqueryResult, SubqueryKey, SubqueryOutput, SUBQUERY_KEY_LEN, + SUBQUERY_RESULT_LEN, + }, + HiLo, +}; + +impl TryFrom> for FlattenedSubqueryResult { + type Error = Error; + + fn try_from(value: Vec) -> Result { + if value.len() != SUBQUERY_RESULT_LEN { + return Err(Error::new(ErrorKind::InvalidInput, "invalid array length")); + } + let mut key = value; + let value = key.split_off(SUBQUERY_KEY_LEN); + let key = key.try_into().map_err(|_| Error::other("should never happen"))?; + let value = value.try_into().map_err(|_| Error::other("should never happen"))?; + Ok(Self { key: SubqueryKey(key), value: SubqueryOutput(value) }) + } +} + +impl TryFrom> for FieldHeaderSubquery { + type Error = Error; + + fn try_from(value: Vec) -> Result { + let [block_number, field_idx] = value + .try_into() + .map_err(|_| Error::new(ErrorKind::InvalidInput, "invalid array length"))?; + Ok(Self { block_number, field_idx }) + } +} + +impl TryFrom> for FieldAccountSubquery { + type Error = Error; + + fn try_from(value: Vec) -> Result { + let [block_number, addr, field_idx] = value + .try_into() + .map_err(|_| Error::new(ErrorKind::InvalidInput, "invalid array length"))?; + Ok(Self { block_number, addr, field_idx }) + } +} + +impl TryFrom> for FieldStorageSubquery { + type Error = Error; + + fn try_from(value: Vec) -> Result { + let [block_number, addr, slot_hi, slot_lo] = value + .try_into() + .map_err(|_| Error::new(ErrorKind::InvalidInput, "invalid array length"))?; + Ok(Self { block_number, addr, slot: HiLo::from_hi_lo([slot_hi, slot_lo]) }) + } +} + +impl TryFrom> for FieldTxSubquery { + type Error = Error; + + fn try_from(value: Vec) -> Result { + let [block_number, tx_idx, field_or_calldata_idx] = value + .try_into() + .map_err(|_| Error::new(ErrorKind::InvalidInput, "invalid array length"))?; + Ok(Self { block_number, tx_idx, field_or_calldata_idx }) + } +} + +impl TryFrom> for FieldReceiptSubquery { + type Error = Error; + + fn try_from(value: Vec) -> Result { + let [block_number, tx_idx, field_or_log_idx, topic_or_data_or_address_idx, event_schema_hi, event_schema_lo] = + value + .try_into() + .map_err(|_| Error::new(ErrorKind::InvalidInput, "invalid array length"))?; + Ok(Self { + block_number, + tx_idx, + field_or_log_idx, + topic_or_data_or_address_idx, + event_schema: HiLo::from_hi_lo([event_schema_hi, event_schema_lo]), + }) + } +} + +impl TryFrom> for FieldSolidityNestedMappingSubquery { + type Error = Error; + + fn try_from(mut value: Vec) -> Result { + if value.len() != NUM_FE_SOLIDITY_NESTED_MAPPING { + return Err(Error::new(ErrorKind::InvalidInput, "invalid array length")); + } + let keys = value.split_off(NUM_FE_SOLIDITY_NESTED_MAPPING_WITHOUT_KEYS); + let [block_number, addr, slot_hi, slot_lo, mapping_depth] = + value.try_into().map_err(|_| Error::other("should never happen"))?; + let mut keys_iter = keys.into_iter(); + let keys: Vec<_> = (0..MAX_SOLIDITY_MAPPING_KEYS) + .map(|_| { + let key_hi = keys_iter.next().unwrap(); + let key_lo = keys_iter.next().unwrap(); + HiLo::from_hi_lo([key_hi, key_lo]) + }) + .collect(); + Ok(Self { + block_number, + addr, + mapping_slot: HiLo::from_hi_lo([slot_hi, slot_lo]), + mapping_depth, + keys: keys.try_into().map_err(|_| Error::other("max keys wrong length"))?, + }) + } +} diff --git a/axiom-codec/src/decoder/mod.rs b/axiom-codec/src/decoder/mod.rs new file mode 100644 index 00000000..3e24b446 --- /dev/null +++ b/axiom-codec/src/decoder/mod.rs @@ -0,0 +1,2 @@ +pub mod field_elements; +pub mod native; diff --git a/axiom-codec/src/decoder/native.rs b/axiom-codec/src/decoder/native.rs new file mode 100644 index 00000000..09759188 --- /dev/null +++ b/axiom-codec/src/decoder/native.rs @@ -0,0 +1,151 @@ +use std::io::{self, Read, Result}; + +use axiom_eth::halo2curves::bn256::G1Affine; +use byteorder::{BigEndian, ReadBytesExt}; +use ethers_core::types::Bytes; + +use crate::{ + constants::MAX_SOLIDITY_MAPPING_KEYS, + types::native::{ + AccountSubquery, AnySubquery, AxiomV2ComputeSnark, HeaderSubquery, ReceiptSubquery, + SolidityNestedMappingSubquery, StorageSubquery, Subquery, SubqueryType, TxSubquery, + }, + utils::reader::{read_address, read_curve_compressed, read_h256, read_u256}, +}; + +impl TryFrom for AnySubquery { + type Error = io::Error; + + fn try_from(subquery: Subquery) -> Result { + let mut reader = &subquery.encoded_subquery_data[..]; + let subquery_type = subquery.subquery_type; + Ok(match subquery_type { + SubqueryType::Null => AnySubquery::Null, + SubqueryType::Header => AnySubquery::Header(decode_header_subquery(&mut reader)?), + SubqueryType::Account => AnySubquery::Account(decode_account_subquery(&mut reader)?), + SubqueryType::Storage => AnySubquery::Storage(decode_storage_subquery(&mut reader)?), + SubqueryType::Transaction => AnySubquery::Transaction(decode_tx_subquery(&mut reader)?), + SubqueryType::Receipt => AnySubquery::Receipt(decode_receipt_subquery(&mut reader)?), + SubqueryType::SolidityNestedMapping => AnySubquery::SolidityNestedMapping( + decode_solidity_nested_mapping_subquery(&mut reader)?, + ), + }) + } +} + +impl TryFrom for SubqueryType { + type Error = io::Error; + fn try_from(value: u16) -> Result { + match value { + 0 => Ok(Self::Null), + 1 => Ok(Self::Header), + 2 => Ok(Self::Account), + 3 => Ok(Self::Storage), + 4 => Ok(Self::Transaction), + 5 => Ok(Self::Receipt), + 6 => Ok(Self::SolidityNestedMapping), + // 7 => Ok(Self::BeaconValidator), + _ => Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid SubqueryType")), + } + } +} + +/// Decoder from `compute_proof` in bytes into `AxiomV2ComputeSnark`. +/// `reader` should be a reader on `compute_proof` in `AxiomV2ComputeQuery`. +pub fn decode_compute_snark( + mut reader: impl Read, + result_len: u16, + is_aggregation: bool, +) -> Result { + let kzg_accumulator = if is_aggregation { + let lhs = read_curve_compressed::(&mut reader)?; + let rhs = read_curve_compressed::(&mut reader)?; + Some((lhs, rhs)) + } else { + read_h256(&mut reader)?; + read_h256(&mut reader)?; + None + }; + let mut compute_results = Vec::with_capacity(result_len as usize); + for _ in 0..result_len { + compute_results.push(read_h256(&mut reader)?); + } + let mut proof_transcript = vec![]; + reader.read_to_end(&mut proof_transcript)?; + Ok(AxiomV2ComputeSnark { kzg_accumulator, compute_results, proof_transcript }) +} + +pub fn decode_subquery(mut reader: impl Read) -> Result { + let subquery_type = reader.read_u16::()?; + let subquery_type = subquery_type.try_into()?; + let mut buf = vec![]; + reader.read_to_end(&mut buf)?; + let encoded_subquery_data = Bytes::from(buf); + let subquery = Subquery { subquery_type, encoded_subquery_data }; + subquery.try_into() +} + +pub fn decode_header_subquery(mut reader: impl Read) -> Result { + let block_number = reader.read_u32::()?; + let field_idx = reader.read_u32::()?; + Ok(HeaderSubquery { block_number, field_idx }) +} + +pub fn decode_account_subquery(mut reader: impl Read) -> Result { + let block_number = reader.read_u32::()?; + let addr = read_address(&mut reader)?; + let field_idx = reader.read_u32::()?; + Ok(AccountSubquery { block_number, addr, field_idx }) +} + +pub fn decode_storage_subquery(mut reader: impl Read) -> Result { + let block_number = reader.read_u32::()?; + let addr = read_address(&mut reader)?; + let slot = read_u256(&mut reader)?; + Ok(StorageSubquery { block_number, addr, slot }) +} + +pub fn decode_tx_subquery(mut reader: impl Read) -> Result { + let block_number = reader.read_u32::()?; + let tx_idx = reader.read_u16::()?; + let field_or_calldata_idx = reader.read_u32::()?; + Ok(TxSubquery { block_number, tx_idx, field_or_calldata_idx }) +} + +pub fn decode_receipt_subquery(mut reader: impl Read) -> Result { + let block_number = reader.read_u32::()?; + let tx_idx = reader.read_u16::()?; + let field_or_log_idx = reader.read_u32::()?; + let topic_or_data_or_address_idx = reader.read_u32::()?; + let event_schema = read_h256(&mut reader)?; + Ok(ReceiptSubquery { + block_number, + tx_idx, + field_or_log_idx, + topic_or_data_or_address_idx, + event_schema, + }) +} + +pub fn decode_solidity_nested_mapping_subquery( + mut reader: impl Read, +) -> Result { + let block_number = reader.read_u32::()?; + let addr = read_address(&mut reader)?; + let mapping_slot = read_u256(&mut reader)?; + let mapping_depth = reader.read_u8()?; + if mapping_depth as usize > MAX_SOLIDITY_MAPPING_KEYS { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "SolidityNestedMappingSubquery mapping_depth {} exceeds MAX_SOLIDITY_MAPPING_KEYS {}", + mapping_depth, MAX_SOLIDITY_MAPPING_KEYS + ), + )); + } + let mut keys = Vec::with_capacity(mapping_depth as usize); + for _ in 0..mapping_depth { + keys.push(read_h256(&mut reader)?); + } + Ok(SolidityNestedMappingSubquery { block_number, addr, mapping_slot, mapping_depth, keys }) +} diff --git a/axiom-codec/src/encoder/field_elements.rs b/axiom-codec/src/encoder/field_elements.rs new file mode 100644 index 00000000..8deb6a86 --- /dev/null +++ b/axiom-codec/src/encoder/field_elements.rs @@ -0,0 +1,332 @@ +use std::io; + +use ethers_core::types::{Bytes, H256}; + +use crate::{ + constants::*, + types::field_elements::*, + types::native::*, + utils::native::{encode_addr_to_field, encode_h256_to_hilo, encode_u256_to_hilo}, + Field, +}; + +pub const NUM_FE_HEADER: usize = 2; +pub const NUM_FE_ACCOUNT: usize = 3; +pub const NUM_FE_STORAGE: usize = 4; +pub const NUM_FE_TX: usize = 3; +pub const NUM_FE_RECEIPT: usize = 6; +pub const NUM_FE_SOLIDITY_NESTED_MAPPING: usize = + NUM_FE_SOLIDITY_NESTED_MAPPING_WITHOUT_KEYS + MAX_SOLIDITY_MAPPING_KEYS * 2; +pub const NUM_FE_SOLIDITY_NESTED_MAPPING_WITHOUT_KEYS: usize = 5; +pub const NUM_FE_SOLIDITY_NESTED_MAPPING_MIN: usize = 7; // assumes >=1 key +pub const NUM_FE_ANY: [usize; NUM_SUBQUERY_TYPES] = [ + 0, + NUM_FE_HEADER, + NUM_FE_ACCOUNT, + NUM_FE_STORAGE, + NUM_FE_TX, + NUM_FE_RECEIPT, + NUM_FE_SOLIDITY_NESTED_MAPPING, +]; + +/// The index of the mapping depth in [`FieldSolidityNestedMappingSubquery`]. +pub const FIELD_SOLIDITY_NESTED_MAPPING_DEPTH_IDX: usize = 4; +/// The index of the mapping depth in [`SubqueryKey`], where the first index holds the subquery type. +pub const FIELD_ENCODED_SOLIDITY_NESTED_MAPPING_DEPTH_IDX: usize = + FIELD_SOLIDITY_NESTED_MAPPING_DEPTH_IDX + 1; + +// The following constants describe how to convert from the field element +// encoding into the bytes encoding, by specifying the fixed width bytes each field +// element represented (in big endian order). +pub const BYTES_PER_FE_HEADER: [usize; NUM_FE_HEADER] = [4, 4]; +pub const BITS_PER_FE_HEADER: [usize; NUM_FE_HEADER] = [32, 32]; +pub const BYTES_PER_FE_ACCOUNT: [usize; NUM_FE_ACCOUNT] = [4, 20, 4]; +pub const BITS_PER_FE_ACCOUNT: [usize; NUM_FE_ACCOUNT] = [32, 160, 32]; +pub const BYTES_PER_FE_STORAGE: [usize; NUM_FE_STORAGE] = [4, 20, 16, 16]; +pub const BITS_PER_FE_STORAGE: [usize; NUM_FE_STORAGE] = [32, 160, 128, 128]; +pub const BYTES_PER_FE_TX: [usize; NUM_FE_TX] = [4, 2, 4]; +pub const BITS_PER_FE_TX: [usize; NUM_FE_TX] = [32, 16, 32]; +pub const BYTES_PER_FE_RECEIPT: [usize; NUM_FE_RECEIPT] = [4, 2, 4, 4, 16, 16]; +pub const BITS_PER_FE_RECEIPT: [usize; NUM_FE_RECEIPT] = [32, 16, 32, 32, 128, 128]; +pub const BYTES_PER_FE_SOLIDITY_NESTED_MAPPING: [usize; NUM_FE_SOLIDITY_NESTED_MAPPING] = + bytes_per_fe_solidity_nested_mapping(); +pub const BITS_PER_FE_SOLIDITY_NESTED_MAPPING: [usize; NUM_FE_SOLIDITY_NESTED_MAPPING] = + bits_per_fe_solidity_nested_mapping(); +pub const BYTES_PER_FE_ANY: [&[usize]; NUM_SUBQUERY_TYPES] = [ + &[], + &BYTES_PER_FE_HEADER, + &BYTES_PER_FE_ACCOUNT, + &BYTES_PER_FE_STORAGE, + &BYTES_PER_FE_TX, + &BYTES_PER_FE_RECEIPT, + &BYTES_PER_FE_SOLIDITY_NESTED_MAPPING, +]; +pub const BYTES_PER_FE_SUBQUERY_OUTPUT: usize = 16; + +pub const BITS_PER_FE_SUBQUERY_RESULT: [usize; SUBQUERY_RESULT_LEN] = [128; SUBQUERY_RESULT_LEN]; +pub const SUBQUERY_OUTPUT_BYTES: usize = MAX_SUBQUERY_OUTPUTS * BYTES_PER_FE_SUBQUERY_OUTPUT; + +const fn bytes_per_fe_solidity_nested_mapping() -> [usize; NUM_FE_SOLIDITY_NESTED_MAPPING] { + let mut bytes_per = [16; MAX_SUBQUERY_INPUTS]; + bytes_per[0] = 4; + bytes_per[1] = 20; + bytes_per[2] = 16; + bytes_per[3] = 16; + bytes_per[4] = 1; + bytes_per +} +const fn bits_per_fe_solidity_nested_mapping() -> [usize; NUM_FE_SOLIDITY_NESTED_MAPPING] { + let mut bits_per = [128; MAX_SUBQUERY_INPUTS]; + bits_per[0] = 32; + bits_per[1] = 160; + bits_per[2] = 128; + bits_per[3] = 128; + bits_per[4] = 8; + bits_per +} + +pub const BITS_PER_FE_SUBQUERY_OUTPUT: usize = BYTES_PER_FE_SUBQUERY_OUTPUT * 8; + +impl From for SubqueryOutput { + fn from(value: H256) -> Self { + let mut output = [F::ZERO; MAX_SUBQUERY_OUTPUTS]; + let hilo = encode_h256_to_hilo(&value); + output[0] = hilo.hi(); + output[1] = hilo.lo(); + Self(output) + } +} + +impl TryFrom for SubqueryOutput { + type Error = io::Error; + fn try_from(bytes: Bytes) -> Result { + if bytes.len() != 32 { + Err(io::Error::new(io::ErrorKind::InvalidData, "result length is not 32")) + } else { + let result = H256::from_slice(&bytes[..]); + let hilo = encode_h256_to_hilo(&result); + let mut result = [F::ZERO; MAX_SUBQUERY_OUTPUTS]; + result[0] = hilo.hi(); + result[1] = hilo.lo(); + Ok(Self(result)) + } + } +} + +impl TryFrom for FlattenedSubqueryResult { + type Error = io::Error; + fn try_from(result: SubqueryResult) -> Result { + let result: FieldSubqueryResult = result.try_into()?; + Ok(result.into()) + } +} + +impl TryFrom for FieldSubqueryResult { + type Error = io::Error; + fn try_from(result: SubqueryResult) -> Result { + let subquery = result.subquery.try_into()?; + let value = result.value.try_into()?; + Ok(FieldSubqueryResult { subquery, value }) + } +} + +impl TryFrom for FieldSubquery { + type Error = io::Error; + fn try_from(subquery: Subquery) -> Result { + let subquery = AnySubquery::try_from(subquery)?; + Ok(subquery.into()) + } +} + +impl From for FieldSubquery { + fn from(subquery: AnySubquery) -> Self { + match subquery { + AnySubquery::Null => FieldSubquery { + subquery_type: SubqueryType::Null, + encoded_subquery_data: Default::default(), + }, + AnySubquery::Header(subquery) => FieldHeaderSubquery::from(subquery).into(), + AnySubquery::Account(subquery) => FieldAccountSubquery::from(subquery).into(), + AnySubquery::Storage(subquery) => FieldStorageSubquery::from(subquery).into(), + AnySubquery::Transaction(subquery) => FieldTxSubquery::from(subquery).into(), + AnySubquery::Receipt(subquery) => FieldReceiptSubquery::from(subquery).into(), + AnySubquery::SolidityNestedMapping(subquery) => { + FieldSolidityNestedMappingSubquery::from(subquery).into() + } + } + } +} + +impl From for FieldHeaderSubquery { + fn from(subquery: HeaderSubquery) -> Self { + Self { + block_number: F::from(subquery.block_number as u64), + field_idx: F::from(subquery.field_idx as u64), + } + } +} + +impl FieldHeaderSubquery { + pub fn flatten(self) -> [T; NUM_FE_HEADER] { + [self.block_number, self.field_idx] + } +} + +impl From> for FieldSubquery { + fn from(subquery: FieldHeaderSubquery) -> Self { + let mut encoded_subquery_data = [F::ZERO; MAX_SUBQUERY_INPUTS]; + encoded_subquery_data[..NUM_FE_HEADER].copy_from_slice(&subquery.flatten()); + Self { subquery_type: SubqueryType::Header, encoded_subquery_data } + } +} + +impl From for FieldAccountSubquery { + fn from(subquery: AccountSubquery) -> Self { + Self { + block_number: F::from(subquery.block_number as u64), + addr: encode_addr_to_field(&subquery.addr), + field_idx: F::from(subquery.field_idx as u64), + } + } +} + +impl FieldAccountSubquery { + pub fn flatten(self) -> [T; NUM_FE_ACCOUNT] { + [self.block_number, self.addr, self.field_idx] + } +} + +impl From> for FieldSubquery { + fn from(value: FieldAccountSubquery) -> Self { + let mut encoded_subquery_data = [F::ZERO; MAX_SUBQUERY_INPUTS]; + encoded_subquery_data[..NUM_FE_ACCOUNT].copy_from_slice(&value.flatten()); + Self { subquery_type: SubqueryType::Account, encoded_subquery_data } + } +} + +impl From for FieldStorageSubquery { + fn from(subquery: StorageSubquery) -> Self { + Self { + block_number: F::from(subquery.block_number as u64), + addr: encode_addr_to_field(&subquery.addr), + slot: encode_u256_to_hilo(&subquery.slot), + } + } +} + +impl FieldStorageSubquery { + pub fn flatten(self) -> [T; NUM_FE_STORAGE] { + [self.block_number, self.addr, self.slot.hi(), self.slot.lo()] + } +} + +impl From> for FieldSubquery { + fn from(value: FieldStorageSubquery) -> Self { + let mut encoded_subquery_data = [F::ZERO; MAX_SUBQUERY_INPUTS]; + encoded_subquery_data[..NUM_FE_STORAGE].copy_from_slice(&value.flatten()); + Self { subquery_type: SubqueryType::Storage, encoded_subquery_data } + } +} + +impl From for FieldTxSubquery { + fn from(subquery: TxSubquery) -> Self { + Self { + block_number: F::from(subquery.block_number as u64), + tx_idx: F::from(subquery.tx_idx as u64), + field_or_calldata_idx: F::from(subquery.field_or_calldata_idx as u64), + } + } +} + +impl FieldTxSubquery { + pub fn flatten(self) -> [T; NUM_FE_TX] { + [self.block_number, self.tx_idx, self.field_or_calldata_idx] + } +} + +impl From> for FieldSubquery { + fn from(value: FieldTxSubquery) -> Self { + let mut encoded_subquery_data = [F::ZERO; MAX_SUBQUERY_INPUTS]; + encoded_subquery_data[..NUM_FE_TX].copy_from_slice(&value.flatten()); + Self { subquery_type: SubqueryType::Transaction, encoded_subquery_data } + } +} + +impl From for FieldReceiptSubquery { + fn from(subquery: ReceiptSubquery) -> Self { + Self { + block_number: F::from(subquery.block_number as u64), + tx_idx: F::from(subquery.tx_idx as u64), + field_or_log_idx: F::from(subquery.field_or_log_idx as u64), + topic_or_data_or_address_idx: F::from(subquery.topic_or_data_or_address_idx as u64), + event_schema: encode_h256_to_hilo(&subquery.event_schema), + } + } +} + +impl FieldReceiptSubquery { + pub fn flatten(self) -> [T; NUM_FE_RECEIPT] { + [ + self.block_number, + self.tx_idx, + self.field_or_log_idx, + self.topic_or_data_or_address_idx, + self.event_schema.hi(), + self.event_schema.lo(), + ] + } +} + +impl From> for FieldSubquery { + fn from(value: FieldReceiptSubquery) -> Self { + let mut encoded_subquery_data = [F::ZERO; MAX_SUBQUERY_INPUTS]; + encoded_subquery_data[..NUM_FE_RECEIPT].copy_from_slice(&value.flatten()); + Self { subquery_type: SubqueryType::Receipt, encoded_subquery_data } + } +} + +impl From for FieldSolidityNestedMappingSubquery { + fn from(mut subquery: SolidityNestedMappingSubquery) -> Self { + assert!(subquery.keys.len() <= MAX_SOLIDITY_MAPPING_KEYS); + subquery.keys.resize(MAX_SOLIDITY_MAPPING_KEYS, H256::zero()); + + Self { + block_number: F::from(subquery.block_number as u64), + addr: encode_addr_to_field(&subquery.addr), + mapping_slot: encode_u256_to_hilo(&subquery.mapping_slot), + mapping_depth: F::from(subquery.mapping_depth as u64), + keys: subquery + .keys + .iter() + .map(|k| encode_h256_to_hilo(k)) + .collect::>() + .try_into() + .unwrap(), + } + } +} + +impl FieldSolidityNestedMappingSubquery { + pub fn flatten(self) -> [T; NUM_FE_SOLIDITY_NESTED_MAPPING] { + assert_eq!(5 + self.keys.len() * 2, NUM_FE_SOLIDITY_NESTED_MAPPING); + let mut result = [self.block_number; NUM_FE_SOLIDITY_NESTED_MAPPING]; // default will be overwritten in all indices so doesn't matter + result[0] = self.block_number; + result[1] = self.addr; + result[2] = self.mapping_slot.hi(); + result[3] = self.mapping_slot.lo(); + result[4] = self.mapping_depth; + for (i, key) in self.keys.iter().enumerate() { + result[5 + i * 2] = key.hi(); + result[5 + i * 2 + 1] = key.lo(); + } + result + } +} + +impl From> for FieldSubquery { + fn from(value: FieldSolidityNestedMappingSubquery) -> Self { + let mut encoded_subquery_data = [F::ZERO; MAX_SUBQUERY_INPUTS]; + encoded_subquery_data[..NUM_FE_SOLIDITY_NESTED_MAPPING].copy_from_slice(&value.flatten()); + Self { subquery_type: SubqueryType::SolidityNestedMapping, encoded_subquery_data } + } +} diff --git a/axiom-codec/src/encoder/mod.rs b/axiom-codec/src/encoder/mod.rs new file mode 100644 index 00000000..1f0e0c19 --- /dev/null +++ b/axiom-codec/src/encoder/mod.rs @@ -0,0 +1,3 @@ +/// Encode different subqueries into in-circuit field element format +pub mod field_elements; +pub mod native; diff --git a/axiom-codec/src/encoder/native.rs b/axiom-codec/src/encoder/native.rs new file mode 100644 index 00000000..7404a6af --- /dev/null +++ b/axiom-codec/src/encoder/native.rs @@ -0,0 +1,304 @@ +use std::{ + io::{Error, ErrorKind, Result, Write}, + iter, +}; + +use byteorder::{BigEndian, WriteBytesExt}; +use ethers_core::{types::H256, utils::keccak256}; + +use crate::{ + constants::USER_PROOF_LEN_BYTES, + types::native::{ + AccountSubquery, AnySubquery, AxiomV2ComputeQuery, AxiomV2ComputeSnark, AxiomV2DataQuery, + HeaderSubquery, ReceiptSubquery, SolidityNestedMappingSubquery, StorageSubquery, Subquery, + SubqueryResult, SubqueryType, TxSubquery, + }, + utils::writer::{write_curve_compressed, write_u256}, + VERSION, +}; + +pub fn get_query_hash_v2( + source_chain_id: u64, + data_query: &AxiomV2DataQuery, + compute_query: &AxiomV2ComputeQuery, +) -> Result { + if source_chain_id != data_query.source_chain_id { + return Err(Error::new(ErrorKind::InvalidInput, "source_chain_id mismatch")); + } + let data_query_hash = data_query.keccak(); + let encoded_compute_query = compute_query.encode()?; + + let mut encoded = vec![]; + encoded.write_u8(VERSION)?; + encoded.write_u64::(source_chain_id)?; + encoded.write_all(data_query_hash.as_bytes())?; + encoded.write_all(&encoded_compute_query)?; + Ok(H256(keccak256(encoded))) +} + +impl AxiomV2ComputeQuery { + pub fn encode(&self) -> Result> { + if self.k == 0 { + return Ok([&[0u8], &self.result_len.to_be_bytes()[..]].concat()); + } + let encoded_query_schema = encode_query_schema(self.k, self.result_len, &self.vkey)?; + let proof_len = self.compute_proof.len() as u32; + let encoded_proof_len = proof_len.to_be_bytes(); + assert_eq!(encoded_proof_len.len(), USER_PROOF_LEN_BYTES); + let encoded_compute_proof = [&encoded_proof_len[..], &self.compute_proof].concat(); + Ok([encoded_query_schema, encoded_compute_proof].concat()) + } + + pub fn keccak(&self) -> Result { + Ok(H256(keccak256(self.encode()?))) + } +} + +impl AxiomV2ComputeSnark { + /// Encoded `kzg_accumulator` (is any) followed by `compute_results`. + pub fn encode_instances(&self) -> Result> { + let mut encoded = vec![]; + if let Some((lhs, rhs)) = self.kzg_accumulator { + write_curve_compressed(&mut encoded, lhs)?; + write_curve_compressed(&mut encoded, rhs)?; + } else { + encoded = vec![0u8; 64]; + } + for &output in &self.compute_results { + encoded.write_all(&output[..])?; + } + Ok(encoded) + } + + pub fn encode(&self) -> Result> { + let mut encoded = self.encode_instances()?; + encoded.write_all(&self.proof_transcript)?; + Ok(encoded) + } +} + +impl AxiomV2DataQuery { + pub fn keccak(&self) -> H256 { + get_data_query_hash(self.source_chain_id, &self.subqueries) + } +} + +pub fn get_data_query_hash(source_chain_id: u64, subqueries: &[Subquery]) -> H256 { + let subquery_hashes = subqueries.iter().flat_map(|subquery| subquery.keccak().0); + let encoded: Vec<_> = + iter::empty().chain(source_chain_id.to_be_bytes()).chain(subquery_hashes).collect(); + H256(keccak256(encoded)) +} + +pub fn get_query_schema_hash(k: u8, result_len: u16, vkey: &[H256]) -> Result { + if k == 0 { + return Ok(H256::zero()); + } + let encoded_query_schema = encode_query_schema(k, result_len, vkey)?; + Ok(H256(keccak256(encoded_query_schema))) +} + +pub fn encode_query_schema(k: u8, result_len: u16, vkey: &[H256]) -> Result> { + if k >= 28 { + return Err(Error::new(ErrorKind::InvalidInput, "k must be less than 28")); + } + if k == 0 { + unreachable!() + } + let mut encoded = Vec::with_capacity(3 + vkey.len() * 32); + encoded.write_u8(k)?; + encoded.write_u16::(result_len)?; + let vkey_len: u8 = vkey + .len() + .try_into() + .map_err(|_| Error::new(ErrorKind::InvalidInput, "vkey len exceeds u8"))?; + encoded.write_u8(vkey_len)?; + for fe in vkey { + encoded.write_all(fe.as_bytes())?; + } + Ok(encoded) +} + +impl Subquery { + pub fn encode(&self) -> Vec { + let sub_type = (self.subquery_type as u16).to_be_bytes(); + let subquery_data = self.encoded_subquery_data.as_ref(); + [&sub_type[..], subquery_data].concat() + } + pub fn keccak(&self) -> H256 { + H256(keccak256(self.encode())) + } +} + +impl SubqueryResult { + pub fn encode(&self) -> Vec { + let subquery = self.subquery.encode(); + let value = self.value.as_ref(); + [&subquery[..], value].concat() + } + pub fn keccak(&self) -> H256 { + H256(keccak256(self.encode())) + } +} + +pub fn encode_header_subquery(writer: &mut impl Write, subquery: HeaderSubquery) -> Result<()> { + let HeaderSubquery { block_number, field_idx } = subquery; + writer.write_u32::(block_number)?; + writer.write_u32::(field_idx)?; + Ok(()) +} + +pub fn encode_account_subquery(writer: &mut impl Write, subquery: AccountSubquery) -> Result<()> { + let AccountSubquery { block_number, addr, field_idx } = subquery; + writer.write_u32::(block_number)?; + writer.write_all(&addr[..])?; + writer.write_u32::(field_idx)?; + Ok(()) +} + +pub fn encode_storage_subquery(writer: &mut impl Write, subquery: StorageSubquery) -> Result<()> { + let StorageSubquery { block_number, addr, slot } = subquery; + writer.write_u32::(block_number)?; + writer.write_all(&addr[..])?; + write_u256(writer, slot)?; + Ok(()) +} + +pub fn encode_tx_subquery(writer: &mut impl Write, subquery: TxSubquery) -> Result<()> { + let TxSubquery { block_number, tx_idx, field_or_calldata_idx } = subquery; + writer.write_u32::(block_number)?; + writer.write_u16::(tx_idx)?; + writer.write_u32::(field_or_calldata_idx)?; + Ok(()) +} + +pub fn encode_receipt_subquery(writer: &mut impl Write, subquery: ReceiptSubquery) -> Result<()> { + let ReceiptSubquery { + block_number, + tx_idx, + field_or_log_idx, + topic_or_data_or_address_idx, + event_schema, + } = subquery; + writer.write_u32::(block_number)?; + writer.write_u16::(tx_idx)?; + writer.write_u32::(field_or_log_idx)?; + writer.write_u32::(topic_or_data_or_address_idx)?; + writer.write_all(&event_schema[..])?; + Ok(()) +} + +pub fn encode_solidity_nested_mapping_subquery( + writer: &mut impl Write, + subquery: SolidityNestedMappingSubquery, +) -> Result<()> { + let SolidityNestedMappingSubquery { block_number, addr, mapping_slot, mapping_depth, mut keys } = + subquery; + writer.write_u32::(block_number)?; + writer.write_all(&addr[..])?; + write_u256(writer, mapping_slot)?; + writer.write_u8(mapping_depth)?; + keys.resize(mapping_depth as usize, H256::zero()); + for key in keys { + writer.write_all(&key[..])?; + } + Ok(()) +} + +impl From for Subquery { + fn from(value: HeaderSubquery) -> Self { + let mut bytes = vec![]; + encode_header_subquery(&mut bytes, value).unwrap(); + Self { subquery_type: SubqueryType::Header, encoded_subquery_data: bytes.into() } + } +} + +impl From for Subquery { + fn from(value: AccountSubquery) -> Self { + let mut bytes = vec![]; + encode_account_subquery(&mut bytes, value).unwrap(); + Self { subquery_type: SubqueryType::Account, encoded_subquery_data: bytes.into() } + } +} + +impl From for Subquery { + fn from(value: StorageSubquery) -> Self { + let mut bytes = vec![]; + encode_storage_subquery(&mut bytes, value).unwrap(); + Self { subquery_type: SubqueryType::Storage, encoded_subquery_data: bytes.into() } + } +} + +impl From for Subquery { + fn from(value: TxSubquery) -> Self { + let mut bytes = vec![]; + encode_tx_subquery(&mut bytes, value).unwrap(); + Self { subquery_type: SubqueryType::Transaction, encoded_subquery_data: bytes.into() } + } +} + +impl From for Subquery { + fn from(value: ReceiptSubquery) -> Self { + let mut bytes = vec![]; + encode_receipt_subquery(&mut bytes, value).unwrap(); + Self { subquery_type: SubqueryType::Receipt, encoded_subquery_data: bytes.into() } + } +} + +impl From for Subquery { + fn from(value: SolidityNestedMappingSubquery) -> Self { + let mut bytes = vec![]; + encode_solidity_nested_mapping_subquery(&mut bytes, value).unwrap(); + Self { + subquery_type: SubqueryType::SolidityNestedMapping, + encoded_subquery_data: bytes.into(), + } + } +} + +impl From for Subquery { + fn from(value: AnySubquery) -> Self { + match value { + AnySubquery::Null => { + Self { subquery_type: SubqueryType::Null, encoded_subquery_data: vec![].into() } + } + AnySubquery::Header(subquery) => subquery.into(), + AnySubquery::Account(subquery) => subquery.into(), + AnySubquery::Storage(subquery) => subquery.into(), + AnySubquery::Transaction(subquery) => subquery.into(), + AnySubquery::Receipt(subquery) => subquery.into(), + AnySubquery::SolidityNestedMapping(subquery) => subquery.into(), + } + } +} + +impl From for AnySubquery { + fn from(value: HeaderSubquery) -> Self { + AnySubquery::Header(value) + } +} +impl From for AnySubquery { + fn from(value: AccountSubquery) -> Self { + AnySubquery::Account(value) + } +} +impl From for AnySubquery { + fn from(value: StorageSubquery) -> Self { + AnySubquery::Storage(value) + } +} +impl From for AnySubquery { + fn from(value: TxSubquery) -> Self { + AnySubquery::Transaction(value) + } +} +impl From for AnySubquery { + fn from(value: ReceiptSubquery) -> Self { + AnySubquery::Receipt(value) + } +} +impl From for AnySubquery { + fn from(value: SolidityNestedMappingSubquery) -> Self { + AnySubquery::SolidityNestedMapping(value) + } +} diff --git a/axiom-codec/src/lib.rs b/axiom-codec/src/lib.rs new file mode 100644 index 00000000..5b91790c --- /dev/null +++ b/axiom-codec/src/lib.rs @@ -0,0 +1,16 @@ +#![feature(trait_alias)] +#![feature(io_error_other)] + +pub use axiom_eth::utils::hilo::HiLo; +pub use axiom_eth::Field; + +/// Constants +pub mod constants; +pub mod decoder; +pub mod encoder; +/// Special constants used in subquery specification +pub mod special_values; +pub mod types; +pub mod utils; + +pub const VERSION: u8 = 0x02; diff --git a/axiom-codec/src/special_values.rs b/axiom-codec/src/special_values.rs new file mode 100644 index 00000000..8d0e12ee --- /dev/null +++ b/axiom-codec/src/special_values.rs @@ -0,0 +1,24 @@ +pub const HEADER_EXTRA_DATA_LEN_FIELD_IDX: usize = 52; +pub const HEADER_HASH_FIELD_IDX: usize = 50; +pub const HEADER_HEADER_SIZE_FIELD_IDX: usize = 51; +pub const HEADER_LOGS_BLOOM_FIELD_IDX_OFFSET: usize = 70; +pub const RECEIPT_ADDRESS_IDX: usize = 50; +pub const RECEIPT_BLOCK_NUMBER_FIELD_IDX: usize = 52; +pub const RECEIPT_DATA_IDX_OFFSET: usize = 100; +pub const RECEIPT_FIELD_IDX_OFFSET: usize = 0; +pub const RECEIPT_LOG_IDX_OFFSET: usize = 100; +pub const RECEIPT_LOGS_BLOOM_IDX_OFFSET: usize = 70; +pub const RECEIPT_TOPIC_IDX_OFFSET: usize = 0; +pub const RECEIPT_TX_INDEX_FIELD_IDX: usize = 53; +pub const RECEIPT_TX_TYPE_FIELD_IDX: usize = 51; +pub const TX_BLOCK_NUMBER_FIELD_IDX: usize = 52; +pub const TX_CALLDATA_HASH_FIELD_IDX: usize = 55; +pub const TX_CALLDATA_IDX_OFFSET: usize = 100; +pub const TX_CONTRACT_DATA_IDX_OFFSET: usize = 100000; +pub const TX_CONTRACT_DEPLOY_SELECTOR_VALUE: usize = 60; +pub const TX_DATA_LENGTH_FIELD_IDX: usize = 56; +pub const TX_FIELD_IDX_OFFSET: usize = 0; +pub const TX_FUNCTION_SELECTOR_FIELD_IDX: usize = 54; +pub const TX_NO_CALLDATA_SELECTOR_VALUE: usize = 61; +pub const TX_TX_INDEX_FIELD_IDX: usize = 53; +pub const TX_TX_TYPE_FIELD_IDX: usize = 51; diff --git a/axiom-codec/src/types/field_elements.rs b/axiom-codec/src/types/field_elements.rs new file mode 100644 index 00000000..1642a601 --- /dev/null +++ b/axiom-codec/src/types/field_elements.rs @@ -0,0 +1,173 @@ +use std::{fmt::Debug, ops::Deref}; + +use serde::{Deserialize, Serialize}; + +use crate::{ + constants::{MAX_SOLIDITY_MAPPING_KEYS, MAX_SUBQUERY_INPUTS, MAX_SUBQUERY_OUTPUTS}, + Field, HiLo, +}; + +use super::native::SubqueryType; + +pub const SUBQUERY_KEY_LEN: usize = 1 + MAX_SUBQUERY_INPUTS; +pub const SUBQUERY_RESULT_LEN: usize = SUBQUERY_KEY_LEN + MAX_SUBQUERY_OUTPUTS; + +/// Subquery type and data +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct SubqueryKey(pub [T; SUBQUERY_KEY_LEN]); + +/// Only the output of the subquery. Does not include subquery data. +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct SubqueryOutput(pub [T; MAX_SUBQUERY_OUTPUTS]); + +impl Deref for SubqueryKey { + type Target = [T]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Deref for SubqueryOutput { + type Target = [T]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct FlattenedSubqueryResult { + pub key: SubqueryKey, + pub value: SubqueryOutput, +} + +impl FlattenedSubqueryResult { + pub fn new(key: SubqueryKey, value: SubqueryOutput) -> Self { + Self { key, value } + } +} + +impl FlattenedSubqueryResult { + pub fn flatten(&self) -> [T; SUBQUERY_RESULT_LEN] { + self.to_fixed_array() + } + + pub fn to_fixed_array(&self) -> [T; SUBQUERY_RESULT_LEN] { + [&self.key.0[..], &self.value.0[..]].concat().try_into().unwrap_or_else(|_| unreachable!()) + } +} + +/// You should probably use [FlattenedSubqueryResult] instead. +#[derive(Clone, Copy, Debug)] +pub struct FieldSubqueryResult { + pub subquery: FieldSubquery, + pub value: SubqueryOutput, +} + +impl FieldSubqueryResult { + pub fn flatten(self) -> FlattenedSubqueryResult { + FlattenedSubqueryResult { key: self.subquery.flatten(), value: self.value } + } + pub fn to_fixed_array(self) -> [F; SUBQUERY_RESULT_LEN] { + self.flatten().to_fixed_array() + } +} + +impl From> for FlattenedSubqueryResult { + fn from(value: FieldSubqueryResult) -> Self { + value.flatten() + } +} + +impl From> for SubqueryKey { + fn from(subquery: FieldSubquery) -> Self { + let mut key = [F::ZERO; 1 + MAX_SUBQUERY_INPUTS]; + key[0] = F::from(subquery.subquery_type as u64); + key[1..].copy_from_slice(&subquery.encoded_subquery_data); + Self(key) + } +} + +/// Subquery resized to fixed length. For ZK use. +/// Consider using [SubqueryKey] instead. +#[derive(Clone, Copy, Debug)] +pub struct FieldSubquery { + pub subquery_type: SubqueryType, + pub encoded_subquery_data: [T; MAX_SUBQUERY_INPUTS], +} + +impl FieldSubquery { + pub fn flatten(&self) -> SubqueryKey { + let mut key = [F::ZERO; SUBQUERY_KEY_LEN]; + key[0] = F::from(self.subquery_type as u64); + key[1..].copy_from_slice(&self.encoded_subquery_data); + SubqueryKey(key) + } +} + +// unpacked for now +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct FieldHeaderSubquery { + pub block_number: T, + pub field_idx: T, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct FieldAccountSubquery { + pub block_number: T, + pub addr: T, // F::CAPACITY >= 160 + pub field_idx: T, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct FieldStorageSubquery { + pub block_number: T, + pub addr: T, // F::CAPACITY >= 160 + pub slot: HiLo, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct FieldTxSubquery { + pub block_number: T, + pub tx_idx: T, + pub field_or_calldata_idx: T, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct FieldReceiptSubquery { + pub block_number: T, + pub tx_idx: T, + pub field_or_log_idx: T, + pub topic_or_data_or_address_idx: T, + pub event_schema: HiLo, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct FieldSolidityNestedMappingSubquery { + pub block_number: T, + pub addr: T, // F::CAPACITY >= 160 + pub mapping_slot: HiLo, + pub mapping_depth: T, + pub keys: [HiLo; MAX_SOLIDITY_MAPPING_KEYS], +} + +/// A result consists of a pair of the original subquery and the output value. +/// This type is just a pair, but has nicer JSON serialization. +#[derive(Clone, Copy, Debug, Hash, Serialize, Deserialize, PartialEq, Eq)] +pub struct AnySubqueryResult { + pub subquery: S, + pub value: T, +} + +impl AnySubqueryResult { + pub fn new(subquery: S, value: T) -> Self { + Self { subquery, value } + } +} + +pub type FieldHeaderSubqueryResult = AnySubqueryResult, HiLo>; +pub type FieldAccountSubqueryResult = AnySubqueryResult, HiLo>; +pub type FieldStorageSubqueryResult = AnySubqueryResult, HiLo>; +pub type FieldTxSubqueryResult = AnySubqueryResult, HiLo>; +pub type FieldReceiptSubqueryResult = AnySubqueryResult, HiLo>; +pub type FieldSolidityNestedMappingSubqueryResult = + AnySubqueryResult, HiLo>; diff --git a/axiom-codec/src/types/mod.rs b/axiom-codec/src/types/mod.rs new file mode 100644 index 00000000..b3deaa5c --- /dev/null +++ b/axiom-codec/src/types/mod.rs @@ -0,0 +1,4 @@ +/// Types consisting of fixed width field elements, for a > 253 bit field. +pub mod field_elements; +/// Rust native types, bytes based +pub mod native; diff --git a/axiom-codec/src/types/native.rs b/axiom-codec/src/types/native.rs new file mode 100644 index 00000000..bf826685 --- /dev/null +++ b/axiom-codec/src/types/native.rs @@ -0,0 +1,149 @@ +use axiom_eth::halo2curves::bn256::G1Affine; +use ethers_core::types::{Address, Bytes, H256, U256}; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +#[derive(Clone, Debug, Serialize, Deserialize, Hash)] +#[serde(rename_all = "camelCase")] +pub struct AxiomV2ComputeQuery { + pub k: u8, + pub result_len: u16, + // Should be bytes32[] + /// The onchain vkey + pub vkey: Vec, + /// This is actually the concatenation of public instances and proof transcript + pub compute_proof: Bytes, +} + +#[derive(Clone, Debug)] +pub struct AxiomV2ComputeSnark { + /// (lhs G1 of pairing, rhs G1 of pairing) if this snark is from an aggregation circuit. + pub kzg_accumulator: Option<(G1Affine, G1Affine)>, + pub compute_results: Vec, + pub proof_transcript: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Hash)] +#[serde(rename_all = "camelCase")] +pub struct AxiomV2DataQuery { + pub source_chain_id: u64, + pub subqueries: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Hash)] +#[serde(rename_all = "camelCase")] +pub struct SubqueryResult { + pub subquery: Subquery, + /// The output of the subquery. In V2, always bytes32. + pub value: Bytes, +} + +impl Default for SubqueryResult { + fn default() -> Self { + Self { subquery: Subquery::default(), value: Bytes::from([0u8; 32]) } + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +pub struct Subquery { + /// uint16 type of subquery + pub subquery_type: SubqueryType, + /// Subquery data encoded, _without_ the subquery type. Length is variable and **not** resized. + pub encoded_subquery_data: Bytes, +} + +#[derive( + Clone, + Copy, + Debug, + Default, + Serialize_repr, + Deserialize_repr, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, +)] +#[repr(u16)] +pub enum SubqueryType { + #[default] + Null = 0, // For lookup tables, important to have a null type + Header = 1, + Account = 2, + Storage = 3, + Transaction = 4, + Receipt = 5, + SolidityNestedMapping = 6, + // BeaconValidator = 7, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum AnySubquery { + Null, + Header(HeaderSubquery), + Account(AccountSubquery), + Storage(StorageSubquery), + Transaction(TxSubquery), + Receipt(ReceiptSubquery), + SolidityNestedMapping(SolidityNestedMappingSubquery), +} + +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +pub struct HeaderSubquery { + pub block_number: u32, + pub field_idx: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +pub struct AccountSubquery { + pub block_number: u32, + pub addr: Address, + pub field_idx: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +pub struct StorageSubquery { + pub block_number: u32, + pub addr: Address, + pub slot: U256, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +pub struct TxSubquery { + /// The block number with the requested transaction. + pub block_number: u32, + /// The index of the transaction in the block. + pub tx_idx: u16, + /// Special index to specify what subquery value to extract from the transaction. + pub field_or_calldata_idx: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +pub struct ReceiptSubquery { + /// The block number with the requested transaction. + pub block_number: u32, + /// The index of the transaction in the block. + pub tx_idx: u16, + /// Special index to specify what subquery value to extract from the transaction. + pub field_or_log_idx: u32, + pub topic_or_data_or_address_idx: u32, + pub event_schema: H256, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +pub struct SolidityNestedMappingSubquery { + pub block_number: u32, + pub addr: Address, + pub mapping_slot: U256, + /// Should be equal to `keys.len()` + pub mapping_depth: u8, + pub keys: Vec, +} diff --git a/axiom-codec/src/utils/mod.rs b/axiom-codec/src/utils/mod.rs new file mode 100644 index 00000000..b63267cf --- /dev/null +++ b/axiom-codec/src/utils/mod.rs @@ -0,0 +1,5 @@ +pub mod native; +pub mod reader; +/// Implement conversion from codec types to [axiom_eth::utils::component::types::Flatten] types +mod shim; +pub mod writer; diff --git a/axiom-codec/src/utils/native.rs b/axiom-codec/src/utils/native.rs new file mode 100644 index 00000000..c24af17e --- /dev/null +++ b/axiom-codec/src/utils/native.rs @@ -0,0 +1,46 @@ +pub use axiom_eth::utils::{encode_addr_to_field, encode_h256_to_hilo}; +use ethers_core::types::{Address, H256, U256}; + +use crate::{Field, HiLo}; + +pub fn u256_to_h256(input: &U256) -> H256 { + let mut bytes = [0; 32]; + input.to_big_endian(&mut bytes); + H256(bytes) +} + +pub fn decode_hilo_to_h256(fe: HiLo) -> H256 { + let mut bytes = [0u8; 32]; + bytes[..16].copy_from_slice(&fe.lo().to_bytes_le()[..16]); + bytes[16..].copy_from_slice(&fe.hi().to_bytes_le()[..16]); + bytes.reverse(); + H256(bytes) +} + +/// Takes U256, converts to bytes32 (big endian) and returns (hash[..16], hash[16..]) represented as big endian numbers in the prime field +pub fn encode_u256_to_hilo(input: &U256) -> HiLo { + let mut bytes = vec![0; 32]; + input.to_little_endian(&mut bytes); + // repr is in little endian + let mut repr = [0u8; 32]; + repr[..16].copy_from_slice(&bytes[16..]); + let hi = F::from_bytes_le(&repr); + let mut repr = [0u8; 32]; + repr[..16].copy_from_slice(&bytes[..16]); + let lo = F::from_bytes_le(&repr); + HiLo::from_lo_hi([lo, hi]) +} + +pub fn decode_hilo_to_u256(fe: HiLo) -> U256 { + let mut bytes = [0u8; 32]; + bytes[..16].copy_from_slice(&fe.lo().to_bytes_le()[..16]); + bytes[16..].copy_from_slice(&fe.hi().to_bytes_le()[..16]); + U256::from_little_endian(&bytes) +} + +pub fn decode_field_to_addr(fe: &F) -> Address { + let mut bytes = [0u8; 20]; + bytes.copy_from_slice(&fe.to_bytes_le()[..20]); + bytes.reverse(); + Address::from_slice(&bytes) +} diff --git a/axiom-codec/src/utils/reader.rs b/axiom-codec/src/utils/reader.rs new file mode 100644 index 00000000..eb3d6610 --- /dev/null +++ b/axiom-codec/src/utils/reader.rs @@ -0,0 +1,44 @@ +use std::io::{Error, ErrorKind, Read, Result}; + +use axiom_eth::halo2curves::CurveAffine; +use ethers_core::types::{Address, H256, U256}; + +use crate::Field; + +pub fn read_address(reader: &mut impl Read) -> Result
{ + let mut addr = [0u8; 20]; + reader.read_exact(&mut addr)?; + Ok(Address::from_slice(&addr)) +} + +pub fn read_u256(reader: &mut impl Read) -> Result { + let mut word = [0u8; 32]; + reader.read_exact(&mut word)?; + Ok(U256::from_big_endian(&word)) +} + +pub fn read_h256(reader: &mut impl Read) -> Result { + let mut hash = [0u8; 32]; + reader.read_exact(&mut hash)?; + Ok(H256(hash)) +} + +pub fn read_field_le(reader: &mut impl Read) -> Result { + let mut repr = [0u8; 32]; + reader.read_exact(&mut repr)?; + Ok(F::from_bytes_le(&repr)) +} + +pub fn read_field_be(reader: &mut impl Read) -> Result { + let mut repr = [0u8; 32]; + reader.read_exact(&mut repr)?; + repr.reverse(); + Ok(F::from_bytes_le(&repr)) +} + +pub fn read_curve_compressed(reader: &mut impl Read) -> Result { + let mut compressed = C::Repr::default(); + reader.read_exact(compressed.as_mut())?; + Option::from(C::from_bytes(&compressed)) + .ok_or_else(|| Error::new(ErrorKind::Other, "Invalid compressed point encoding")) +} diff --git a/axiom-codec/src/utils/shim.rs b/axiom-codec/src/utils/shim.rs new file mode 100644 index 00000000..061324df --- /dev/null +++ b/axiom-codec/src/utils/shim.rs @@ -0,0 +1,30 @@ +use axiom_eth::{impl_flatten_conversion, impl_logical_input, Field}; + +use crate::{ + encoder::field_elements::{ + BITS_PER_FE_ACCOUNT, BITS_PER_FE_HEADER, BITS_PER_FE_RECEIPT, + BITS_PER_FE_SOLIDITY_NESTED_MAPPING, BITS_PER_FE_STORAGE, BITS_PER_FE_SUBQUERY_RESULT, + BITS_PER_FE_TX, + }, + types::field_elements::{ + FieldAccountSubquery, FieldHeaderSubquery, FieldReceiptSubquery, + FieldSolidityNestedMappingSubquery, FieldStorageSubquery, FieldTxSubquery, + FlattenedSubqueryResult, + }, +}; + +// Inputs by subquery type, in field elements +impl_flatten_conversion!(FieldHeaderSubquery, BITS_PER_FE_HEADER); +impl_logical_input!(FieldHeaderSubquery, 1); +impl_flatten_conversion!(FieldAccountSubquery, BITS_PER_FE_ACCOUNT); +impl_logical_input!(FieldAccountSubquery, 1); +impl_flatten_conversion!(FieldStorageSubquery, BITS_PER_FE_STORAGE); +impl_logical_input!(FieldStorageSubquery, 1); +impl_flatten_conversion!(FieldTxSubquery, BITS_PER_FE_TX); +impl_logical_input!(FieldTxSubquery, 1); +impl_flatten_conversion!(FieldReceiptSubquery, BITS_PER_FE_RECEIPT); +impl_logical_input!(FieldReceiptSubquery, 1); +impl_flatten_conversion!(FieldSolidityNestedMappingSubquery, BITS_PER_FE_SOLIDITY_NESTED_MAPPING); +impl_logical_input!(FieldSolidityNestedMappingSubquery, 1); +impl_flatten_conversion!(FlattenedSubqueryResult, BITS_PER_FE_SUBQUERY_RESULT); +impl_logical_input!(FlattenedSubqueryResult, 0); diff --git a/axiom-codec/src/utils/writer.rs b/axiom-codec/src/utils/writer.rs new file mode 100644 index 00000000..755638e9 --- /dev/null +++ b/axiom-codec/src/utils/writer.rs @@ -0,0 +1,25 @@ +use std::io::{Result, Write}; + +use axiom_eth::{halo2_base::utils::ScalarField, halo2curves::CurveAffine}; +use ethers_core::types::U256; + +use crate::Field; + +pub fn write_u256(writer: &mut impl Write, word: U256) -> Result<()> { + let mut buf = [0u8; 32]; + word.to_big_endian(&mut buf); + writer.write_all(&buf)?; + Ok(()) +} + +pub fn write_field_le(writer: &mut impl Write, fe: F) -> Result<()> { + let repr = ScalarField::to_bytes_le(&fe); + writer.write_all(&repr)?; + Ok(()) +} + +pub fn write_curve_compressed(writer: &mut impl Write, point: C) -> Result<()> { + let compressed = point.to_bytes(); + writer.write_all(compressed.as_ref())?; + Ok(()) +} diff --git a/axiom-core/Cargo.toml b/axiom-core/Cargo.toml new file mode 100644 index 00000000..5adac55a --- /dev/null +++ b/axiom-core/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "axiom-core" +version = "2.0.12" +authors = ["Intrinsic Technologies"] +license = "MIT" +edition = "2021" +repository = "https://github.com/axiom-crypto/axiom-eth" +readme = "README.md" +description = "This contains the ZK circuits that generate proofs for the `AxiomV2Core` smart contract. These circuits read the RLP encoded block headers for a chain of blocks and verify that the block headers form a chain. They output a Merkle Mountain Range of the block hashes of the chain. This crate also contains aggregation circuits to aggregate multiple circuits for the purpose of proving longer chains." +rust-version = "1.73.0" + +[[bin]] +name = "axiom-core-keygen" +path = "src/bin/keygen.rs" +required-features = ["keygen"] + +[dependencies] +itertools = "0.11" +lazy_static = "1.4.0" +# serialization +serde = { version = "1.0", default-features = false, features = ["derive"] } +serde_json = { version = "1.0", default-features = false } +serde_with = { version = "3.3", features = ["base64"] } +# misc +log = "0.4" +env_logger = "0.10" +#getset = "0.1.2" +anyhow = "1.0" +hex = "0.4.3" + +# halo2, features turned on by axiom-eth +axiom-eth = { version = "=0.4.0", path = "../axiom-eth", default-features = false, features = ["providers", "aggregation", "evm"] } + +# crypto +ethers-core = { version = "=2.0.10" } + +# keygen +clap = { version = "=4.4.7", features = ["derive"], optional = true } +blake3 = { version = "=1.5", optional = true } +serde_yaml = { version = "=0.9.16", optional = true } + +[dev-dependencies] +hex = "0.4.3" +ark-std = { version = "0.3.0", features = ["print-trace"] } +test-log = "0.2.11" +test-case = "3.1.0" +rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } +rand = "0.8" +rand_chacha = "0.3.1" +axiom-eth = { path = "../axiom-eth", features = ["revm"] } + +[features] +default = ["halo2-axiom", "jemallocator", "keygen", "display"] +display = ["axiom-eth/display"] +asm = ["axiom-eth/asm"] +revm = ["axiom-eth/revm"] +# halo2-pse = ["axiom-eth/halo2-pse"] +halo2-axiom = ["axiom-eth/halo2-axiom"] +jemallocator = ["axiom-eth/jemallocator"] +keygen = ["axiom-eth/keygen", "dep:clap", "dep:serde_yaml"] diff --git a/axiom-core/KEYGEN.md b/axiom-core/KEYGEN.md new file mode 100644 index 00000000..7469b4ea --- /dev/null +++ b/axiom-core/KEYGEN.md @@ -0,0 +1,62 @@ +# AxiomV2Core ZK Circuits + +# Proving and Verifying Key Generation + +To generate the exact proving and verifying keys we use in production on Ethereum Mainnet, you can do the following: + +1. Download the KZG trusted setup that we use with [this script](../trusted_setup_s3.sh). + +``` +bash ../trusted_setup_s3.sh +``` + +You can read more about the trusted setup we use and how it was generated [here](https://docs.axiom.xyz/docs/transparency-and-security/kzg-trusted-setup). + +The trusted setup will be downloaded to a directory called `params/` by default. You can move the directory elsewhere. We'll refer to the directory as `$SRS_DIR` below. + +2. Install `axiom-core-keygen` binary to your path via: + +```bash +cargo install --path axiom-core --force +``` + +This builds the `axiom-core-keygen` binary in release mode and installs it to your path. +Additional details about the binary can be found [here](./src/bin/README.md). + +3. Generate the proving and verifying keys for the `AxiomV2CoreVerifier` smart contract via: + +```bash +axiom-core-keygen --srs-dir $SRS_DIR --intent configs/production/core.yml --tag v2.0.12 --data-dir $CIRCUIT_DATA_DIR +``` + +where `$CIRCUIT_DATA_DIR` is the directory you want to store the output files. After the process is complete, a summary JSON with the different circuit IDs created will be output to `$CIRCUIT_DATA_DIR/v2.0.12.cids`. + +4. Rename and forge format the Solidity SNARK verifier file for `AxiomV2CoreVerifier`: + +Check that in `$CIRCUIT_DATA_DIR/v2.0.12.cids` the final aggregation circuit with `"node_type": {"Evm":1}` has circuit ID `39cb264c605428fc752e90b6ac1b77427ab06b795419a759e237e283b95f377f`. +Then run + +```bash +bash src/bin/rename_snark_verifier.sh $CIRCUIT_DATA_DIR/39cb264c605428fc752e90b6ac1b77427ab06b795419a759e237e283b95f377f.sol +``` + +The final Solidity file will be output to `AxiomV2CoreVerifier.sol`. + +5. Generate the proving and verifying keys for the `AxiomV2CoreHistoricalVerifier` smart contract via: + +```bash +axiom-core-keygen --srs-dir $SRS_DIR --intent configs/production/core_historical.yml --tag v2.0.12.historical --data-dir $CIRCUIT_DATA_DIR +``` + +where `$CIRCUIT_DATA_DIR` is the directory you want to store the output files. After the process is complete, a summary JSON with the different circuit IDs created will be output to `$CIRCUIT_DATA_DIR/v2.0.12.historical.cids`. + +6. Rename and forge format the Solidity SNARK verifier file for `AxiomV2CoreHistoricalVerifier`: + +Check that in `$CIRCUIT_DATA_DIR/v2.0.12.historical.cids` the final aggregation circuit with `"node_type": {"Evm":1}` has circuit ID `0379c723deafac09822de4f36da40a5595331c447a5cc7c342eb839cd199be02`. +Then run + +```bash +bash src/bin/rename_snark_verifier.sh $CIRCUIT_DATA_DIR/0379c723deafac09822de4f36da40a5595331c447a5cc7c342eb839cd199be02.sol +``` + +The final Solidity file will be output to `AxiomV2CoreHistoricalVerifier.sol`. diff --git a/axiom-core/README.md b/axiom-core/README.md new file mode 100644 index 00000000..0d1c5f9c --- /dev/null +++ b/axiom-core/README.md @@ -0,0 +1,100 @@ +# AxiomV2Core ZK Circuits + +# Proving and Verifying Key Generation + +For instructions on how to generate the exact proving and verifying keys we use in production on Ethereum Mainnet, see [here](./KEYGEN.md). + +# Public Instance Formats + +Any `Snark` has an associated `Vec` of public instances. We describe the format for the ones relevant to the `AxiomV2Core` circuits below. + +## `EthBlockHeaderChainCircuit` + +```rust +pub struct EthBlockHeaderChainInput { + header_rlp_encodings: Vec>, + num_blocks: u32, // num_blocks in [0, 2 ** max_depth) + max_depth: usize, + network: Network, + _marker: PhantomData, +} +``` + +This depends on a `max_depth` parameter. The public instances are: + +- `prev_hash`: `H256` as two `Fr` elements in hi-lo format +- `end_hash`: `H256` as two `Fr` elements in hi-lo format +- `start_block_number . end_block_number`: we assume both numbers are `u32` and encode them to a single `Fr` element as `start_block_number * 2^32 + end_block_number` +- `merkle_mountain_range`: a sequence of `max_depth + 1` `H256` elements, each encoded as two `Fr` elements in hi-lo format + +Notes: + +- `prev_hash` is the parent hash of block number `start_block_number` +- `end_hash` is the block hash of block number `end_block_number` +- `end_block_number - start_block_number` is constrained to be `<= 2^max_depth` + - This was previously assumed in `axiom-eth` `v0.1.1` but not enforced because the block numbers are public instances, but we now enforce it for safety +- `merkle_mountain_range` is ordered from largest peak (depth `max_depth`) first to smallest peak (depth `0`) last + +## `EthBlockHeaderChainIntermediateAggregationCircuit` + +```rust +pub struct EthBlockHeaderChainIntermediateAggregationInput { + num_blocks: u32, + snarks: Vec, + pub max_depth: usize, + pub initial_depth: usize, +} +``` + +This circuit takes two [`EthBlockHeaderChainCircuit`s](#ethblockheaderchaincircuit) and aggregates them. The public instances are: + +- `4 * LIMBS = 12` `Fr` elements for the two BN254 `G1` points representing the _accumulator_, used by the verifier for a pairing check +- `prev_hash`: `H256` as two `Fr` elements in hi-lo format +- `end_hash`: `H256` as two `Fr` elements in hi-lo format +- `start_block_number . end_block_number`: we assume both numbers are `u32` and encode them to a single `Fr` element as `start_block_number * 2^32 + end_block_number` +- `merkle_mountain_range`: a sequence of `2^{max_depth - initial_depth} + initial_depth` `H256` elements, each encoded as two `Fr` elements in hi-lo format + +Notes: + +- Same notes as [`EthBlockHeaderChainCircuit`](#ethblockheaderchaincircuit) **except** that `merkle_mountain_range` is not actually a Merkle mountain range: we recover a Merkle mountain range of length `max_depth + 1` by forming a Merkle mountain range from leaves `merkle_mountain_range[..2^{max_depth - initial_depth}]` and then appending `merkle_mountain_range[2^{max_depth - initial_depth}..]` to the end of it. + - The reason is that we want to delay Keccaks + +## `EthBlockHeaderChainRootAggregationCircuit` + +```rust +pub struct EthBlockHeaderChainRootAggregationInput { + /// See [EthBlockHeaderChainIntermediateAggregationInput] + pub inner: EthBlockHeaderChainIntermediateAggregationInput, + /// Succinct verifying key (generator of KZG trusted setup) should match `inner.snarks` + pub svk: Svk, + prev_acc_indices: Vec>, +} +``` + +This circuit takes two [`EthBlockHeaderChainIntermediateAggregationCircuit`s](#ethblockheaderchainintermediateaggregationcircuit) and aggregates them. The public instances are: + +- `4 * LIMBS = 12` `Fr` elements for the two BN254 `G1` points representing the _accumulator_, used by the verifier for a pairing check +- `prev_hash`: `H256` as two `Fr` elements in hi-lo format +- `end_hash`: `H256` as two `Fr` elements in hi-lo format +- `start_block_number . end_block_number`: we assume both numbers are `u32` and encode them to a single `Fr` element as `start_block_number * 2^32 + end_block_number` +- `merkle_mountain_range`: a sequence of `max_depth + 1` `H256` elements, each encoded as two `Fr` elements in hi-lo format + +Notes: + +- Same notes as [`EthBlockHeaderChainCircuit`](#ethblockheaderchaincircuit) +- This circuit is the same as [`EthBlockHeaderChainIntermediateAggregationCircuit`](#ethblockheaderchainintermediateaggregationcircuit) except that it does do the final Keccaks to form the full Merkle mountain range + +## Passthrough Aggregation Circuit + +This is from [`axiom-eth`](../axiom-eth/src/utils/merkle_aggregation.rs). + +```rust +pub struct InputMerkleAggregation { + pub snarks: Vec, +} +``` + +We will only use this where `snarks` has length 1 and consists of a single snark. In this case it is an `AggregationCircuit` that purely passes through the public instances of the single snark in `snarks`, discarding old accumulators (there is no Merkle root computation because there is only one snark). + +We will use this snark on [`EthBlockHeaderChainRootAggregationCircuit`] or itself if we want multiple rounds of passthrough aggregation. +The public instances are exactly the same as for [`EthBlockHeaderChainRootAggregationCircuit`]. diff --git a/axiom-core/configs/production/core.yml b/axiom-core/configs/production/core.yml new file mode 100644 index 00000000..0f23d9d3 --- /dev/null +++ b/axiom-core/configs/production/core.yml @@ -0,0 +1,8 @@ +# Core configuration used for chains where extra data field has at most 32 bytes +k_at_depth: [23, 22, 19, 21, 22, 19] +max_extra_data_bytes: 32 +params: + node_type: + Evm: 1 + depth: 10 + initial_depth: 7 diff --git a/axiom-core/configs/production/core_historical.yml b/axiom-core/configs/production/core_historical.yml new file mode 100644 index 00000000..768a9723 --- /dev/null +++ b/axiom-core/configs/production/core_historical.yml @@ -0,0 +1,8 @@ +# Core Historical configuration used for chains where extra data field has at most 32 bytes +k_at_depth: [23, 22, 22, 20, 21, 20, 20, 21, 20, 20, 21, 22, 19] +max_extra_data_bytes: 32 +params: + node_type: + Evm: 1 + depth: 17 + initial_depth: 7 diff --git a/axiom-core/configs/tests/mainnet_3.json b/axiom-core/configs/tests/mainnet_3.json new file mode 100644 index 00000000..ea1dd4f6 --- /dev/null +++ b/axiom-core/configs/tests/mainnet_3.json @@ -0,0 +1,52 @@ +[ + { + "rlc": { + "base": { + "k": 16, + "num_advice_per_phase": [ + 17, + 5 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 0, + 0 + ], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 1 + }, + "keccak_rows_per_round": 50 + }, + { + "base": [ + [ + 65425, + 65426, + 65425, + 65424, + 65425, + 65424, + 65424, + 65426, + 65426, + 65424, + 65424, + 65425, + 65426, + 65424, + 65426, + 65424 + ], + [ + 65426, + 65426, + 65425, + 65426 + ] + ], + "rlc": [] + } +] \ No newline at end of file diff --git a/axiom-core/configs/tests/mainnet_4_3.json b/axiom-core/configs/tests/mainnet_4_3.json new file mode 100644 index 00000000..aa7e8386 --- /dev/null +++ b/axiom-core/configs/tests/mainnet_4_3.json @@ -0,0 +1,65 @@ +{ + "params": { + "degree": 20, + "num_advice": 53, + "num_lookup_advice": 6, + "num_fixed": 1, + "lookup_bits": 19 + }, + "break_points": [ + [ + 1048564, + 1048566, + 1048565, + 1048564, + 1048566, + 1048564, + 1048565, + 1048565, + 1048566, + 1048564, + 1048566, + 1048564, + 1048565, + 1048565, + 1048566, + 1048564, + 1048565, + 1048566, + 1048564, + 1048565, + 1048566, + 1048564, + 1048564, + 1048566, + 1048565, + 1048564, + 1048565, + 1048564, + 1048566, + 1048565, + 1048564, + 1048566, + 1048566, + 1048566, + 1048565, + 1048566, + 1048566, + 1048565, + 1048565, + 1048566, + 1048564, + 1048565, + 1048566, + 1048564, + 1048564, + 1048566, + 1048565, + 1048564, + 1048566, + 1048565, + 1048566, + 1048566 + ] + ] +} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/storage_3_6_1.json b/axiom-core/configs/tests/mainnet_5_3_for_evm_0.json similarity index 83% rename from axiom-eth/configs/tests/batch_query/storage_3_6_1.json rename to axiom-core/configs/tests/mainnet_5_3_for_evm_0.json index 79b22b9a..f8189914 100644 --- a/axiom-eth/configs/tests/batch_query/storage_3_6_1.json +++ b/axiom-core/configs/tests/mainnet_5_3_for_evm_0.json @@ -9,10 +9,8 @@ "break_points": [ [ 8388596, - 8388596, + 8388598, 8388597 - ], - [], - [] + ] ] } \ No newline at end of file diff --git a/axiom-core/configs/tests/mainnet_5_3_root.json b/axiom-core/configs/tests/mainnet_5_3_root.json new file mode 100644 index 00000000..ab1b97ee --- /dev/null +++ b/axiom-core/configs/tests/mainnet_5_3_root.json @@ -0,0 +1,78 @@ +[ + { + "rlc": { + "base": { + "k": 20, + "num_advice_per_phase": [ + 48, + 0 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 6, + 0, + 0 + ], + "lookup_bits": 19, + "num_instance_columns": 1 + }, + "num_rlc_columns": 0 + }, + "keccak_rows_per_round": 50 + }, + { + "base": [ + [ + 1048464, + 1048464, + 1048464, + 1048466, + 1048465, + 1048464, + 1048465, + 1048465, + 1048465, + 1048466, + 1048466, + 1048465, + 1048466, + 1048465, + 1048466, + 1048464, + 1048466, + 1048464, + 1048464, + 1048466, + 1048464, + 1048465, + 1048466, + 1048465, + 1048466, + 1048464, + 1048466, + 1048465, + 1048464, + 1048464, + 1048465, + 1048465, + 1048466, + 1048466, + 1048465, + 1048464, + 1048464, + 1048464, + 1048466, + 1048466, + 1048465, + 1048466, + 1048466, + 1048464, + 1048466, + 1048464, + 1048466 + ], + [] + ], + "rlc": [] + } +] \ No newline at end of file diff --git a/axiom-core/configs/tests/multi_block.json b/axiom-core/configs/tests/multi_block.json new file mode 100644 index 00000000..fb01796c --- /dev/null +++ b/axiom-core/configs/tests/multi_block.json @@ -0,0 +1,21 @@ +{ + "rlc": { + "base": { + "k": 14, + "num_advice_per_phase": [ + 85, + 24 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 0, + 0 + ], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 3 + }, + "keccak_rows_per_round": 9 +} \ No newline at end of file diff --git a/axiom-eth/data/headers/default_blocks_goerli.json b/axiom-core/data/headers/default_blocks_goerli.json similarity index 100% rename from axiom-eth/data/headers/default_blocks_goerli.json rename to axiom-core/data/headers/default_blocks_goerli.json diff --git a/axiom-eth/data/headers/default_hashes_goerli.json b/axiom-core/data/headers/default_hashes_goerli.json similarity index 100% rename from axiom-eth/data/headers/default_hashes_goerli.json rename to axiom-core/data/headers/default_hashes_goerli.json diff --git a/axiom-eth/data/headers/mainnet_10_7_for_evm_1.yul b/axiom-core/data/headers/mainnet_10_7_for_evm_1.yul similarity index 98% rename from axiom-eth/data/headers/mainnet_10_7_for_evm_1.yul rename to axiom-core/data/headers/mainnet_10_7_for_evm_1.yul index 32802ed2..4fa2368e 100644 --- a/axiom-eth/data/headers/mainnet_10_7_for_evm_1.yul +++ b/axiom-core/data/headers/mainnet_10_7_for_evm_1.yul @@ -70,7 +70,7 @@ mstore(0x480, mod(calldataload(0x460), f_q)) mstore(0x4a0, mod(calldataload(0x480), f_q)) mstore(0x4c0, mod(calldataload(0x4a0), f_q)) mstore(0x4e0, mod(calldataload(0x4c0), f_q)) -mstore(0x0, 8412653266354028249917373514688863030371584715513288348683675775000288975891) +mstore(0x0, 12714566557495233314129647473762182141380482669824660892726865342775293934986) { let x := calldataload(0x4e0) @@ -1321,8 +1321,8 @@ mstore(0x4de0, mload(0x4d00)) mstore(0x4e20, mload(0x4d80)) mstore(0x4e40, mload(0x4da0)) success := and(eq(staticcall(gas(), 0x6, 0x4de0, 0x80, 0x4de0, 0x40), 1), success) -mstore(0x4e60, 0x1d13ede7ff4c42ae75f3e6d92da2827cbef2d3aef89ed2ccc9a4f765f6360188) - mstore(0x4e80, 0x030f7e60f598b79db0bae27268cbd88a7ae4df973c35453b5a6ccfdad9ff11ff) +mstore(0x4e60, 0x0b9a1d90f4d2149e6fd88763df52ee3b7fbd7479c8d3880038b37ab455228ca1) + mstore(0x4e80, 0x0a126c5191f45b3f6db196968d725c39024e122f77faf484cadbf20d453bee9e) mstore(0x4ea0, mload(0x4820)) success := and(eq(staticcall(gas(), 0x7, 0x4e60, 0x60, 0x4e60, 0x40), 1), success) mstore(0x4ec0, mload(0x4de0)) @@ -1339,8 +1339,8 @@ mstore(0x4fa0, mload(0x4ec0)) mstore(0x4fe0, mload(0x4f40)) mstore(0x5000, mload(0x4f60)) success := and(eq(staticcall(gas(), 0x6, 0x4fa0, 0x80, 0x4fa0, 0x40), 1), success) -mstore(0x5020, 0x24511bb528e2e9ac50ab0ed0af2698996c42f025757a6c6725cd8678bc986afc) - mstore(0x5040, 0x0bbb921d6dd604b30b9ad2b3a21a9ff666f0f31ce0397280b36c8b3e7fcb69e6) +mstore(0x5020, 0x0f030ba7d233a926023772efd048a06115a81f44243d9eb142dcc334045e3984) + mstore(0x5040, 0x122aef3feca33f9fa2b58473490d79bd7740e61c5e48be97fb8e03ffc56c1594) mstore(0x5060, mload(0x4860)) success := and(eq(staticcall(gas(), 0x7, 0x5020, 0x60, 0x5020, 0x40), 1), success) mstore(0x5080, mload(0x4fa0)) @@ -1348,8 +1348,8 @@ mstore(0x5080, mload(0x4fa0)) mstore(0x50c0, mload(0x5020)) mstore(0x50e0, mload(0x5040)) success := and(eq(staticcall(gas(), 0x6, 0x5080, 0x80, 0x5080, 0x40), 1), success) -mstore(0x5100, 0x2af66054cae62f0c8f9fccd1dc80e91e29e05c42638a0064ccd23ddb907b41c1) - mstore(0x5120, 0x00a744ccce7ca0fa72198c504cb9135bdc4c4644dcef20be2a4fcc8da824f90b) +mstore(0x5100, 0x2e2491f99291ea9f817cb8e6e6886faf5c55f365bbc21c797e229330ac7ebe65) + mstore(0x5120, 0x259e1755aa8905122f69471618263dfac184a26ffae23687d515274a74e7a8ac) mstore(0x5140, mload(0x4880)) success := and(eq(staticcall(gas(), 0x7, 0x5100, 0x60, 0x5100, 0x40), 1), success) mstore(0x5160, mload(0x5080)) @@ -1357,8 +1357,8 @@ mstore(0x5160, mload(0x5080)) mstore(0x51a0, mload(0x5100)) mstore(0x51c0, mload(0x5120)) success := and(eq(staticcall(gas(), 0x6, 0x5160, 0x80, 0x5160, 0x40), 1), success) -mstore(0x51e0, 0x2e852506bb76850bd60c8e13c186f78e105123f87c3a80ed0b5566c56e310a17) - mstore(0x5200, 0x2607ef20e29fab6b8703414cde488052a2cefdd2e4d81f0b40105f1e703e699a) +mstore(0x51e0, 0x2f4d2bf2ca4cd42f927d0fabbd31965da8bd92a4a6363692f649113d222ee176) + mstore(0x5200, 0x081f92dc68d325cac733543bfac7639fafdd59604910dd41e876bce399fff3a0) mstore(0x5220, mload(0x48a0)) success := and(eq(staticcall(gas(), 0x7, 0x51e0, 0x60, 0x51e0, 0x40), 1), success) mstore(0x5240, mload(0x5160)) @@ -1366,8 +1366,8 @@ mstore(0x5240, mload(0x5160)) mstore(0x5280, mload(0x51e0)) mstore(0x52a0, mload(0x5200)) success := and(eq(staticcall(gas(), 0x6, 0x5240, 0x80, 0x5240, 0x40), 1), success) -mstore(0x52c0, 0x059381fafcf28a916d81002477897dfd1772363fadcb0143f9151930297bac65) - mstore(0x52e0, 0x03b347be31d37236965be868ba48852c029e5124a774d2483df72990f54b3cbc) +mstore(0x52c0, 0x0acd7d0e602fca913d344401b686c3516f9b02695d957da91ba5c349eee3759f) + mstore(0x52e0, 0x0e73c1bce2daf77a052c94603561ae6d05bdf5e61adfb8ee4628feca60e0f9c2) mstore(0x5300, mload(0x48c0)) success := and(eq(staticcall(gas(), 0x7, 0x52c0, 0x60, 0x52c0, 0x40), 1), success) mstore(0x5320, mload(0x5240)) @@ -1375,8 +1375,8 @@ mstore(0x5320, mload(0x5240)) mstore(0x5360, mload(0x52c0)) mstore(0x5380, mload(0x52e0)) success := and(eq(staticcall(gas(), 0x6, 0x5320, 0x80, 0x5320, 0x40), 1), success) -mstore(0x53a0, 0x21200ab05a3645e0025a5971ae43c96947bbaa2fa02cbe3ce17d095904bd98c4) - mstore(0x53c0, 0x2fe6f17111569c4137890e976237d5edb8e060b52a29566a0218ebf048655bb1) +mstore(0x53a0, 0x00f3b507db1425bc54a89078f8954e9ee67eee7b46bccdea7bb3adc9ed9c9248) + mstore(0x53c0, 0x0a204ce583710da1783761b04a91bb79fe0e9de3e21e481e814d9f1c481c3523) mstore(0x53e0, mload(0x48e0)) success := and(eq(staticcall(gas(), 0x7, 0x53a0, 0x60, 0x53a0, 0x40), 1), success) mstore(0x5400, mload(0x5320)) diff --git a/axiom-eth/data/headers/mainnet_17_7_for_evm_1.yul b/axiom-core/data/headers/mainnet_17_7_for_evm_1.yul similarity index 100% rename from axiom-eth/data/headers/mainnet_17_7_for_evm_1.yul rename to axiom-core/data/headers/mainnet_17_7_for_evm_1.yul diff --git a/axiom-eth/data/headers/shasums.txt b/axiom-core/data/headers/shasums.txt similarity index 100% rename from axiom-eth/data/headers/shasums.txt rename to axiom-core/data/headers/shasums.txt diff --git a/axiom-core/src/aggregation/final_merkle.rs b/axiom-core/src/aggregation/final_merkle.rs new file mode 100644 index 00000000..d4715d15 --- /dev/null +++ b/axiom-core/src/aggregation/final_merkle.rs @@ -0,0 +1,184 @@ +//! The root of the aggregation tree. +//! An [EthBlockHeaderChainRootAggregationCircuit] can aggregate either: +//! - two [crate::header_chain::EthBlockHeaderChainCircuit]s (if `max_depth == initial_depth + 1`) or +//! - two [super::intermediate::EthBlockHeaderChainIntermediateAggregationCircuit]s. +//! +//! The difference between Intermediate and Root aggregation circuits is that the Intermediate ones +//! do not have a keccak sub-circuit: all keccaks are delayed until the Root aggregation. +use std::iter; + +use anyhow::{bail, Result}; +use axiom_eth::{ + halo2_base::{ + gates::{GateInstructions, RangeInstructions}, + QuantumCell::Constant, + }, + halo2_proofs::poly::{commitment::ParamsProver, kzg::commitment::ParamsKZG}, + halo2curves::bn256::{Bn256, Fr}, + mpt::MPTChip, + rlc::circuit::builder::RlcCircuitBuilder, + snark_verifier_sdk::halo2::aggregation::{ + aggregate_snarks, SnarkAggregationOutput, Svk, VerifierUniversality, + }, + snark_verifier_sdk::{Snark, SHPLONK}, + utils::{ + build_utils::aggregation::CircuitMetadata, + eth_circuit::EthCircuitInstructions, + keccak::decorator::RlcKeccakCircuitImpl, + snark_verifier::{get_accumulator_indices, NUM_FE_ACCUMULATOR}, + uint_to_bytes_be, + }, +}; +use itertools::Itertools; + +use super::intermediate::{ + join_previous_instances, EthBlockHeaderChainIntermediateAggregationInput, +}; + +/// Same as [super::intermediate::EthBlockHeaderChainIntermediateAggregationCircuit] but uses a Keccak sub-circuit to compute the final merkle mountain range. Specifically, it aggregates two snarks at `max_depth - 1` and then computes the keccaks to get the final merkle mountain root. +pub type EthBlockHeaderChainRootAggregationCircuit = + RlcKeccakCircuitImpl; + +/// The input needed to construct [EthBlockHeaderChainRootAggregationCircuit] +#[derive(Clone, Debug)] +pub struct EthBlockHeaderChainRootAggregationInput { + /// See [EthBlockHeaderChainIntermediateAggregationInput] + pub inner: EthBlockHeaderChainIntermediateAggregationInput, + /// Succinct verifying key (generator of KZG trusted setup) should match `inner.snarks` + pub svk: Svk, + prev_acc_indices: Vec>, +} + +impl EthBlockHeaderChainRootAggregationInput { + /// `snarks` should be exactly two snarks of either + /// - `EthBlockHeaderChainCircuit` if `max_depth == initial_depth + 1` or + /// - `EthBlockHeaderChainIntermediateAggregationCircuit` otherwise + /// + /// We only need the generator `kzg_params.get_g()[0]` to match that of the trusted setup used + /// in the creation of `snarks`. + pub fn new( + snarks: Vec, + num_blocks: u32, + max_depth: usize, + initial_depth: usize, + kzg_params: &ParamsKZG, + ) -> Result { + let svk = kzg_params.get_g()[0].into(); + let prev_acc_indices = get_accumulator_indices(&snarks); + if max_depth == initial_depth + 1 + && prev_acc_indices.iter().any(|indices| !indices.is_empty()) + { + bail!("Snarks to be aggregated must not have accumulators: they should come from EthBlockHeaderChainCircuit"); + } + if max_depth > initial_depth + 1 + && prev_acc_indices.iter().any(|indices| indices.len() != NUM_FE_ACCUMULATOR) + { + bail!("Snarks to be aggregated must all be EthBlockHeaderChainIntermediateAggregationCircuits"); + } + let inner = EthBlockHeaderChainIntermediateAggregationInput::new( + snarks, + num_blocks, + max_depth, + initial_depth, + ); + Ok(Self { inner, svk, prev_acc_indices }) + } +} + +impl EthCircuitInstructions for EthBlockHeaderChainRootAggregationInput { + type FirstPhasePayload = (); + + fn virtual_assign_phase0(&self, builder: &mut RlcCircuitBuilder, mpt: &MPTChip) { + let EthBlockHeaderChainIntermediateAggregationInput { + max_depth, + initial_depth, + num_blocks, + snarks, + } = self.inner.clone(); + + let keccak = mpt.keccak(); + let range = keccak.range(); + let pool = builder.base.pool(0); + let SnarkAggregationOutput { mut previous_instances, accumulator, .. } = + aggregate_snarks::(pool, range, self.svk, snarks, VerifierUniversality::None); + // remove old accumulators + for (prev_instance, acc_indices) in + previous_instances.iter_mut().zip_eq(&self.prev_acc_indices) + { + for i in acc_indices.iter().sorted().rev() { + prev_instance.remove(*i); + } + } + + let ctx = pool.main(); + let num_blocks_minus_one = ctx.load_witness(Fr::from(num_blocks as u64 - 1)); + let new_instances = join_previous_instances( + ctx, + range, + previous_instances.try_into().unwrap(), + num_blocks_minus_one, + max_depth, + initial_depth, + ); + let num_blocks = range.gate().add(ctx, num_blocks_minus_one, Constant(Fr::one())); + + // compute the keccaks that were delayed, to get the `max_depth - initial_depth + 1` biggest merkle mountain ranges + let bits = range.gate().num_to_bits(ctx, num_blocks, max_depth + 1); + // bits is in little endian, we take the top `max_depth - initial_depth + 1` bits + let num_leaves = 1 << (max_depth - initial_depth); + let num_leaves_bits = &bits[initial_depth..]; + let mmr_instances = &new_instances[5..]; + // convert from u128 to bytes + let leaves: Vec<_> = mmr_instances + .chunks(2) + .take(num_leaves) + .map(|hash| -> Vec<_> { + hash.iter() + .flat_map(|hash_u128| { + uint_to_bytes_be(ctx, range, hash_u128, 16).into_iter().map(|x| x.into()) + }) + .collect() + }) + .collect(); + let new_mmr = keccak.merkle_mountain_range(ctx, &leaves, num_leaves_bits); + let new_mmr_len = new_mmr.len(); + debug_assert_eq!(new_mmr_len, max_depth - initial_depth + 1); + // convert from bytes to u128 + let new_mmr = new_mmr + .into_iter() + .zip(num_leaves_bits.iter().rev()) + .flat_map(|((_hash_bytes, hash_u128s), bit)| { + // hash_u128s is in hi-lo form + hash_u128s.map(|hash_u128| range.gate().mul(ctx, hash_u128, *bit)) + }) + .collect_vec(); + + // expose public instances + let assigned_instances = builder.public_instances(); + assert_eq!(assigned_instances.len(), 1); + assigned_instances[0].extend( + iter::empty() + .chain(accumulator) + .chain(new_instances[..5].to_vec()) + .chain(new_mmr) + .chain(mmr_instances[2 * num_leaves..].to_vec()), + ); + } + + fn virtual_assign_phase1( + &self, + _: &mut RlcCircuitBuilder, + _: &MPTChip, + _: Self::FirstPhasePayload, + ) { + // do nothing + } +} + +impl CircuitMetadata for EthBlockHeaderChainRootAggregationInput { + const HAS_ACCUMULATOR: bool = true; + /// The instance format exactly matches that of `EthBlockHeaderChainInput`. + fn num_instance(&self) -> Vec { + vec![NUM_FE_ACCUMULATOR + 2 + 2 + 1 + 2 * (self.inner.max_depth + 1)] + } +} diff --git a/axiom-eth/src/block_header/aggregation/mod.rs b/axiom-core/src/aggregation/intermediate.rs similarity index 53% rename from axiom-eth/src/block_header/aggregation/mod.rs rename to axiom-core/src/aggregation/intermediate.rs index c3c7d0ea..4e5ddb26 100644 --- a/axiom-eth/src/block_header/aggregation/mod.rs +++ b/axiom-core/src/aggregation/intermediate.rs @@ -1,39 +1,69 @@ -use std::mem; - -use halo2_base::{ - gates::{ - builder::{CircuitBuilderStage, MultiPhaseThreadBreakPoints}, - GateInstructions, RangeChip, RangeInstructions, +//! Intermediate aggregation circuits that aggregate in a binary tree topology: +//! The leaves of the tree are formed by [crate::header_chain::EthBlockHeaderChainCircuit]s, and intermediate notes +//! of the tree are formed by [EthBlockHeaderChainIntermediateAggregationCircuit]s. +//! +//! An [EthBlockHeaderChainIntermediateAggregationCircuit] can aggregate either: +//! - two [crate::header_chain::EthBlockHeaderChainCircuit]s or +//! - two [EthBlockHeaderChainIntermediateAggregationCircuit]s. +//! +//! The root of the aggregation tree will be a [super::final_merkle::EthBlockHeaderChainRootAggregationCircuit]. +//! The difference between Intermediate and Root aggregation circuits is that the Intermediate ones +//! do not have a keccak sub-circuit: all keccaks are delayed until the Root aggregation. +use anyhow::{bail, Result}; +use axiom_eth::{ + halo2_base::{ + gates::{circuit::CircuitBuilderStage, GateInstructions, RangeChip, RangeInstructions}, + utils::ScalarField, + AssignedValue, Context, + QuantumCell::{Constant, Existing, Witness}, }, halo2_proofs::{ halo2curves::bn256::{Bn256, Fr}, poly::kzg::commitment::ParamsKZG, }, - utils::ScalarField, - AssignedValue, Context, - QuantumCell::{Constant, Existing, Witness}, + snark_verifier_sdk::{ + halo2::aggregation::{AggregationCircuit, VerifierUniversality}, + Snark, SHPLONK, + }, + utils::snark_verifier::{ + get_accumulator_indices, AggregationCircuitParams, NUM_FE_ACCUMULATOR, + }, }; use itertools::Itertools; -use snark_verifier_sdk::{halo2::aggregation::AggregationCircuit, Snark, LIMBS, SHPLONK}; -use crate::{AggregationPreCircuit, Field}; +use crate::Field; -mod final_merkle; -pub use final_merkle::*; +/// Newtype to distinguish an aggregation circuit created from [EthBlockHeaderChainIntermediateAggregationInput] +pub struct EthBlockHeaderChainIntermediateAggregationCircuit(pub AggregationCircuit); + +impl EthBlockHeaderChainIntermediateAggregationCircuit { + /// The number of instances NOT INCLUDING the accumulator + pub fn get_num_instance(max_depth: usize, initial_depth: usize) -> usize { + assert!(max_depth >= initial_depth); + 5 + 2 * ((1 << (max_depth - initial_depth)) + initial_depth) + } +} +/// The input to create an intermediate [AggregationCircuit] that aggregates [crate::header_chain::EthBlockHeaderChainCircuit]s. +/// These are intemediate aggregations because they do not perform additional keccaks. Therefore the public instance format (after excluding accumulators) is +/// different from that of the original [crate::header_chain::EthBlockHeaderChainCircuit]s. #[derive(Clone, Debug)] -pub struct EthBlockHeaderChainAggregationCircuit { +pub struct EthBlockHeaderChainIntermediateAggregationInput { // aggregation circuit with `instances` the accumulator (two G1 points) for delayed pairing verification - num_blocks: u32, - snarks: Vec, + pub num_blocks: u32, + /// `snarks` should be exactly two snarks of either + /// - `EthBlockHeaderChainCircuit` if `max_depth == initial_depth + 1` or + /// - `EthBlockHeaderChainIntermediateAggregationCircuit` (this circuit) otherwise + /// + /// Assumes `num_blocks > 0`. + pub snarks: Vec, pub max_depth: usize, pub initial_depth: usize, // because the aggregation circuit doesn't have a keccak chip, in the mountain range // vector we will store the `2^{max_depth - initial_depth}` "new roots" as well as the length `initial_depth` mountain range tail, which determines the smallest entries in the mountain range. - // chain_instance: EthBlockHeaderChainInstance, // only needed for testing } -impl EthBlockHeaderChainAggregationCircuit { +impl EthBlockHeaderChainIntermediateAggregationInput { /// `snarks` should be exactly two snarks of either /// - `EthBlockHeaderChainCircuit` if `max_depth == initial_depth + 1` or /// - `EthBlockHeaderChainAggregationCircuit` otherwise @@ -50,118 +80,86 @@ impl EthBlockHeaderChainAggregationCircuit { assert!(max_depth > initial_depth); assert!(num_blocks <= 1 << max_depth); - /* - // OLD, no longer needed except for debugging - let chain_instance = { - // basically the same logic as `join_previous_instances` except in native rust - let instance_start_idx = usize::from(initial_depth + 1 != max_depth) * 4 * LIMBS; - let [instance0, instance1] = [0, 1].map(|i| { - EthBlockHeaderChainInstance::from_instance( - &snarks[i].instances[0][instance_start_idx..], - ) - }); - - let mut roots = Vec::with_capacity((1 << (max_depth - initial_depth)) + initial_depth); - let cutoff = 1 << (max_depth - initial_depth - 1); - roots.extend_from_slice(&instance0.merkle_mountain_range[..cutoff]); - roots.extend_from_slice(&instance1.merkle_mountain_range[..cutoff]); - if num_blocks <= 1 << (max_depth - 1) { - assert_eq!( - instance0.end_block_number - instance0.start_block_number, - num_blocks - 1 - ); - roots.extend_from_slice(&instance0.merkle_mountain_range[cutoff..]); - } else { - assert_eq!(instance0.end_hash, instance1.prev_hash); - assert_eq!( - instance0.end_block_number - instance0.start_block_number, - (1 << (max_depth - 1)) - 1 - ); - assert_eq!(instance0.end_block_number, instance1.start_block_number - 1); - assert_eq!( - instance1.end_block_number - instance0.start_block_number, - num_blocks - 1 - ); - roots.extend_from_slice(&instance1.merkle_mountain_range[cutoff..]); - }; - EthBlockHeaderChainInstance { - prev_hash: instance0.prev_hash, - end_hash: if num_blocks <= 1 << (max_depth - 1) { - instance0.end_hash - } else { - instance1.end_hash - }, - start_block_number: instance0.start_block_number, - end_block_number: instance0.start_block_number + num_blocks - 1, - merkle_mountain_range: roots, - } - };*/ - Self { - snarks, - num_blocks, - max_depth, - initial_depth, - // chain_instance, - } - } - - /// The number of instances NOT INCLUDING the accumulator - pub fn get_num_instance(max_depth: usize, initial_depth: usize) -> usize { - debug_assert!(max_depth >= initial_depth); - 5 + 2 * ((1 << (max_depth - initial_depth)) + initial_depth) + Self { snarks, num_blocks, max_depth, initial_depth } } } -impl AggregationPreCircuit for EthBlockHeaderChainAggregationCircuit { - fn create( +impl EthBlockHeaderChainIntermediateAggregationInput { + pub fn build( self, stage: CircuitBuilderStage, - break_points: Option, - lookup_bits: usize, - params: &ParamsKZG, - ) -> AggregationCircuit { + circuit_params: AggregationCircuitParams, + kzg_params: &ParamsKZG, + ) -> Result { let num_blocks = self.num_blocks; let max_depth = self.max_depth; let initial_depth = self.initial_depth; log::info!( "New EthBlockHeaderChainAggregationCircuit | num_blocks: {num_blocks} | max_depth: {max_depth} | initial_depth: {initial_depth}" ); - let mut aggregation = AggregationCircuit::new::( + let prev_acc_indices = get_accumulator_indices(&self.snarks); + if self.max_depth == self.initial_depth + 1 + && prev_acc_indices.iter().any(|indices| !indices.is_empty()) + { + bail!("Snarks to be aggregated must not have accumulators: they should come from EthBlockHeaderChainCircuit"); + } + if self.max_depth > self.initial_depth + 1 + && prev_acc_indices.iter().any(|indices| indices.len() != NUM_FE_ACCUMULATOR) + { + bail!("Snarks to be aggregated must all be EthBlockHeaderChainIntermediateAggregationCircuits"); + } + let mut circuit = AggregationCircuit::new::( stage, - break_points, - lookup_bits, - params, + circuit_params, + kzg_params, self.snarks, + VerifierUniversality::None, ); - // TODO: should reuse RangeChip from aggregation circuit, but can't refactor right now - let range = RangeChip::::default(lookup_bits); - let mut builder = aggregation.inner.circuit.0.builder.borrow_mut(); + let mut prev_instances = circuit.previous_instances().clone(); + // remove old accumulators + for (prev_instance, acc_indices) in prev_instances.iter_mut().zip_eq(prev_acc_indices) { + for i in acc_indices.into_iter().sorted().rev() { + prev_instance.remove(i); + } + } + + let builder = &mut circuit.builder; + // TODO: slight computational overhead from recreating RangeChip; builder should store RangeChip as OnceCell + let range = builder.range_chip(); let ctx = builder.main(0); - let num_blocks_minus_one = - ctx.load_witness(range.gate().get_field_element(num_blocks as u64 - 1)); - let mut new_instances = join_previous_instances( + let num_blocks_minus_one = ctx.load_witness(Fr::from(num_blocks as u64 - 1)); + + let new_instances = join_previous_instances::( ctx, &range, - mem::take(&mut aggregation.previous_instances).try_into().unwrap(), + prev_instances.try_into().unwrap(), num_blocks_minus_one, max_depth, initial_depth, ); - drop(builder); - aggregation.inner.assigned_instances.append(&mut new_instances); + if builder.assigned_instances.len() != 1 { + bail!("should only have 1 instance column"); + } + assert_eq!(builder.assigned_instances[0].len(), NUM_FE_ACCUMULATOR); + builder.assigned_instances[0].extend(new_instances); - aggregation + Ok(EthBlockHeaderChainIntermediateAggregationCircuit(circuit)) } } -/// Takes the concatenated previous instances from two `EthBlockHeaderChainAggregationCircuit`s -/// of max depth `max_depth - 1` and +/// Takes the concatenated previous instances from two `EthBlockHeaderChainIntermediateAggregationCircuit`s +/// of max depth `max_depth - 1` and /// - checks that they form a chain of `max_depth` /// - updates the merkle mountain range: /// - stores the latest `2^{max_depth - initial_depth}` roots for keccak later /// - selects the correct last `initial_depth` roots for the smallest part of the range /// +/// If `max_depth - 1 == initial_depth`, then the previous instances are from two `EthBlockHeaderChainCircuit`s. +/// /// Returns the new instances for the depth `max_depth` circuit (without accumulators) +/// +/// ## Assumptions +/// - `prev_instances` are the previous instances **with old accumulators removed**. pub fn join_previous_instances( ctx: &mut Context, range: &RangeChip, @@ -171,15 +169,14 @@ pub fn join_previous_instances( initial_depth: usize, ) -> Vec> { let prev_depth = max_depth - 1; - let non_accumulator_start = if prev_depth != initial_depth { 4 * LIMBS } else { 0 }; - let num_instance = - EthBlockHeaderChainAggregationCircuit::get_num_instance(prev_depth, initial_depth) - + non_accumulator_start; - debug_assert_eq!(num_instance, prev_instances[0].len()); - debug_assert_eq!(num_instance, prev_instances[1].len()); + let num_instance = EthBlockHeaderChainIntermediateAggregationCircuit::get_num_instance( + prev_depth, + initial_depth, + ); + assert_eq!(num_instance, prev_instances[0].len()); + assert_eq!(num_instance, prev_instances[1].len()); - let instance0 = &prev_instances[0][non_accumulator_start..]; - let instance1 = &prev_instances[1][non_accumulator_start..]; + let [instance0, instance1] = prev_instances; let mountain_selector = range.is_less_than_safe(ctx, num_blocks_minus_one, 1u64 << prev_depth); // join block hashes @@ -191,7 +188,7 @@ pub fn join_previous_instances( // a == b || num_blocks <= 2^prev_depth let mut eq_check = range.gate().is_equal(ctx, *a, *b); eq_check = range.gate().or(ctx, eq_check, mountain_selector); - range.gate().assert_is_const(ctx, &eq_check, &F::one()); + range.gate().assert_is_const(ctx, &eq_check, &F::ONE); } let end_hash = intermed_hash0 .iter() @@ -210,16 +207,16 @@ pub fn join_previous_instances( end_block_number = range.gate().select(ctx, intermed_block_num0, end_block_number, mountain_selector); // make sure chains link up - let next_block_num0 = range.gate().add(ctx, intermed_block_num0, Constant(F::one())); + let next_block_num0 = range.gate().add(ctx, intermed_block_num0, Constant(F::ONE)); let mut eq_check = range.gate().is_equal(ctx, next_block_num0, intermed_block_num1); eq_check = range.gate().or(ctx, eq_check, mountain_selector); - range.gate().assert_is_const(ctx, &eq_check, &F::one()); + range.gate().assert_is_const(ctx, &eq_check, &F::ONE); // if num_blocks > 2^prev_depth, then num_blocks0 must equal 2^prev_depth let prev_max_blocks = range.gate().pow_of_two()[prev_depth]; let is_max_depth0 = - range.gate().is_equal(ctx, num_blocks0_minus_one, Constant(prev_max_blocks - F::one())); + range.gate().is_equal(ctx, num_blocks0_minus_one, Constant(prev_max_blocks - F::ONE)); eq_check = range.gate().or(ctx, is_max_depth0, mountain_selector); - range.gate().assert_is_const(ctx, &eq_check, &F::one()); + range.gate().assert_is_const(ctx, &eq_check, &F::ONE); // check number of blocks is correct let boundary_num_diff = range.gate().sub(ctx, end_block_number, start_block_number); ctx.constrain_equal(&boundary_num_diff, &num_blocks_minus_one); @@ -258,11 +255,11 @@ fn split_u64_into_u32s( range: &RangeChip, num: AssignedValue, ) -> (AssignedValue, AssignedValue) { - let v = num.value().get_lower_128() as u64; + let v = num.value().get_lower_64(); let first = F::from(v >> 32); let second = F::from(v & u32::MAX as u64); ctx.assign_region( - [Witness(second), Witness(first), Constant(range.gate().pow_of_two()[32]), Existing(num)], + [Witness(second), Witness(first), Constant(F::from(1u64 << 32)), Existing(num)], [0], ); let second = ctx.get(-4); diff --git a/axiom-core/src/aggregation/mod.rs b/axiom-core/src/aggregation/mod.rs new file mode 100644 index 00000000..0f4d4082 --- /dev/null +++ b/axiom-core/src/aggregation/mod.rs @@ -0,0 +1,2 @@ +pub mod final_merkle; +pub mod intermediate; diff --git a/axiom-core/src/bin/keygen.rs b/axiom-core/src/bin/keygen.rs new file mode 100644 index 00000000..96fdfee7 --- /dev/null +++ b/axiom-core/src/bin/keygen.rs @@ -0,0 +1,79 @@ +use std::{ + collections::BTreeMap, + env, + fs::{self, File}, + path::PathBuf, +}; + +use anyhow::Context; +use axiom_core::{keygen::RecursiveCoreIntent, types::CoreNodeType}; +use axiom_eth::{ + snark_verifier_sdk::{evm::gen_evm_verifier_shplonk, halo2::aggregation::AggregationCircuit}, + utils::build_utils::keygen::read_srs_from_dir, +}; +use clap::Parser; + +#[derive(Parser, Debug)] +pub struct Cli { + #[arg(long = "srs-dir")] + pub srs_dir: PathBuf, + #[arg(long = "data-dir")] + pub data_dir: Option, + #[arg(long = "intent")] + pub intent_path: PathBuf, + /// Tag for the output circuit IDs files. Defaults to the root circuit ID. We auto-add the .cids extension. + #[arg(short, long = "tag")] + pub tag: Option, +} + +fn main() -> anyhow::Result<()> { + env_logger::try_init().unwrap(); + let cli = Cli::parse(); + let srs_dir = cli.srs_dir; + let data_dir = cli.data_dir.unwrap_or_else(|| { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + PathBuf::from(cargo_manifest_dir).join("data").join("playground") + }); + fs::create_dir_all(&data_dir)?; + // Directly deserializing from yaml doesn't work, but going to json first does?? + let intent_json: serde_json::Value = serde_yaml::from_reader( + File::open(&cli.intent_path) + .with_context(|| format!("Failed to open file {}", cli.intent_path.display()))?, + )?; + let intent: RecursiveCoreIntent = serde_json::from_value(intent_json)?; + let node_type = intent.params.node_type; + let k = intent.k_at_depth[0]; + let mut cid_repo = BTreeMap::new(); + let (proof_node, pk, pinning) = + intent.create_and_serialize_proving_key(&srs_dir, &data_dir, &mut cid_repo)?; + println!("Circuit id: {}", proof_node.circuit_id); + + if matches!(node_type, CoreNodeType::Evm(_)) { + log::debug!("Creating verifier contract"); + let num_instance: Vec = serde_json::from_value(pinning["num_instance"].clone())?; + let solc_path = data_dir.join(format!("{}.sol", proof_node.circuit_id)); + let kzg_params = read_srs_from_dir(&srs_dir, k as u32)?; + gen_evm_verifier_shplonk::( + &kzg_params, + pk.get_vk(), + num_instance, + Some(&solc_path), + ); + println!("Verifier contract written to {}", solc_path.display()); + } + + let tag = cli.tag.unwrap_or_else(|| proof_node.circuit_id.clone()); + + // Why do we need to do this? https://stackoverflow.com/questions/62977485/how-to-serialise-and-deserialise-btreemaps-with-arbitrary-key-types + let cids: Vec<_> = cid_repo + .into_iter() + .map(|(key, cid)| (serde_json::to_string(&key).unwrap(), cid)) + .collect(); + let cid_path = data_dir.join(format!("{tag}.cids")); + let f = File::create(&cid_path).with_context(|| { + format!("Failed to create circuit IDs repository file {}", cid_path.display()) + })?; + serde_json::to_writer_pretty(f, &cids)?; + println!("Wrote circuit IDs repository to: {}", cid_path.display()); + Ok(()) +} diff --git a/axiom-core/src/bin/readme.md b/axiom-core/src/bin/readme.md new file mode 100644 index 00000000..a0e1ef7c --- /dev/null +++ b/axiom-core/src/bin/readme.md @@ -0,0 +1,29 @@ +# Proving and Verifying Key Generation + +To recursively run proving key generation on all circuits in an aggregation tree specified by an intent file, you can install the keygen binary to your path via: + +```bash +cargo install --path axiom-core --force +``` +This builds `axiom-core-keygen` binary in release mode and installs it to your path. +Then run: +```bash +axiom-core-keygen --srs-dir --intent configs/production/core.yml --tag --data-dir +``` +to actually generate the proving keys. + +For faster compile times, you can run the keygen binary directly in dev mode (still with `opt-level=3`) via: +```bash +CARGO_PROFILE_DEV_DEBUG_ASSERTIONS=false cargo run --bin axiom-core-keygen -- --srs-dir --intent configs/production/core.yml --tag --data-dir +``` +Debug assertions needs to be **off** as we use dummy witnesses that do not pass certain debug assertions. + +* A file with the mapping of circuit types to circuit IDs will be output to a `.cids` file as a JSON. +* `` defaults to `` if not specified. + +To only get the raw list of circuit IDs from the `.cids` file, run: + +```bash +jq -r '.[][1]' .cids > .txt +``` +with `jq` installed. diff --git a/axiom-core/src/bin/rename_snark_verifier.sh b/axiom-core/src/bin/rename_snark_verifier.sh new file mode 100644 index 00000000..d1c58d6e --- /dev/null +++ b/axiom-core/src/bin/rename_snark_verifier.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Check if a file name is provided +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +# File to be modified +FILE="$1" + +# The first and third line are empty +# Remove the first and third line from the file +sed '1d;3d' $FILE > "$FILE.0" + +base_name=$(basename $FILE) +if [ $base_name == "39cb264c605428fc752e90b6ac1b77427ab06b795419a759e237e283b95f377f.sol" ]; then + # Replace 'contract Halo2Verifier' with 'contract AxiomV2CoreVerifier' + new_file="AxiomV2CoreVerifier.sol" + sed "s/contract Halo2Verifier/contract AxiomV2CoreVerifier/g" "$FILE.0" > $new_file + rm -f $FILE.0 +elif [ $base_name == "0379c723deafac09822de4f36da40a5595331c447a5cc7c342eb839cd199be02.sol" ]; then + # Replace 'contract Halo2Verifier' with 'contract AxiomV2CoreHistoricalVerifier' + new_file="AxiomV2CoreHistoricalVerifier.sol" + sed "s/contract Halo2Verifier/contract AxiomV2CoreHistoricalVerifier/g" "$FILE.0" > $new_file +else + echo "Unknown file" + exit 1 +fi + +echo "Modifications complete. New file output to $new_file" +rm -f "$FILE.0" + +echo "To diff is:" +diff $FILE $new_file + +echo "Running forge fmt on $new_file" +forge fmt $new_file diff --git a/axiom-core/src/header_chain.rs b/axiom-core/src/header_chain.rs new file mode 100644 index 00000000..2346688f --- /dev/null +++ b/axiom-core/src/header_chain.rs @@ -0,0 +1,170 @@ +use std::{iter, marker::PhantomData}; + +use axiom_eth::{ + block_header::{ + get_block_header_rlp_max_lens_from_extra, get_boundary_block_data, EthBlockHeaderChip, + EthBlockHeaderWitness, + }, + halo2_base::{ + gates::{GateInstructions, RangeInstructions}, + AssignedValue, + QuantumCell::Constant, + }, + mpt::MPTChip, + rlc::circuit::builder::RlcCircuitBuilder, + utils::assign_vec, + utils::{ + build_utils::aggregation::CircuitMetadata, eth_circuit::EthCircuitInstructions, + keccak::decorator::RlcKeccakCircuitImpl, + }, +}; +use itertools::Itertools; + +use crate::Field; + +pub type EthBlockHeaderChainCircuit = RlcKeccakCircuitImpl>; + +/// The input datum for the block header chain circuit. It is used to generate a circuit. +/// +/// The public instances: +/// * prev_hash (hi-lo) +/// * end_hash (hi-lo) +/// * solidityPacked(["uint32", "uint32"], [start_block_number, end_block_number]) (F) +/// * merkle_roots: [HiLo; max_depth + 1] +#[derive(Clone, Debug)] +pub struct EthBlockHeaderChainInput { + /// The private inputs, which are the RLP encodings of the block headers + header_rlp_encodings: Vec>, + num_blocks: u32, // num_blocks in [1, 2 ** max_depth] + max_depth: usize, + /// Configuration parameters of the maximum number of bytes in the extra data field. + /// This is mostly to distinguish between mainnet and Goerli (or other forks). + max_extra_data_bytes: usize, + _marker: PhantomData, +} + +impl EthBlockHeaderChainInput { + /// Handles resizing of each `header_rlp_encodings` to max length and also + /// resizes the number of rlp encodings to 2^max_depth. + pub fn new( + mut header_rlp_encodings: Vec>, + num_blocks: u32, + max_depth: usize, + max_extra_data_bytes: usize, + ) -> Self { + let (header_rlp_max_bytes, _) = + get_block_header_rlp_max_lens_from_extra(max_extra_data_bytes); + // pad to correct length with dummies + let dummy_block_rlp = header_rlp_encodings[0].clone(); + header_rlp_encodings.resize(1 << max_depth, dummy_block_rlp); + for header_rlp in header_rlp_encodings.iter_mut() { + header_rlp.resize(header_rlp_max_bytes, 0u8); + } + Self { + header_rlp_encodings, + num_blocks, + max_depth, + max_extra_data_bytes, + _marker: PhantomData, + } + } +} + +/// Data passed from phase0 to phase1 +#[derive(Clone, Debug)] +pub struct EthBlockHeaderchainWitness { + pub max_extra_data_bytes: usize, + /// The chain of blocks, where the hash of block_chain\[i\] is proved to be the parent hash of block_chain\[i+1\] + pub block_chain: Vec>, + pub num_blocks_minus_one: AssignedValue, + pub indicator: Vec>, +} + +impl EthCircuitInstructions for EthBlockHeaderChainInput { + type FirstPhasePayload = EthBlockHeaderchainWitness; + + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + ) -> Self::FirstPhasePayload { + let chip = EthBlockHeaderChip::new(mpt.rlp, self.max_extra_data_bytes); + let keccak = mpt.keccak(); + + // ======== FIRST PHASE =========== + let ctx = builder.base.main(0); + // ==== Load private inputs ===== + let num_blocks = ctx.load_witness(F::from(self.num_blocks as u64)); + let num_blocks_minus_one = chip.gate().sub(ctx, num_blocks, Constant(F::ONE)); + // `num_blocks_minus_one` should be < 2^max_depth. + // We check this for safety, although it is not technically necessary because `num_blocks_minus_one` will equal the difference of the start, end block numbers, which are public inputs + chip.range().range_check(ctx, num_blocks_minus_one, self.max_depth); + + // ==== Load RLP encoding and decode ==== + let max_len = get_block_header_rlp_max_lens_from_extra(self.max_extra_data_bytes).0; + let block_headers = self + .header_rlp_encodings + .iter() + .map(|header| assign_vec(ctx, header.clone(), max_len)) + .collect_vec(); + let block_chain_witness = + chip.decompose_block_header_chain_phase0(builder, keccak, block_headers); + // All keccaks must be done in FirstPhase, so we compute the merkle mountain range from the RLP decoded witnesses now + let ctx = builder.base.main(0); + let num_leaves_bits = chip.gate().num_to_bits(ctx, num_blocks, self.max_depth + 1); + let block_hashes = block_chain_witness + .iter() + .map(|witness| witness.block_hash.output_bytes.as_ref().to_vec()) + .collect_vec(); + // mountain range in bytes + let mountain_range = keccak.merkle_mountain_range(ctx, &block_hashes, &num_leaves_bits); + let mountain_range = mountain_range + .into_iter() + .zip(num_leaves_bits.into_iter().rev()) + .flat_map(|((_hash_bytes, hash_u128s), bit)| { + // if the bit is 0, then we set the hash root to 0 + hash_u128s.map(|hash_u128| chip.gate().mul(ctx, hash_u128, bit)) + }) + .collect_vec(); + + let indicator = + chip.gate().idx_to_indicator(ctx, num_blocks_minus_one, block_chain_witness.len()); + let (prev_block_hash, end_block_hash, block_numbers) = + get_boundary_block_data(ctx, chip.gate(), &block_chain_witness, &indicator); + let assigned_instances = iter::empty() + .chain(prev_block_hash) + .chain(end_block_hash) + .chain(iter::once(block_numbers)) + .chain(mountain_range) + .collect_vec(); + builder.base.assigned_instances[0] = assigned_instances; + + EthBlockHeaderchainWitness { + max_extra_data_bytes: self.max_extra_data_bytes, + block_chain: block_chain_witness, + num_blocks_minus_one, + indicator, + } + } + + fn virtual_assign_phase1( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + witness: Self::FirstPhasePayload, + ) { + let chip = EthBlockHeaderChip::new(mpt.rlp, witness.max_extra_data_bytes); + let _block_chain_trace = chip.decompose_block_header_chain_phase1( + builder, + witness.block_chain, + Some((witness.num_blocks_minus_one, witness.indicator)), + ); + } +} + +impl CircuitMetadata for EthBlockHeaderChainInput { + const HAS_ACCUMULATOR: bool = false; + fn num_instance(&self) -> Vec { + vec![2 + 2 + 1 + 2 * (self.max_depth + 1)] + } +} diff --git a/axiom-core/src/keygen/mod.rs b/axiom-core/src/keygen/mod.rs new file mode 100644 index 00000000..c613185e --- /dev/null +++ b/axiom-core/src/keygen/mod.rs @@ -0,0 +1,392 @@ +use std::{collections::BTreeMap, path::Path, sync::Arc}; + +use axiom_eth::{ + block_header::GENESIS_BLOCK_RLP, + halo2_base::{ + gates::circuit::CircuitBuilderStage, + utils::halo2::{KeygenCircuitIntent, ProvingKeyGenerator}, + }, + halo2_proofs::{ + plonk::{Circuit, ProvingKey}, + poly::{commitment::ParamsProver, kzg::commitment::ParamsKZG}, + }, + halo2curves::bn256::{Bn256, Fr, G1Affine}, + snark_verifier_sdk::{ + halo2::{ + aggregation::AggregationCircuit, + utils::{ + AggregationDependencyIntent, AggregationDependencyIntentOwned, + KeygenAggregationCircuitIntent, + }, + }, + CircuitExt, Snark, + }, + utils::{ + build_utils::{ + aggregation::get_dummy_aggregation_params, + keygen::{ + compile_agg_dep_to_protocol, get_dummy_rlc_keccak_params, read_srs_from_dir, + write_pk_and_pinning, + }, + pinning::aggregation::{AggTreeId, GenericAggParams, GenericAggPinning}, + }, + merkle_aggregation::keygen::AggIntentMerkle, + DEFAULT_RLC_CACHE_BITS, + }, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + aggregation::{ + final_merkle::{ + EthBlockHeaderChainRootAggregationCircuit, EthBlockHeaderChainRootAggregationInput, + }, + intermediate::EthBlockHeaderChainIntermediateAggregationInput, + }, + header_chain::{EthBlockHeaderChainCircuit, EthBlockHeaderChainInput}, + types::{ + CoreNodeParams, CoreNodeType, CorePinningIntermediate, CorePinningLeaf, CorePinningRoot, + }, +}; + +/// Recursive intent for a node in the aggregation tree that can construct proving keys for this node and all its children. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct RecursiveCoreIntent { + /// `k_at_depth[i]` is the log2 domain size at depth `i`. So it starts from the current node and goes down the + /// layers of the tree. Our aggregation structure is such that each layer of the tree only has a single circuit type so only one `k` is needed. + pub k_at_depth: Vec, + /// Different chains (e.g., Goerli) can have different maximum number of bytes in the extra data field of the block header. + /// We configure the circuits differently based on this. + pub max_extra_data_bytes: usize, + pub params: CoreNodeParams, +} + +impl RecursiveCoreIntent { + pub fn new(k_at_depth: Vec, max_extra_data_bytes: usize, params: CoreNodeParams) -> Self { + Self { k_at_depth, max_extra_data_bytes, params } + } + /// Each layer of tree has a unique circuit type, so this is the child circuit type. + pub fn child(&self) -> Option { + assert!(!self.k_at_depth.is_empty()); + self.params.child(Some(self.max_extra_data_bytes)).map(|params| Self { + k_at_depth: self.k_at_depth[1..].to_vec(), + max_extra_data_bytes: self.max_extra_data_bytes, + params, + }) + } +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct CoreIntentLeaf { + pub k: u32, + /// Different chains (e.g., Goerli) can have different maximum number of bytes in the extra data field of the block header. + /// We configure the circuits differently based on this. + pub max_extra_data_bytes: usize, + /// The leaf layer of the aggregation starts with max number of block headers equal to 2depth. + pub depth: usize, +} + +#[derive(Clone, Debug)] +pub(crate) struct CoreIntentIntermediate { + pub k: u32, + // This is from bad UX; only svk = kzg_params.get_g()[0] is used + pub kzg_params: Arc>, + // There will be exponential duplication since both children have the same circuit type, but it seems better for clarity + pub to_agg: Vec, + /// There are always two children of the same type, so we only specify the intent for one of them + pub child_intent: AggregationDependencyIntentOwned, + /// The maximum number of block headers in the chain at this level of the tree is 2depth. + pub depth: usize, + /// The leaf layer of the aggregation starts with max number of block headers equal to 2initial_depth. + pub initial_depth: usize, +} + +#[derive(Clone, Debug)] +pub(crate) struct CoreIntentRoot { + pub k: u32, + // This is from bad UX; only svk = kzg_params.get_g()[0] is used + pub(crate) kzg_params: Arc>, + // There will be exponential duplication since both children have the same circuit type, but it seems better for clarity + pub to_agg: Vec, + /// There are always two children of the same type, so we only specify the intent for one of them + pub child_intent: AggregationDependencyIntentOwned, + /// The maximum number of block headers in the chain at this level of the tree is 2depth. + pub depth: usize, + /// The leaf layer of the aggregation starts with max number of block headers equal to 2initial_depth. + pub initial_depth: usize, +} + +/// Passthrough wrapper aggregation. +/// Internal version doesn't need any additional context. +#[derive(Clone, Debug)] +pub(crate) struct CoreIntentEvm { + pub k: u32, + // This is from bad UX; only svk = kzg_params.get_g()[0] is used + pub kzg_params: Arc>, + /// Tree of the single child + pub to_agg: AggTreeId, + /// Wrap single child + pub child_intent: AggregationDependencyIntentOwned, +} + +impl KeygenCircuitIntent for CoreIntentLeaf { + type ConcreteCircuit = EthBlockHeaderChainCircuit; + type Pinning = CorePinningLeaf; + fn get_k(&self) -> u32 { + self.k + } + fn build_keygen_circuit(self) -> Self::ConcreteCircuit { + let dummy_input = EthBlockHeaderChainInput::::new( + vec![GENESIS_BLOCK_RLP.to_vec(); 1 << self.depth], // the resizing of each header RLP is handled by constructor + 1, + self.depth, + self.max_extra_data_bytes, + ); + let circuit_params = get_dummy_rlc_keccak_params(self.k as usize, 8); + let mut circuit = EthBlockHeaderChainCircuit::new_impl( + CircuitBuilderStage::Keygen, + dummy_input, + circuit_params, + DEFAULT_RLC_CACHE_BITS, + ); + circuit.calculate_params(); + circuit + } + fn get_pinning_after_keygen( + self, + kzg_params: &ParamsKZG, + circuit: &Self::ConcreteCircuit, + ) -> Self::Pinning { + let params = circuit.params(); + let break_points = circuit.break_points(); + let num_instance = circuit.num_instance(); + let dk = (kzg_params.get_g()[0], kzg_params.g2(), kzg_params.s_g2()); + CorePinningLeaf { params, break_points, num_instance, dk: dk.into() } + } +} + +impl KeygenAggregationCircuitIntent for CoreIntentIntermediate { + fn intent_of_dependencies(&self) -> Vec { + vec![(&self.child_intent).into(); 2] + } + fn build_keygen_circuit_from_snarks(self, snarks: Vec) -> Self::AggregationCircuit { + assert_eq!(snarks.len(), 2); + + let input = EthBlockHeaderChainIntermediateAggregationInput::new( + snarks, + 1, + self.depth, + self.initial_depth, + ); + let circuit_params = get_dummy_aggregation_params(self.k as usize); + let mut circuit = + input.build(CircuitBuilderStage::Keygen, circuit_params, &self.kzg_params).unwrap(); + circuit.0.calculate_params(Some(20)); + circuit.0 + } +} + +impl KeygenCircuitIntent for CoreIntentIntermediate { + type ConcreteCircuit = AggregationCircuit; + type Pinning = CorePinningIntermediate; + fn get_k(&self) -> u32 { + self.k + } + fn build_keygen_circuit(self) -> Self::ConcreteCircuit { + self.build_keygen_circuit_shplonk() + } + fn get_pinning_after_keygen( + self, + kzg_params: &ParamsKZG, + circuit: &Self::ConcreteCircuit, + ) -> Self::Pinning { + let to_agg = compile_agg_dep_to_protocol(kzg_params, &self.child_intent, false); + let dk = (kzg_params.get_g()[0], kzg_params.g2(), kzg_params.s_g2()); + CorePinningIntermediate { + params: circuit.params(), + to_agg: vec![to_agg; self.to_agg.len()], + break_points: circuit.break_points(), + num_instance: circuit.num_instance(), + dk: dk.into(), + } + } +} + +impl KeygenAggregationCircuitIntent for CoreIntentRoot { + type AggregationCircuit = EthBlockHeaderChainRootAggregationCircuit; + fn intent_of_dependencies(&self) -> Vec { + vec![(&self.child_intent).into(); 2] + } + fn build_keygen_circuit_from_snarks(self, snarks: Vec) -> Self::AggregationCircuit { + assert_eq!(snarks.len(), 2); + + let input = EthBlockHeaderChainRootAggregationInput::new( + snarks, + 1, + self.depth, + self.initial_depth, + &self.kzg_params, + ) + .unwrap(); + // This is aggregation circuit, so set lookup bits to max + let circuit_params = get_dummy_rlc_keccak_params(self.k as usize, self.k as usize - 1); + // This is from bad UX; only svk = kzg_params.get_g()[0] is used + let mut circuit = EthBlockHeaderChainRootAggregationCircuit::new_impl( + CircuitBuilderStage::Keygen, + input, + circuit_params, + 0, // note: rlc is not used + ); + circuit.calculate_params(); + circuit + } +} + +impl KeygenCircuitIntent for CoreIntentRoot { + type ConcreteCircuit = EthBlockHeaderChainRootAggregationCircuit; + type Pinning = CorePinningRoot; + fn get_k(&self) -> u32 { + self.k + } + fn build_keygen_circuit(self) -> Self::ConcreteCircuit { + self.build_keygen_circuit_shplonk() + } + fn get_pinning_after_keygen( + self, + kzg_params: &ParamsKZG, + circuit: &Self::ConcreteCircuit, + ) -> Self::Pinning { + let params = circuit.params(); + let break_points = circuit.break_points(); + let to_agg = compile_agg_dep_to_protocol(kzg_params, &self.child_intent, false); + let dk = (kzg_params.get_g()[0], kzg_params.g2(), kzg_params.s_g2()); + CorePinningRoot { + params, + to_agg: vec![to_agg; self.to_agg.len()], + num_instance: circuit.num_instance(), + break_points, + dk: dk.into(), + } + } +} + +impl From for AggIntentMerkle { + fn from(value: CoreIntentEvm) -> Self { + AggIntentMerkle { + kzg_params: value.kzg_params, + to_agg: vec![value.to_agg], + deps: vec![value.child_intent], + k: value.k, + } + } +} + +impl KeygenCircuitIntent for CoreIntentEvm { + type ConcreteCircuit = AggregationCircuit; + type Pinning = GenericAggPinning; + fn get_k(&self) -> u32 { + self.k + } + fn build_keygen_circuit(self) -> Self::ConcreteCircuit { + AggIntentMerkle::from(self).build_keygen_circuit() + } + fn get_pinning_after_keygen( + self, + kzg_params: &ParamsKZG, + circuit: &Self::ConcreteCircuit, + ) -> Self::Pinning { + AggIntentMerkle::from(self).get_pinning_after_keygen(kzg_params, circuit) + } +} + +impl RecursiveCoreIntent { + /// Recursively creates and serializes proving keys and pinnings. + /// + /// Computes `circuit_id` as the blake3 hash of the halo2 VerifyingKey written to bytes. Writes proving key to `circuit_id.pk`, verifying key to `circuit_id.vk` and pinning to `circuit_id.json` in the `data_dir` directory. + /// + /// Returns the `circuit_id, proving_key, pinning`. + /// * `cid_repo` stores a mapping from the [CoreNodeParams] to the corresponding circuit ID. In an Axiom Core aggregation tree, the [CoreNodeParams] determines the node. + /// * `PARAMS_DIR` **must** be set because the aggregation circuit creation requires reading trusted setup files (this can be removed later). + pub fn create_and_serialize_proving_key( + self, + srs_dir: &Path, + data_dir: &Path, + cid_repo: &mut BTreeMap, + ) -> anyhow::Result<(AggTreeId, ProvingKey, serde_json::Value)> { + // If there is child, do it first + let child = if let Some(child_intent) = self.child() { + let is_aggregation = !matches!(child_intent.params.node_type, CoreNodeType::Leaf(_)); + let (child_id, child_pk, child_pinning) = + child_intent.create_and_serialize_proving_key(srs_dir, data_dir, cid_repo)?; + let num_instance: Vec = + serde_json::from_value(child_pinning["num_instance"].clone())?; + // !! ** ASSERTION: all aggregation circuits in Axiom Core have accumulator in indices 0..12 ** !! + // No aggregation circuits in Axiom Core are universal. + let agg_intent = AggregationDependencyIntentOwned { + vk: child_pk.get_vk().clone(), + num_instance, + accumulator_indices: is_aggregation + .then(|| AggregationCircuit::accumulator_indices().unwrap()), + agg_vk_hash_data: None, + }; + Some((child_id, agg_intent)) + } else { + None + }; + assert!(!self.k_at_depth.is_empty()); + let k = self.k_at_depth[0]; + let kzg_params = Arc::new(read_srs_from_dir(srs_dir, k)?); + let ((pk, pinning), children) = match self.params.node_type { + CoreNodeType::Leaf(max_extra_data_bytes) => { + let intent = + CoreIntentLeaf { k, max_extra_data_bytes, depth: self.params.initial_depth }; + (intent.create_pk_and_pinning(&kzg_params), vec![]) + } + CoreNodeType::Intermediate => { + let (child_id, child_intent) = child.unwrap(); + let to_agg = vec![child_id; 2]; + let intent = CoreIntentIntermediate { + k, + kzg_params: kzg_params.clone(), + to_agg: to_agg.clone(), + child_intent, + depth: self.params.depth, + initial_depth: self.params.initial_depth, + }; + (intent.create_pk_and_pinning(&kzg_params), to_agg) + } + CoreNodeType::Root => { + let (child_id, child_intent) = child.unwrap(); + let to_agg = vec![child_id; 2]; + let intent = CoreIntentRoot { + k, + kzg_params: kzg_params.clone(), + to_agg: to_agg.clone(), + child_intent, + depth: self.params.depth, + initial_depth: self.params.initial_depth, + }; + (intent.create_pk_and_pinning(&kzg_params), to_agg) + } + CoreNodeType::Evm(_) => { + let (child_id, child_intent) = child.unwrap(); + let to_agg = vec![child_id.clone()]; + let intent = CoreIntentEvm { + k, + to_agg: child_id, + child_intent, + kzg_params: kzg_params.clone(), + }; + (intent.create_pk_and_pinning(&kzg_params), to_agg) + } + }; + let circuit_id = write_pk_and_pinning(data_dir, &pk, &pinning)?; + if let Some(old_cid) = cid_repo.insert(self.params, circuit_id.clone()) { + if old_cid != circuit_id { + anyhow::bail!("Different circuit ID for the same node params") + } + } + let tree_id = AggTreeId { circuit_id, children, aggregate_vk_hash: None }; + Ok((tree_id, pk, pinning)) + } +} diff --git a/axiom-core/src/lib.rs b/axiom-core/src/lib.rs new file mode 100644 index 00000000..1449d755 --- /dev/null +++ b/axiom-core/src/lib.rs @@ -0,0 +1,16 @@ +pub use axiom_eth; + +/// Aggregation circuits +pub mod aggregation; +/// Circuit that parses RLP encoded block headers and constrains that the block headers actually form a block chain. +pub mod header_chain; +#[cfg(feature = "keygen")] +/// Intents and utilities for generating proving and verifying keys for production +pub mod keygen; +/// Types for different nodes in Axiom Core aggregation tree +pub mod types; + +#[cfg(test)] +pub mod tests; + +pub use axiom_eth::Field; diff --git a/axiom-core/src/tests/chain_instance.rs b/axiom-core/src/tests/chain_instance.rs new file mode 100644 index 00000000..4c1009fc --- /dev/null +++ b/axiom-core/src/tests/chain_instance.rs @@ -0,0 +1,115 @@ +use axiom_eth::utils::encode_h256_to_hilo; +use ethers_core::types::H256; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::Field; + +#[derive(Serialize, Deserialize)] +pub struct EthBlockHeaderChainInstance { + pub prev_hash: H256, + pub end_hash: H256, + pub start_block_number: u32, + pub end_block_number: u32, + merkle_mountain_range: Vec, +} + +impl EthBlockHeaderChainInstance { + pub fn new( + prev_hash: H256, + end_hash: H256, + start_block_number: u32, + end_block_number: u32, + merkle_mountain_range: Vec, + ) -> Self { + Self { prev_hash, end_hash, start_block_number, end_block_number, merkle_mountain_range } + } + pub fn to_instance(&self) -> Vec { + // * prevHash: uint256 represented as 2 uint128s + // * endHash: uint256 represented as 2 uint128s + // * startBlockNumber || endBlockNumber: 0..0 || uint32 || 0..0 || uint32 as u64 (exactly 64 bits) + // * merkleRoots: Vec, each represented as 2 uint128s + let [prev_hash, end_hash] = + [&self.prev_hash, &self.end_hash].map(|hash| encode_h256_to_hilo::(hash)); + let block_numbers = + F::from(((self.start_block_number as u64) << 32) + (self.end_block_number as u64)); + let merkle_mountain_range = self + .merkle_mountain_range + .iter() + .flat_map(|hash| encode_h256_to_hilo::(hash).hi_lo()) + .collect_vec(); + + [&prev_hash.hi_lo()[..], &end_hash.hi_lo()[..], &[block_numbers], &merkle_mountain_range] + .concat() + } + + /*fn from_instance(instance: &[F]) -> Self { + let prev_hash = decode_f(&instance[0..2]); + let end_hash = decode_field_to_h256(&instance[2..4]); + let block_numbers = instance[4].to_repr(); // little endian + let start_block_number = u32::from_le_bytes(block_numbers[4..8].try_into().unwrap()); + let end_block_number = u32::from_le_bytes(block_numbers[..4].try_into().unwrap()); + let merkle_mountain_range = + instance[5..].chunks(2).map(|chunk| decode_field_to_h256(chunk)).collect_vec(); + + Self::new(prev_hash, end_hash, start_block_number, end_block_number, merkle_mountain_range) + }*/ +} + +/* +// OLD, no longer needed except for debugging +let chain_instance = { + // basically the same logic as `join_previous_instances` except in native rust + let instance_start_idx = usize::from(initial_depth + 1 != max_depth) * 4 * LIMBS; + let [instance0, instance1] = [0, 1].map(|i| { + EthBlockHeaderChainInstance::from_instance( + &snarks[i].instances[0][instance_start_idx..], + ) + }); + + let mut roots = Vec::with_capacity((1 << (max_depth - initial_depth)) + initial_depth); + let cutoff = 1 << (max_depth - initial_depth - 1); + roots.extend_from_slice(&instance0.merkle_mountain_range[..cutoff]); + roots.extend_from_slice(&instance1.merkle_mountain_range[..cutoff]); + if num_blocks <= 1 << (max_depth - 1) { + assert_eq!( + instance0.end_block_number - instance0.start_block_number, + num_blocks - 1 + ); + roots.extend_from_slice(&instance0.merkle_mountain_range[cutoff..]); + } else { + assert_eq!(instance0.end_hash, instance1.prev_hash); + assert_eq!( + instance0.end_block_number - instance0.start_block_number, + (1 << (max_depth - 1)) - 1 + ); + assert_eq!(instance0.end_block_number, instance1.start_block_number - 1); + assert_eq!( + instance1.end_block_number - instance0.start_block_number, + num_blocks - 1 + ); + roots.extend_from_slice(&instance1.merkle_mountain_range[cutoff..]); + }; + EthBlockHeaderChainInstance { + prev_hash: instance0.prev_hash, + end_hash: if num_blocks <= 1 << (max_depth - 1) { + instance0.end_hash + } else { + instance1.end_hash + }, + start_block_number: instance0.start_block_number, + end_block_number: instance0.start_block_number + num_blocks - 1, + merkle_mountain_range: roots, + } +};*/ + +// RootAggregation +/* // Only for testing + let leaves = + &inner.chain_instance.merkle_mountain_range[..num_blocks as usize >> initial_depth]; + let mut new_mmr = get_merkle_mountain_range(leaves, max_depth - initial_depth); + new_mmr.extend_from_slice( + &inner.chain_instance.merkle_mountain_range[1 << (max_depth - initial_depth)..], + ); + inner.chain_instance.merkle_mountain_range = new_mmr; +*/ diff --git a/axiom-core/src/tests/integration.rs b/axiom-core/src/tests/integration.rs new file mode 100644 index 00000000..fa28be1e --- /dev/null +++ b/axiom-core/src/tests/integration.rs @@ -0,0 +1,403 @@ +use std::{ + fs::File, + path::{Path, PathBuf}, +}; + +use axiom_eth::{ + block_header::get_block_header_extra_bytes, + halo2_base::{gates::circuit::CircuitBuilderStage, utils::fs::gen_srs}, + halo2_proofs::plonk::Circuit, + halo2curves::bn256::Fr, + providers::{ + block::{get_block_rlp, get_blocks}, + setup_provider, + }, + rlc::virtual_region::RlcThreadBreakPoints, + snark_verifier_sdk::{ + evm::{evm_verify, gen_evm_proof_shplonk, gen_evm_verifier_shplonk}, + gen_pk, + halo2::{aggregation::AggregationCircuit, gen_snark_shplonk}, + read_pk, CircuitExt, Snark, + }, + utils::{ + build_utils::pinning::{aggregation::AggregationCircuitPinning, Halo2CircuitPinning}, + get_merkle_mountain_range, + keccak::decorator::RlcKeccakCircuitParams, + merkle_aggregation::InputMerkleAggregation, + snark_verifier::{EnhancedSnark, NUM_FE_ACCUMULATOR}, + DEFAULT_RLC_CACHE_BITS, + }, +}; +use ethers_core::types::Chain; +use itertools::Itertools; +use serde::{de::DeserializeOwned, Serialize}; +use test_log::test; + +use crate::{ + aggregation::{ + final_merkle::{ + EthBlockHeaderChainRootAggregationCircuit, EthBlockHeaderChainRootAggregationInput, + }, + intermediate::EthBlockHeaderChainIntermediateAggregationInput, + }, + header_chain::{EthBlockHeaderChainCircuit, EthBlockHeaderChainInput}, +}; + +use super::chain_instance::EthBlockHeaderChainInstance; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub enum Finality { + /// Produces as many snarks as needed to fit the entire block number range, without any final processing. + None, + /// The block number range must fit within the specified max depth. + /// Produces a single final snark with the starting & ending block numbers, previous and last block hashes, + /// and merkle mountain range as output. + Root, + /// The block number range must fit within the specified max depth. `Evm(round)` performs `round + 1` + /// rounds of SNARK verification on the final `Merkle` circuit + Evm(usize), +} + +fn fname(network: Chain, initial_depth: usize, depth: usize, finality: Finality) -> String { + let prefix = if depth == initial_depth { + format!("{}_{}", network, depth) + } else { + format!("{}_{}_{}", network, depth, initial_depth) + }; + let suffix = match finality { + Finality::None => "".to_string(), + Finality::Root => "_root".to_string(), + Finality::Evm(round) => format!("_for_evm_{round}"), + }; + format!("{}{}", prefix, suffix) +} + +fn read_json(path: impl AsRef) -> T { + serde_json::from_reader(File::open(path).unwrap()).unwrap() +} +fn write_json(path: impl AsRef, value: &T) { + serde_json::to_writer_pretty(File::create(path).unwrap(), value).unwrap(); +} + +type KeccakPinning = (RlcKeccakCircuitParams, RlcThreadBreakPoints); + +/// Does binary tree aggregation with leaf circuit EthBlockHeaderChainCircuit of depth `initial_depth`. +/// aggregates intermediate layers up to depth `max_depth`. For the root aggregation of depth `max_depth` +/// it will either use EthBlockHeaderIntermediateAggregationCircuit if finality is None, or else +/// EthBlockHeaderRootAggregationCircuit. +/// If finality is `Evm(round)`, then it will perform `round + 1` rounds of passthrough SNARK verification +/// using MerkleAggregationCircuit (with a single snark there is no merklelization, it is just passthrough). +/// +/// Proof will be for blocks [start_num, stop_num) +pub fn header_tree_aggregation( + network: Chain, + start_num: usize, + stop_num: usize, + initial_depth: usize, + max_depth: usize, + finality: Finality, +) { + // ===== Initial leaf layer ==== + // get RLP encoded headers + let provider = setup_provider(network); + let blocks = get_blocks(&provider, start_num as u64..stop_num as u64).unwrap(); + let header_rlp_encodings = + blocks.iter().map(|block| get_block_rlp(block.as_ref().unwrap())).collect_vec(); + + // create pkey + let name = fname(network, initial_depth, initial_depth, Finality::None); + let pk_path = PathBuf::from(format!("data/tests/{}.pk", &name)); + let pinning_path = format!("configs/tests/{}.json", &name); + let header_extra_bytes = get_block_header_extra_bytes(network); + let pinning: KeccakPinning = read_json(&pinning_path); + + let params = gen_srs(pinning.0.k() as u32); + let (pk, pinning) = + if let Ok(pk) = read_pk::>(&pk_path, pinning.0.clone()) { + (pk, pinning) + } else { + let first_rlp = header_rlp_encodings[0].clone(); + let input = EthBlockHeaderChainInput::::new( + vec![first_rlp], + 1, + initial_depth, + header_extra_bytes, + ); + let mut circuit = EthBlockHeaderChainCircuit::new_impl( + CircuitBuilderStage::Keygen, + input, + pinning.0, + DEFAULT_RLC_CACHE_BITS, + ); + circuit.calculate_params(); + let pk = gen_pk(¶ms, &circuit, Some(&pk_path)); + let pinning = (circuit.params(), circuit.break_points()); + write_json(pinning_path, &pinning); + (pk, pinning) + }; + let mut snarks: Vec = vec![]; + for start in (start_num..stop_num).step_by(1 << initial_depth) { + let stop = std::cmp::min(start + (1 << initial_depth), stop_num) as usize; + let rlps = header_rlp_encodings[start - start_num..stop - start_num].to_vec(); + let input = EthBlockHeaderChainInput::::new( + rlps, + (stop - start) as u32, + initial_depth, + header_extra_bytes, + ); + let circuit = EthBlockHeaderChainCircuit::new_impl( + CircuitBuilderStage::Prover, + input, + pinning.0.clone(), + DEFAULT_RLC_CACHE_BITS, + ) + .use_break_points(pinning.1.clone()); + let snark_path = format!("data/tests/{}_{}_{}.snark", &name, start, stop); + let snark = gen_snark_shplonk(¶ms, &pk, circuit, Some(snark_path)); + snarks.push(snark); + } + drop(params); + drop(pk); + + // ====== Intermediate layers ====== + let mut last_inter_depth = max_depth - 1; + if finality == Finality::None { + last_inter_depth += 1; + } + for depth in initial_depth + 1..=last_inter_depth { + let prev_snarks = std::mem::take(&mut snarks); + let mut start = start_num; + + let name = fname(network, initial_depth, depth, Finality::None); + let pk_path = PathBuf::from(format!("data/tests/{}.pk", &name)); + let pinning_path = format!("configs/tests/{}.json", &name); + let pinning: AggregationCircuitPinning = read_json(&pinning_path); + let params = gen_srs(pinning.params.degree); + let (pk, pinning) = if let Ok(pk) = read_pk::(&pk_path, pinning.params) + { + (pk, pinning) + } else { + let stop = std::cmp::min(start + (1 << (depth - 1)), stop_num); + let input = EthBlockHeaderChainIntermediateAggregationInput::new( + vec![prev_snarks[0].clone(), prev_snarks[0].clone()], + (stop - start) as u32, + depth, + initial_depth, + ); + let mut circuit = + input.build(CircuitBuilderStage::Keygen, pinning.params, ¶ms).unwrap().0; + circuit.calculate_params(Some(9)); + let pk = gen_pk(¶ms, &circuit, Some(&pk_path)); + let pinning = AggregationCircuitPinning::new(circuit.params(), circuit.break_points()); + write_json(pinning_path, &pinning); + (pk, pinning) + }; + for snark_pair in prev_snarks.into_iter().chunks(2).into_iter() { + let mut snark_pair = snark_pair.collect_vec(); + if snark_pair.len() == 1 { + let first = snark_pair[0].clone(); + snark_pair.push(first); + } + let stop = std::cmp::min(start + (1 << depth), stop_num); + let input = EthBlockHeaderChainIntermediateAggregationInput::new( + snark_pair, + (stop - start) as u32, + depth, + initial_depth, + ); + let circuit = input + .build(CircuitBuilderStage::Prover, pinning.params, ¶ms) + .unwrap() + .0 + .use_break_points(pinning.break_points.clone()); + let snark_path = format!("data/tests/{}_{}_{}.snark", &name, start, stop); + let snark = gen_snark_shplonk(¶ms, &pk, circuit, Some(snark_path)); + snarks.push(snark); + start = stop; + } + } + if finality == Finality::None { + return; + } + // ==== Root layer ==== + let depth = max_depth; + let prev_snarks = std::mem::take(&mut snarks); + let mut start = start_num; + + let name = fname(network, initial_depth, depth, Finality::Root); + let pk_path = PathBuf::from(format!("data/tests/{}.pk", &name)); + let pinning_path = format!("configs/tests/{}.json", &name); + let pinning: KeccakPinning = read_json(&pinning_path); + let params = gen_srs(pinning.0.k() as u32); + let (pk, pinning) = if let Ok(pk) = + read_pk::(&pk_path, pinning.0.clone()) + { + (pk, pinning) + } else { + let stop = std::cmp::min(start + (1 << (depth - 1)), stop_num); + let input = EthBlockHeaderChainRootAggregationInput::new( + vec![prev_snarks[0].clone(), prev_snarks[0].clone()], + (stop - start) as u32, + depth, + initial_depth, + ¶ms, + ) + .unwrap(); + let mut circuit = EthBlockHeaderChainRootAggregationCircuit::new_impl( + CircuitBuilderStage::Keygen, + input, + pinning.0.clone(), + 0, // note: rlc is not used + ); + circuit.calculate_params(); + let pk = gen_pk(¶ms, &circuit, Some(&pk_path)); + let pinning = (circuit.params(), circuit.break_points()); + write_json(pinning_path, &pinning); + (pk, pinning) + }; + for snark_pair in prev_snarks.into_iter().chunks(2).into_iter() { + let mut snark_pair = snark_pair.collect_vec(); + if snark_pair.len() == 1 { + let first = snark_pair[0].clone(); + snark_pair.push(first); + } + let stop = std::cmp::min(start + (1 << depth), stop_num); + let input = EthBlockHeaderChainRootAggregationInput::new( + snark_pair, + (stop - start) as u32, + depth, + initial_depth, + ¶ms, + ) + .unwrap(); + let circuit = EthBlockHeaderChainRootAggregationCircuit::new_impl( + CircuitBuilderStage::Prover, + input, + pinning.0.clone(), + 0, // note: rlc is not used + ) + .use_break_points(pinning.1.clone()); + let snark_path = format!("data/tests/{}_{}_{}.snark", &name, start, stop); + let snark = gen_snark_shplonk(¶ms, &pk, circuit, Some(snark_path)); + snarks.push(snark); + start = stop; + } + drop(params); + drop(pk); + // ==== Passthrough verification to shrink snark size ==== + let mut final_instances = vec![]; + if let Finality::Evm(round) = finality { + for r in 0..=round { + let name = fname(network, initial_depth, depth, Finality::Evm(r)); + let pk_path = PathBuf::from(format!("data/tests/{}.pk", &name)); + let pinning_path = format!("configs/tests/{}.json", &name); + let pinning: AggregationCircuitPinning = read_json(&pinning_path); + let params = gen_srs(pinning.params.degree); + let (pk, pinning) = + if let Ok(pk) = read_pk::(&pk_path, pinning.params) { + (pk, pinning) + } else { + let input = + InputMerkleAggregation::new([EnhancedSnark::new(snarks[0].clone(), None)]); + let mut circuit = + input.build(CircuitBuilderStage::Keygen, pinning.params, ¶ms).unwrap(); + circuit.calculate_params(Some(9)); + let pk = gen_pk(¶ms, &circuit, Some(&pk_path)); + let pinning = + AggregationCircuitPinning::new(circuit.params(), circuit.break_points()); + write_json(pinning_path, &pinning); + (pk, pinning) + }; + let prev_snarks = std::mem::take(&mut snarks); + + if r < round { + let mut start = start_num; + for snark in prev_snarks { + let stop = std::cmp::min(start + (1 << depth), stop_num); + let input = InputMerkleAggregation::new([EnhancedSnark::new(snark, None)]); + let circuit = input + .build(CircuitBuilderStage::Prover, pinning.params, ¶ms) + .unwrap() + .use_break_points(pinning.break_points.clone()); + let snark_path = format!("data/tests/{}_{}_{}.snark", &name, start, stop); + let snark = gen_snark_shplonk(¶ms, &pk, circuit, Some(snark_path)); + snarks.push(snark); + start = stop; + } + } else { + // FINAL ROUND, do evm verification, requires solc installed + let num_instance = NUM_FE_ACCUMULATOR + 5 + 2 * (depth + 1); + let sol_path = PathBuf::from(format!("data/tests/{}.sol", &name)); + let deploy_code = gen_evm_verifier_shplonk::( + ¶ms, + pk.get_vk(), + vec![num_instance], + Some(&sol_path), + ); + for snark in prev_snarks { + let input = InputMerkleAggregation::new([EnhancedSnark::new(snark, None)]); + let circuit = input + .build(CircuitBuilderStage::Prover, pinning.params, ¶ms) + .unwrap() + .use_break_points(pinning.break_points.clone()); + let instances = circuit.instances(); + let proof = gen_evm_proof_shplonk(¶ms, &pk, circuit, instances.clone()); + evm_verify(deploy_code.clone(), instances.clone(), proof); + final_instances.push(instances); + } + } + } + } + // all done! + // check final instances + // if not Finality::Evm, check instances from snarks + if !snarks.is_empty() { + for snark in &snarks { + final_instances.push(snark.instances.clone()); + } + } + let mut start = start_num; + for instances in final_instances { + let stop = std::cmp::min(start + (1 << depth), stop_num); + let blocks = &blocks[start - start_num..stop - start_num]; + let prev_hash = blocks[0].clone().unwrap().parent_hash; + let block_hashes = blocks.iter().map(|b| b.clone().unwrap().hash.unwrap()).collect_vec(); + let end_hash = *block_hashes.last().unwrap(); + let mmr = get_merkle_mountain_range(&block_hashes, depth); + let chain_instance = EthBlockHeaderChainInstance::new( + prev_hash, + end_hash, + start as u32, + stop as u32 - 1, + mmr, + ) + .to_instance(); + // instances has accumulator, remove it + assert_eq!(&instances[0][NUM_FE_ACCUMULATOR..], &chain_instance); + + start = stop; + } +} + +#[test] +fn test_mainnet_header_chain_provider() { + header_tree_aggregation(Chain::Mainnet, 0x765fb3, 0x765fb3 + 7, 3, 3, Finality::None); +} + +#[test] +#[ignore = "requires a lot of memory"] +fn test_mainnet_header_chain_intermediate_aggregation() { + header_tree_aggregation(Chain::Mainnet, 0x765fb3, 0x765fb3 + 11, 3, 4, Finality::None); +} + +#[test] +#[ignore = "requires over 32G memory"] +fn test_mainnet_header_chain_root_aggregation() { + header_tree_aggregation(Chain::Mainnet, 0x765fb3, 0x765fb3 + 11, 3, 5, Finality::Root); +} + +#[test] +#[ignore = "requires over 32G memory and solc installed"] +fn test_mainnet_header_chain_aggregation_for_evm() { + header_tree_aggregation(Chain::Mainnet, 0x765fb3, 0x765fb3 + 11, 3, 5, Finality::Evm(0)); +} diff --git a/axiom-core/src/tests/mod.rs b/axiom-core/src/tests/mod.rs new file mode 100644 index 00000000..d07f84cf --- /dev/null +++ b/axiom-core/src/tests/mod.rs @@ -0,0 +1,103 @@ +use std::fs::File; + +use axiom_eth::{ + block_header::get_block_header_extra_bytes, + halo2_base::{ + gates::circuit::CircuitBuilderStage::{self, Keygen, Mock, Prover}, + utils::fs::gen_srs, + }, + halo2_proofs::{dev::MockProver, plonk::Circuit}, + halo2curves::bn256::Fr, + rlc::circuit::RlcCircuitParams, + snark_verifier_sdk::{gen_pk, halo2::gen_snark_shplonk}, + utils::{keccak::decorator::RlcKeccakCircuitParams, DEFAULT_RLC_CACHE_BITS}, +}; +use ethers_core::types::Chain; +use hex::FromHex; +use itertools::Itertools; +use test_log::test; + +use super::header_chain::*; + +pub mod chain_instance; +pub mod integration; + +fn get_rlc_keccak_params(path: &str) -> RlcKeccakCircuitParams { + serde_json::from_reader(File::open(path).unwrap()).unwrap() +} + +#[allow(dead_code)] +fn get_dummy_rlc_keccak_params(k: usize) -> RlcKeccakCircuitParams { + let mut rlc = RlcCircuitParams::default(); + rlc.base.k = k; + rlc.base.lookup_bits = Some(8); + rlc.base.num_instance_columns = 1; + RlcKeccakCircuitParams { rlc, keccak_rows_per_round: 20 } +} + +fn get_default_goerli_header_chain_circuit( + stage: CircuitBuilderStage, + circuit_params: RlcKeccakCircuitParams, +) -> EthBlockHeaderChainCircuit { + let network = Chain::Goerli; + let max_extra_data_bytes = get_block_header_extra_bytes(network); + let blocks: Vec = + serde_json::from_reader(File::open("data/headers/default_blocks_goerli.json").unwrap()) + .unwrap(); + let header_rlp_encodings = blocks.into_iter().map(|s| Vec::from_hex(s).unwrap()).collect_vec(); + let max_depth = 3; + + let input = + EthBlockHeaderChainInput::new(header_rlp_encodings, 7, max_depth, max_extra_data_bytes); + EthBlockHeaderChainCircuit::new_impl(stage, input, circuit_params, DEFAULT_RLC_CACHE_BITS) +} + +#[test] +pub fn test_multi_goerli_header_mock() { + // let circuit_params = get_dummy_rlc_keccak_params(k); + let circuit_params = get_rlc_keccak_params("configs/tests/multi_block.json"); + let k = circuit_params.k() as u32; + + let mut circuit = get_default_goerli_header_chain_circuit(Mock, circuit_params); + circuit.calculate_params(); + let instances = circuit.instances(); + MockProver::run(k, &circuit, instances).unwrap().assert_satisfied(); + serde_json::to_writer_pretty( + File::create("configs/tests/multi_block.json").unwrap(), + &circuit.params(), + ) + .unwrap(); +} + +#[test] +pub fn test_header_instances_constrained() { + let circuit_params = get_rlc_keccak_params("configs/tests/multi_block.json"); + let k = circuit_params.k() as u32; + + let circuit = get_default_goerli_header_chain_circuit(Mock, circuit_params); + + assert!( + MockProver::run(k, &circuit, vec![vec![]]).unwrap().verify().is_err(), + "instances were not constrained" + ); +} + +#[test] +pub fn test_multi_goerli_header_prover() { + let circuit_params = get_rlc_keccak_params("configs/tests/multi_block.json"); + let k = circuit_params.k() as u32; + + let mut circuit = get_default_goerli_header_chain_circuit(Keygen, circuit_params); + circuit.calculate_params(); + + let params = gen_srs(k); + let pk = gen_pk(¶ms, &circuit, None); + let circuit_params = circuit.params(); + let break_points = circuit.break_points(); + drop(circuit); + + let circuit = get_default_goerli_header_chain_circuit(Prover, circuit_params) + .use_break_points(break_points); + // this does proof verification automatically + gen_snark_shplonk(¶ms, &pk, circuit, None::<&str>); +} diff --git a/axiom-core/src/types.rs b/axiom-core/src/types.rs new file mode 100644 index 00000000..6e60a7f4 --- /dev/null +++ b/axiom-core/src/types.rs @@ -0,0 +1,132 @@ +use axiom_eth::{ + halo2_base::gates::flex_gate::MultiPhaseThreadBreakPoints, + halo2curves::bn256::{Bn256, G1Affine}, + rlc::virtual_region::RlcThreadBreakPoints, + snark_verifier::{pcs::kzg::KzgDecidingKey, verifier::plonk::PlonkProtocol}, + utils::{ + build_utils::pinning::aggregation::{GenericAggParams, GenericAggPinning}, + keccak::decorator::RlcKeccakCircuitParams, + snark_verifier::{AggregationCircuitParams, Base64Bytes}, + }, +}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// Circuit parameters by node type +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] +pub struct CoreNodeParams { + /// Type of the node in the aggregation tree. + pub node_type: CoreNodeType, + /// The maximum number of block headers in the chain at this level of the tree is 2depth. + pub depth: usize, + /// The leaf layer of the aggregation starts with max number of block headers equal to 2initial_depth. + pub initial_depth: usize, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] +pub enum CoreNodeType { + /// Leaf(max_extra_data_bytes) + /// + /// Different chains (e.g., Goerli) can have different maximum number of bytes in the extra data field of the block header. + /// We configure the circuits differently based on this. + Leaf(usize), + /// Produces as many snarks as needed to fit the entire block number range, without any final processing. + Intermediate, + /// The block number range must fit within the specified max depth. + /// Produces a single final snark with the starting & ending block numbers, previous and last block hashes, + /// and merkle mountain range as output. + Root, + /// The block number range must fit within the specified max depth. `Evm(round)` performs `round + 1` + /// rounds of SNARK verification on the final `Root` circuit + Evm(usize), +} + +impl CoreNodeParams { + pub fn new(node_type: CoreNodeType, depth: usize, initial_depth: usize) -> Self { + assert!(depth >= initial_depth); + Self { node_type, depth, initial_depth } + } + + pub fn child(&self, max_extra_data_bytes: Option) -> Option { + match self.node_type { + CoreNodeType::Leaf(_) => None, + CoreNodeType::Intermediate | CoreNodeType::Root => { + assert!(self.depth > self.initial_depth); + if self.depth == self.initial_depth + 1 { + Some(Self::new( + CoreNodeType::Leaf( + max_extra_data_bytes.expect("must provide max_extra_data_bytes"), + ), + self.initial_depth, + self.initial_depth, + )) + } else { + Some(Self::new(CoreNodeType::Intermediate, self.depth - 1, self.initial_depth)) + } + } + CoreNodeType::Evm(round) => { + if round == 0 { + let node_type = if self.depth == self.initial_depth { + CoreNodeType::Leaf( + max_extra_data_bytes.expect("must provide max_extra_data_bytes"), + ) + } else { + CoreNodeType::Root + }; + Some(Self::new(node_type, self.depth, self.initial_depth)) + } else { + Some(Self::new(CoreNodeType::Evm(round - 1), self.depth, self.initial_depth)) + } + } + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CorePinningLeaf { + pub num_instance: Vec, + pub params: RlcKeccakCircuitParams, + pub break_points: RlcThreadBreakPoints, + /// g1 generator, g2 generator, s_g2 (s is generator of trusted setup). + /// Together with domain size `2^k`, this commits to the trusted setup used. + /// This is all that's needed to verify the final ecpairing check on the KZG proof. + pub dk: KzgDecidingKey, +} + +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CorePinningIntermediate { + /// Configuration parameters + pub params: AggregationCircuitParams, + /// PlonkProtocol of the children + #[serde_as(as = "Vec")] + pub to_agg: Vec>, + /// Number of instances in each instance column + pub num_instance: Vec, + /// Break points. Should only have phase0, so MultiPhase is extraneous. + pub break_points: MultiPhaseThreadBreakPoints, + /// g1 generator, g2 generator, s_g2 (s is generator of trusted setup). + /// Together with domain size `2^k`, this commits to the trusted setup used. + /// This is all that's needed to verify the final ecpairing check on the KZG proof. + pub dk: KzgDecidingKey, +} + +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CorePinningRoot { + /// Configuration parameters + pub params: RlcKeccakCircuitParams, + /// PlonkProtocol of the children + #[serde_as(as = "Vec")] + pub to_agg: Vec>, + /// Number of instances in each instance column + pub num_instance: Vec, + /// Break points. + pub break_points: RlcThreadBreakPoints, + /// g1 generator, g2 generator, s_g2 (s is generator of trusted setup). + /// Together with domain size `2^k`, this commits to the trusted setup used. + /// This is all that's needed to verify the final ecpairing check on the KZG proof. + pub dk: KzgDecidingKey, +} + +pub type CorePinningEvm = GenericAggPinning; diff --git a/axiom-eth/.env.example b/axiom-eth/.env.example index ae955fa8..76413210 100644 --- a/axiom-eth/.env.example +++ b/axiom-eth/.env.example @@ -1,2 +1,2 @@ # need to export for tests to work -export JSON_RPC_URL= +export JSON_RPC_URL= \ No newline at end of file diff --git a/axiom-eth/.gitignore b/axiom-eth/.gitignore index b7317614..730093f4 100644 --- a/axiom-eth/.gitignore +++ b/axiom-eth/.gitignore @@ -1,4 +1,6 @@ **/target Cargo.lock **/*.rs.bk -*~ \ No newline at end of file +*~ +.env +/params \ No newline at end of file diff --git a/axiom-eth/Cargo.toml b/axiom-eth/Cargo.toml index 62378336..6e0eaa28 100644 --- a/axiom-eth/Cargo.toml +++ b/axiom-eth/Cargo.toml @@ -1,68 +1,97 @@ [package] name = "axiom-eth" -version = "0.2.0" +version = "0.4.0" +authors = ["Intrinsic Technologies"] +license = "MIT" edition = "2021" - -[[bin]] -name = "header_chain" -required-features = ["aggregation", "clap"] +repository = "https://github.com/axiom-crypto/axiom-eth" +readme = "README.md" +description = "This crate is the main library for building ZK circuits that prove data about the Ethereum virtual machine (EVM)." +rust-version = "1.73.0" [dependencies] -itertools = "0.10" +itertools = "0.11" lazy_static = "1.4.0" serde = { version = "1.0", default-features = false, features = ["derive"] } serde_json = { version = "1.0", default-features = false } -rayon = "1.7" - +serde_with = { version = "3.3", features = ["base64"], optional = true } +bincode = { version = "1.3.3" } +rayon = "1.8" # misc log = "0.4" env_logger = "0.10" ark-std = { version = "0.3.0", features = ["print-trace"], optional = true } -clap = { version = "4.0.13", features = ["derive"], optional = true } -clap-num = { version = "1.0.2", optional = true } -bincode = { version = "1.3.3", optional = true } -base64 = { version = "0.21", optional = true } -serde_with = { version = "2.2", optional = true } +getset = "0.1.2" +thiserror = "1" +anyhow = "1.0" +static_assertions = "1.1.0" +type-map = "0.5.0" # halo2 -ff = "0.12" -halo2-base = { git = "https://github.com/axiom-crypto/halo2-lib.git", tag = "v0.3.0", default-features = false } -zkevm-keccak = { git = "https://github.com/axiom-crypto/halo2-lib.git", tag = "v0.3.0", default-features = false } -# macro -any_circuit_derive = { path = "../any_circuit_derive", optional = true } +halo2-base = { version = "=0.4.1", default-features = false, features = ["test-utils"] } +# halo2-base = { git = "https://github.com/axiom-crypto/halo2-lib.git", tag = "v0.4.1-git", default-features = false, features = ["test-utils"] } +zkevm-hashes = { version = "=0.2.1", default-features = false } +# zkevm-hashes = { git = "https://github.com/axiom-crypto/halo2-lib.git", tag = "v0.4.1-git", default-features = false } # crypto rlp = "=0.5.2" -ethers-core = { version = "=2.0.6" } +ethers-core = { version = "2.0.10" } +# mpt implementation +hasher = { version = "=0.1", features = ["hash-keccak"] } +cita_trie = "=5.0.0" +num-bigint = { version = "0.4" } +sha3 = "0.10.6" +# rand rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } rand = "0.8" rand_chacha = "0.3.1" -# aggregation -snark-verifier = { git = "https://github.com/axiom-crypto/snark-verifier.git", tag = "v0.1.1", default-features = false, features = ["loader_halo2"], optional = true } -snark-verifier-sdk = { git = "https://github.com/axiom-crypto/snark-verifier.git", tag = "v0.1.1", default-features = false, features = ["loader_halo2"], optional = true } +# aggregation +snark-verifier = { version = "=0.1.7", default-features = false } +# snark-verifier = { git = "https://github.com/axiom-crypto/snark-verifier.git", tag = "v0.1.7-git", default-features = false } +snark-verifier-sdk = { version = "=0.1.7", default-features = false } +# snark-verifier-sdk = { git = "https://github.com/axiom-crypto/snark-verifier.git", tag = "v0.1.7-git", default-features = false } # generating circuit inputs from blockchain -ethers-providers = { version = "=2.0.6", optional = true } -tokio = { version = "=1.28", default-features = false, features = ["rt", "rt-multi-thread"], optional = true } -futures = { version = "=0.3", optional = true } +ethers-providers = { version = "2.0.10", optional = true } +tokio = { version = "1.28", default-features = false, features = ["rt", "rt-multi-thread", "macros"], optional = true } +futures = { version = "0.3", optional = true } + +# keygen +blake3 = { version = "=1.5", optional = true } [dev-dependencies] hex = "0.4.3" ark-std = { version = "0.3.0", features = ["print-trace"] } log = "0.4" test-log = "0.2.11" +pprof = { version = "0.11", features = ["criterion", "flamegraph"] } +criterion = "0.4" +criterion-macro = "0.4" +rayon = "1.6.1" +test-case = "3.1.0" +proptest = "1.1.0" +hasher = { version = "0.1", features = ["hash-keccak"] } [features] -default = ["halo2-axiom", "jemallocator", "display", "aggregation", "clap", "evm"] -aggregation = ["dep:snark-verifier", "snark-verifier-sdk", "providers", "dep:any_circuit_derive"] -evm = ["snark-verifier-sdk?/loader_evm", "aggregation"] -providers = ["dep:ethers-providers", "dep:tokio", "dep:bincode", "dep:base64", "dep:serde_with", "dep:futures"] -display = ["zkevm-keccak/display", "snark-verifier-sdk?/display", "dep:ark-std"] -clap = ["dep:clap", "dep:clap-num"] -# 'production' feature turns off circuit auto-configuration and forces trusted setup SRS to be read (and not generated) -production = [] +default = [ + "halo2-axiom", + "jemallocator", + "providers", + "display", + "aggregation", + "keygen", + # "evm" +] +aggregation = ["snark-verifier/loader_halo2", "snark-verifier-sdk/loader_halo2", "providers", "dep:serde_with"] +evm = ["snark-verifier-sdk/loader_evm", "aggregation"] +revm = ["snark-verifier-sdk/revm"] +providers = ["dep:ethers-providers", "dep:tokio", "dep:futures"] +display = ["zkevm-hashes/display", "snark-verifier-sdk/display", "dep:ark-std"] # EXACTLY one of halo2-pse / halo2-axiom should always be turned on -halo2-pse = ["zkevm-keccak/halo2-pse", "snark-verifier-sdk?/halo2-pse"] -halo2-axiom = ["zkevm-keccak/halo2-axiom", "snark-verifier-sdk?/halo2-axiom"] +# halo2-pse = ["zkevm-hashes/halo2-pse", "snark-verifier-sdk/halo2-pse"] +halo2-axiom = ["zkevm-hashes/halo2-axiom", "snark-verifier-sdk/halo2-axiom"] +keygen = ["aggregation", "dep:blake3"] jemallocator = ["halo2-base/jemallocator"] +asm = ["halo2-base/asm"] +profile = ["halo2-base/profile"] diff --git a/axiom-eth/README.md b/axiom-eth/README.md index e69de29b..85f4cefd 100644 --- a/axiom-eth/README.md +++ b/axiom-eth/README.md @@ -0,0 +1,77 @@ +# `axiom-eth` + +This crate is the main library for building ZK circuits that prove data about the Ethereum virtual machine (EVM). + +## Modules + +### Primitives + +The rest of this crate makes heavy use of the following primitives: + +- [`halo2-base`](https://github.com/axiom-crypto/halo2-lib/tree/main/halo2-base) Specifically the `BaseCircuitBuilder` and `LookupAnyManager`. +- `rlc`: In this crate we introduce a new custom gate, which is a vertical gate (1 advice column, 1 selector column) using 3 rotations. + This is in [`PureRlcConfig`](./src/rlc/circuit/mod.rs). We extend `BaseCircuitBuilder` to support this gate in [`RlcCircuitBuilder`](./src/rlc/circuit/builder.rs). + `RlcCircuitBuilder` is the core circuit builder that is used in the rest of this crate. +- `keccak`: The keccak hash plays a very special role in the EVM, so it is heavily used in chips in this crate. The `keccak` module contains an **adapter** `KeccakChip` that + collects requests for keccak hashes of either fixed length or variable length byte arrays. It is very important to note that the `KeccakChip` does **not** explicitly constrain + the computation of these keccak hashes. The hash digests are returned as **unconstrained** witnesses. The verification of these hashes depends on the concrete `Circuit` + implementation that uses the `KeccakChip`. To that end, we now discuss the `Circuit` implementations in this crate. + +### `Circuit` implementations + +This crate has three main Halo2 `Circuit` trait implementations: + +- `RlcKeccakCircuitImpl`: this is the most direct circuit implementation. It creates a standalone circuit with a `RlcCircuitBuilder` managed sub-circuit as well as a vanilla zkEVM keccak sub-circuit. The keccak calls collected by `KeccakChip` are directly constrained against the output table from the zkEVM keccak sub-circuit. One creates such a circuit by implementing `EthCircuitInstructions`. +- `EthCircuitImpl`: this creates a circuit with a `RlcCircuitBuilder` managed sub-circuit. In addition, it loads a table of keccak hashes (and their inputs) as private witnesses and outputs a Poseidon hash of the table as a public output. The keccak calls collected by `KeccakChip` are dynamically looked up into the table of keccak hashes. One creates such a circuit by implementing `EthCircuitInstructions`. It is important to note that a proof generated by this circuit is **not** fully verified yet: the loaded keccak table must be verified by constraining the Poseidon hash of the table equals the public output of a `KeccakComponentShardCircuit` that actually proves the validity of the table. This must be done by an aggregation circuit. +- `ComponentCircuitImpl`: this is part of the [Component Framework](./src/utils/README.md). See there for more details. Note that the implementation of `EthCircuitImpl` is really a specialized version of `ComponentCircuitImpl` since it uses the same `PromiseLoader, PromiseCollector` pattern. + +### Data structures + +- `rlp`: this module defines `RlpChip`, which heavily uses RLCs to prove the correct encoding/decoding of RLP (Recursive Length Prefix) serialization. +- `mpt`: this module defines `MPTChip`, which uses `rlp` and `keccak` to prove inclusion and exclusion proofs of nodes in a Merkle Patricia Trie (MPT). + +### Ethereum execution layer (EL) chips + +- `block_header`: proves the decomposition of an RLP encoded block header (as bytes) into block header fields. Also computes the block hash by keccak hashing the block header. +- `storage`: proves account proofs into the state trie and storage proofs into the storage trie. +- `transaction`: proves inclusion of an RLP encoded transaction into the transaction trie. Also provides function to extract a specific field from a transaction. +- `receipt`: proves inclusion of an RLP encoded receipt into the receipt trie. Also provides function to extract a specific field from a receipt. +- `solidity`: proves the raw storage slot corresponding to a sequence of Solidity mapping keys. + +## Circuit Builders + +This is an abstract concept that we do not codify into a trait at the top level. +This crate builds heavily upon `halo2-base`, and specifically the use of [virtual region managers](https://github.com/axiom-crypto/halo2-lib/pull/123). Within this framework, circuits are written using circuit builders, which is some collection of virtual region managers. A circuit builder owns a collection of raw Halo2 columns and custom gates, but stores the logic of the circuit itself in some virtual state. The circuit builder may then do auto-configuration or optimizations based on this virtual state before assigning the raw columns and gate selectors into the raw Halo2 circuit. + +Therefore for each **phase** of the Halo2 Challenge API, there are two steps to writing a circuit: + +- Specifying the assignments to virtual regions within the circuit builder +- Assigning the raw columns and gate selectors into the raw Halo2 circuit + +In this crate we make heavy use of Random Linear Combinations (RLCs), which require **two** phases: phase0 and phase1. Phase0 corresponds to the `FirstPhase` struct in raw Halo2, and phase1 corresponds to the `SecondPhase` struct. Because the correct calculations of the virtual assignments in phase1 require a challenge value, phase1 virtual assignments _must_ be done after phase0 +Concretely, this means that one cannot fully pre-populate all virtual regions of your circuit builder ahead of time. Instead, you must specify a function for the virtual assignments to be done once the challenge value is received. + +The above motivates the definitions of the core traits in this module such as [CoreBuilder](./src/utils/component/circuit.rs) and [PromiseBuilder](./src/utils/component/circuit.rs). +These are traits that specify interfaces for variations of the functions: + +- `virtual_assign_phase0` +- `raw_synthesize_phase0` +- `virtual_assign_phase1` +- `raw_synthesize_phase1` + +These four functions are all called in sequence in the `Circuit::synthesize` trait implementation in raw Halo2, with the phase0 columns being commited to between `raw_synthesize_phase0` and `virtual_assign_phase1` so that the challenge values are availabe starting in `virtual_assign_phase1`. (The other technical distinction is that `virtual_assign_*` can be called outside of `Circuit::synthesize` without a `Layouter`, for any precomputation purposes.) + +Most of the time, one circuit builder can be re-used for different circuit implementations: +the `raw_synthesize_*` logic stays the same, and you only need to specify the `virtual_assign_*` instructions to quickly write new circuits. + +### Phase0 Payload + +Results/data from phase0 also needs to be passed to phase1 to "continue the computation". +Ideally one would use Rust closures to auto-infer these data to be passed between phases. However +due to some compiler limitations / possible instability, we have opted to require all data that is +passed from phase0 to phase1 to be explicitly declared in some kind of `Payload` struct. +These should always be viewed as an explicit implementation of a closure. + +## Component Framework + +See [README](./src/utils/README.md). diff --git a/axiom-eth/configs/bench/keccak.json b/axiom-eth/configs/bench/keccak.json deleted file mode 100644 index 28e8c256..00000000 --- a/axiom-eth/configs/bench/keccak.json +++ /dev/null @@ -1,154 +0,0 @@ -[ - { - "degree": 14, - "range_advice": [1, 7], - "num_rlc": 2, - "keccak_advice": 140, - "unusable_rows": 55, - "rows_per_round": 9 - }, - { - "degree": 14, - "range_advice": [1, 3], - "num_rlc": 1, - "keccak_advice": 65, - "unusable_rows": 59, - "rows_per_round": 25 - }, - { - "degree": 14, - "range_advice": [1, 2], - "num_rlc": 1, - "keccak_advice": 49, - "unusable_rows": 109, - "rows_per_round": 32 - }, - { - "degree": 14, - "range_advice": [1, 2], - "num_rlc": 1, - "keccak_advice": 46, - "unusable_rows": 109, - "rows_per_round": 50 - }, - { - "degree": 15, - "range_advice": [1, 7], - "num_rlc": 2, - "keccak_advice": 140, - "unusable_rows": 55, - "rows_per_round": 9 - }, - { - "degree": 15, - "range_advice": [1, 3], - "num_rlc": 1, - "keccak_advice": 65, - "unusable_rows": 59, - "rows_per_round": 25 - }, - { - "degree": 15, - "range_advice": [1, 2], - "num_rlc": 1, - "keccak_advice": 49, - "unusable_rows": 109, - "rows_per_round": 32 - }, - { - "degree": 15, - "range_advice": [1, 2], - "num_rlc": 1, - "keccak_advice": 46, - "unusable_rows": 109, - "rows_per_round": 50 - }, - { - "degree": 16, - "range_advice": [1, 3], - "num_rlc": 1, - "keccak_advice": 65, - "unusable_rows": 59, - "rows_per_round": 25 - }, - { - "degree": 16, - "range_advice": [1, 2], - "num_rlc": 1, - "keccak_advice": 49, - "unusable_rows": 109, - "rows_per_round": 32 - }, - { - "degree": 16, - "range_advice": [1, 2], - "num_rlc": 1, - "keccak_advice": 46, - "unusable_rows": 109, - "rows_per_round": 50 - }, - { - "degree": 16, - "range_advice": [1, 1], - "num_rlc": 1, - "keccak_advice": 29, - "unusable_rows": 109, - "rows_per_round": 75 - }, - { - "degree": 16, - "range_advice": [1, 1], - "num_rlc": 1, - "keccak_advice": 29, - "unusable_rows": 109, - "rows_per_round": 100 - }, - { - "degree": 17, - "range_advice": [1, 7], - "num_rlc": 2, - "keccak_advice": 123, - "unusable_rows": 55, - "rows_per_round": 9 - }, - { - "degree": 17, - "range_advice": [1, 3], - "num_rlc": 1, - "keccak_advice": 50, - "unusable_rows": 59, - "rows_per_round": 25 - }, - { - "degree": 17, - "range_advice": [1, 2], - "num_rlc": 1, - "keccak_advice": 47, - "unusable_rows": 109, - "rows_per_round": 32 - }, - { - "degree": 17, - "range_advice": [1, 2], - "num_rlc": 1, - "keccak_advice": 31, - "unusable_rows": 109, - "rows_per_round": 50 - }, - { - "degree": 17, - "range_advice": [1, 1], - "num_rlc": 1, - "keccak_advice": 29, - "unusable_rows": 109, - "rows_per_round": 75 - }, - { - "degree": 17, - "range_advice": [1, 1], - "num_rlc": 1, - "keccak_advice": 29, - "unusable_rows": 109, - "rows_per_round": 100 - } -] diff --git a/axiom-eth/configs/bench/mpt.json b/axiom-eth/configs/bench/mpt.json index 5de58641..fbfadc69 100644 --- a/axiom-eth/configs/bench/mpt.json +++ b/axiom-eth/configs/bench/mpt.json @@ -1,47 +1,56 @@ [ { - "degree": 15, - "num_rlc_columns": 2, - "num_range_advice": [18, 10, 0], - "num_lookup_advice": [1, 1, 0], - "num_fixed": 1, - "unusable_rows": 65, - "keccak_rows_per_round": 22 + "base": { + "k": 15, + "num_advice_per_phase": [ + 29, + 18 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 1, + 0 + ], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 2 }, { - "degree": 16, - "num_rlc_columns": 1, - "num_range_advice": [8, 5], - "num_lookup_advice": [1, 1], - "num_fixed": 1, - "unusable_rows": 109, - "keccak_rows_per_round": 45 + "base": { + "k": 16, + "num_advice_per_phase": [ + 15, + 9 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 1, + 0 + ], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 1 }, { - "degree": 17, - "num_rlc_columns": 1, - "num_range_advice": [4, 3], - "num_lookup_advice": [1, 1], - "num_fixed": 1, - "unusable_rows": 109, - "keccak_rows_per_round": 50 - }, - { - "degree": 17, - "num_rlc_columns": 1, - "num_range_advice": [4, 3], - "num_lookup_advice": [1, 1], - "num_fixed": 1, - "unusable_rows": 109, - "keccak_rows_per_round": 90 - }, - { - "degree": 18, - "num_rlc_columns": 1, - "num_range_advice": [2, 2], - "num_lookup_advice": [1, 1], - "num_fixed": 1, - "unusable_rows": 59, - "keccak_rows_per_round": 50 + "base": { + "k": 17, + "num_advice_per_phase": [ + 8, + 5 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 1, + 0 + ], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 1 } -] +] \ No newline at end of file diff --git a/axiom-eth/configs/bench/storage.json b/axiom-eth/configs/bench/storage.json index 4ca4289f..000b2597 100644 --- a/axiom-eth/configs/bench/storage.json +++ b/axiom-eth/configs/bench/storage.json @@ -1,38 +1,65 @@ [ [ { - "degree": 18, - "num_rlc_columns": 1, - "num_range_advice": [5, 3], - "num_lookup_advice": [1, 1], - "num_fixed": 1, - "unusable_rows": 69, - "keccak_rows_per_round": 50 + "base": { + "k": 15, + "num_advice_per_phase": [ + 92, + 58 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 1, + 0 + ], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 5 }, 1 ], [ { - "degree": 17, - "num_rlc_columns": 2, - "num_range_advice": [24, 14], - "num_lookup_advice": [1, 1], - "num_fixed": 1, - "unusable_rows": 55, - "keccak_rows_per_round": 17 + "base": { + "k": 18, + "num_advice_per_phase": [ + 56, + 39 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 1, + 0 + ], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 3 }, - 4 + 10 ], [ { - "degree": 18, - "num_rlc_columns": 2, - "num_range_advice": [26, 15], - "num_lookup_advice": [1, 1], - "num_fixed": 1, - "unusable_rows": 77, - "keccak_rows_per_round": 16 + "base": { + "k": 20, + "num_advice_per_phase": [ + 41, + 29 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 1, + 0 + ], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 3 }, - 10 + 32 ] -] +] \ No newline at end of file diff --git a/axiom-eth/configs/bench/storage_evm.json b/axiom-eth/configs/bench/storage_evm.json deleted file mode 100644 index e1e6c744..00000000 --- a/axiom-eth/configs/bench/storage_evm.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "degree": 22, - "num_advice": 5, - "num_lookup_advice": 1, - "num_fixed": 1, - "lookup_bits": 21 - }, - { - "degree": 23, - "num_advice": 8, - "num_lookup_advice": 1, - "num_fixed": 1, - "lookup_bits": 22 - }, - { - "degree": 24, - "num_advice": 3, - "num_lookup_advice": 1, - "num_fixed": 1, - "lookup_bits": 22 - } -] diff --git a/axiom-eth/configs/bench/transaction.json b/axiom-eth/configs/bench/transaction.json new file mode 100644 index 00000000..dd16b246 --- /dev/null +++ b/axiom-eth/configs/bench/transaction.json @@ -0,0 +1,65 @@ +[ + [ + { + "base": { + "k": 15, + "num_advice_per_phase": [ + 25, + 16 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 1, + 0 + ], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 2 + }, + 1 + ], + [ + { + "base": { + "k": 17, + "num_advice_per_phase": [ + 47, + 36 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 1, + 0 + ], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 3 + }, + 10 + ], + [ + { + "base": { + "k": 18, + "num_advice_per_phase": [ + 73, + 57 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 1, + 0 + ], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 5 + }, + 32 + ] +] \ No newline at end of file diff --git a/axiom-eth/configs/headers/goerli_3.json b/axiom-eth/configs/headers/goerli_3.json deleted file mode 100644 index 93b9cde6..00000000 --- a/axiom-eth/configs/headers/goerli_3.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "params": { - "degree": 15, - "num_rlc_columns": 2, - "num_range_advice": [ - 23, - 8, - 0 - ], - "num_lookup_advice": [ - 1, - 1, - 0 - ], - "num_fixed": 1, - "unusable_rows": 59, - "keccak_rows_per_round": 25, - "lookup_bits": 8 - }, - "break_points": { - "gate": [ - [ - 32707, - 32706, - 32707, - 32706, - 32707, - 32708, - 32706, - 32708, - 32708, - 32708, - 32708, - 32706, - 32708, - 32707, - 32708, - 32708, - 32708, - 32706, - 32707, - 32707, - 32708, - 32708 - ], - [ - 32706, - 32706, - 32707, - 32708, - 32708, - 32708, - 32706 - ], - [] - ], - "rlc": [ - 32708 - ] - } -} \ No newline at end of file diff --git a/axiom-eth/configs/headers/goerli_4_3.json b/axiom-eth/configs/headers/goerli_4_3.json deleted file mode 100644 index ffd4fcca..00000000 --- a/axiom-eth/configs/headers/goerli_4_3.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "params": { - "degree": 21, - "num_advice": 35, - "num_lookup_advice": 4, - "num_fixed": 1, - "lookup_bits": 20 - }, - "break_points": [ - [ - 2097141, - 2097142, - 2097142, - 2097140, - 2097142, - 2097140, - 2097140, - 2097140, - 2097142, - 2097140, - 2097140, - 2097142, - 2097141, - 2097142, - 2097142, - 2097140, - 2097140, - 2097140, - 2097141, - 2097141, - 2097141, - 2097141, - 2097140, - 2097142, - 2097142, - 2097141, - 2097142, - 2097140, - 2097142, - 2097142, - 2097141, - 2097140, - 2097142, - 2097142 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/headers/goerli_4_3_final.json b/axiom-eth/configs/headers/goerli_4_3_final.json deleted file mode 100644 index 66d8844f..00000000 --- a/axiom-eth/configs/headers/goerli_4_3_final.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "params": { - "degree": 21, - "num_rlc_columns": 1, - "num_range_advice": [ - 48, - 0, - 0 - ], - "num_lookup_advice": [ - 8, - 0, - 0 - ], - "num_fixed": 1, - "unusable_rows": 109, - "keccak_rows_per_round": 50, - "lookup_bits": 8 - }, - "break_points": { - "gate": [ - [ - 2097040, - 2097041, - 2097040, - 2097042, - 2097041, - 2097041, - 2097041, - 2097040, - 2097042, - 2097041, - 2097040, - 2097041, - 2097042, - 2097041, - 2097042, - 2097042, - 2097040, - 2097041, - 2097041, - 2097042, - 2097042, - 2097042, - 2097042, - 2097042, - 2097042, - 2097041, - 2097041, - 2097040, - 2097042, - 2097042, - 2097040, - 2097041, - 2097040, - 2097040, - 2097040, - 2097042, - 2097041, - 2097040, - 2097041, - 2097042, - 2097042, - 2097041, - 2097041, - 2097040, - 2097041, - 2097042, - 2097042 - ], - [], - [] - ], - "rlc": [] - } -} \ No newline at end of file diff --git a/axiom-eth/configs/headers/goerli_4_3_for_evm_0.json b/axiom-eth/configs/headers/goerli_4_3_for_evm_0.json deleted file mode 100644 index 7af89dd4..00000000 --- a/axiom-eth/configs/headers/goerli_4_3_for_evm_0.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "params": { - "degree": 23, - "num_advice": 4, - "num_lookup_advice": 1, - "num_fixed": 1, - "lookup_bits": 22 - }, - "break_points": [ - [ - 8388598, - 8388598, - 8388596 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_10_7.json b/axiom-eth/configs/headers/mainnet_10_7.json deleted file mode 100644 index db678022..00000000 --- a/axiom-eth/configs/headers/mainnet_10_7.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "params": { - "degree": 20, - "num_advice": 15, - "num_lookup_advice": 2, - "num_fixed": 1, - "lookup_bits": 19 - }, - "break_points": [ - [ - 1048566, - 1048565, - 1048564, - 1048564, - 1048564, - 1048566, - 1048566, - 1048565, - 1048566, - 1048566, - 1048566, - 1048565, - 1048565, - 1048565 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_10_7_final.json b/axiom-eth/configs/headers/mainnet_10_7_final.json deleted file mode 100644 index 0fe47712..00000000 --- a/axiom-eth/configs/headers/mainnet_10_7_final.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "params": { - "degree": 19, - "num_rlc_columns": 1, - "num_range_advice": [ - 30, - 0, - 0 - ], - "num_lookup_advice": [ - 4, - 0, - 0 - ], - "num_fixed": 1, - "unusable_rows": 109, - "keccak_rows_per_round": 50, - "lookup_bits": 18 - }, - "break_points": { - "gate": [ - [ - 524178, - 524177, - 524178, - 524176, - 524177, - 524177, - 524176, - 524176, - 524177, - 524177, - 524176, - 524178, - 524178, - 524177, - 524178, - 524178, - 524178, - 524178, - 524176, - 524177, - 524178, - 524177, - 524177, - 524178, - 524177, - 524178, - 524176, - 524178, - 524178 - ], - [], - [] - ], - "rlc": [] - } -} \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_10_7_for_evm_0.json b/axiom-eth/configs/headers/mainnet_10_7_for_evm_0.json deleted file mode 100644 index 45d965b2..00000000 --- a/axiom-eth/configs/headers/mainnet_10_7_for_evm_0.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "params": { - "degree": 22, - "num_advice": 7, - "num_lookup_advice": 1, - "num_fixed": 1, - "lookup_bits": 20 - }, - "break_points": [ - [ - 4194292, - 4194294, - 4194294, - 4194294, - 4194292, - 4194292 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_10_7_for_evm_1.json b/axiom-eth/configs/headers/mainnet_10_7_for_evm_1.json deleted file mode 100644 index b3e69eea..00000000 --- a/axiom-eth/configs/headers/mainnet_10_7_for_evm_1.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "params": { - "degree": 23, - "num_advice": 1, - "num_lookup_advice": 1, - "num_fixed": 1, - "lookup_bits": 22 - }, - "break_points": [ - [], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_11_7.json b/axiom-eth/configs/headers/mainnet_11_7.json deleted file mode 100644 index d3af7017..00000000 --- a/axiom-eth/configs/headers/mainnet_11_7.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "params": { - "degree": 20, - "num_advice": 19, - "num_lookup_advice": 3, - "num_fixed": 1, - "lookup_bits": 19 - }, - "break_points": [ - [ - 1048565, - 1048565, - 1048564, - 1048565, - 1048564, - 1048564, - 1048565, - 1048565, - 1048566, - 1048565, - 1048566, - 1048564, - 1048565, - 1048565, - 1048565, - 1048564, - 1048564, - 1048566 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_12_7.json b/axiom-eth/configs/headers/mainnet_12_7.json deleted file mode 100644 index f45235fd..00000000 --- a/axiom-eth/configs/headers/mainnet_12_7.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "params": { - "degree": 21, - "num_advice": 12, - "num_lookup_advice": 2, - "num_fixed": 1, - "lookup_bits": 20 - }, - "break_points": [ - [ - 2097141, - 2097140, - 2097142, - 2097142, - 2097141, - 2097140, - 2097142, - 2097140, - 2097140, - 2097141, - 2097140 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_15_7.json b/axiom-eth/configs/headers/mainnet_15_7.json deleted file mode 100644 index 0616207f..00000000 --- a/axiom-eth/configs/headers/mainnet_15_7.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "params": { - "degree": 21, - "num_advice": 13, - "num_lookup_advice": 2, - "num_fixed": 1, - "lookup_bits": 20 - }, - "break_points": [ - [ - 2097142, - 2097142, - 2097142, - 2097140, - 2097140, - 2097142, - 2097141, - 2097142, - 2097142, - 2097141, - 2097142, - 2097142 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_17_7_final.json b/axiom-eth/configs/headers/mainnet_17_7_final.json deleted file mode 100644 index c5a8f649..00000000 --- a/axiom-eth/configs/headers/mainnet_17_7_final.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "params": { - "degree": 22, - "num_rlc_columns": 1, - "num_range_advice": [ - 7, - 0, - 0 - ], - "num_lookup_advice": [ - 1, - 0, - 0 - ], - "num_fixed": 1, - "unusable_rows": 109, - "keccak_rows_per_round": 50, - "lookup_bits": 21 - }, - "break_points": { - "gate": [ - [ - 4194194, - 4194192, - 4194194, - 4194194, - 4194192, - 4194192 - ], - [], - [] - ], - "rlc": [] - } -} \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_17_7_for_evm_0.json b/axiom-eth/configs/headers/mainnet_17_7_for_evm_0.json deleted file mode 100644 index 96bdd702..00000000 --- a/axiom-eth/configs/headers/mainnet_17_7_for_evm_0.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "params": { - "degree": 22, - "num_advice": 4, - "num_lookup_advice": 1, - "num_fixed": 1, - "lookup_bits": 21 - }, - "break_points": [ - [ - 4194293, - 4194292, - 4194293 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_7.json b/axiom-eth/configs/headers/mainnet_7.json deleted file mode 100644 index d2e043d4..00000000 --- a/axiom-eth/configs/headers/mainnet_7.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "params": { - "degree": 19, - "num_rlc_columns": 1, - "num_range_advice": [ - 22, - 8, - 0 - ], - "num_lookup_advice": [ - 1, - 1, - 0 - ], - "num_fixed": 1, - "unusable_rows": 63, - "keccak_rows_per_round": 23, - "lookup_bits": 8 - }, - "break_points": { - "gate": [ - [ - 524222, - 524223, - 524223, - 524223, - 524223, - 524223, - 524224, - 524223, - 524222, - 524223, - 524223, - 524223, - 524224, - 524223, - 524224, - 524223, - 524224, - 524224, - 524222, - 524224, - 524224 - ], - [ - 524224, - 524222, - 524222, - 524224, - 524222, - 524224, - 524222 - ], - [] - ], - "rlc": [] - } -} \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_8_7.json b/axiom-eth/configs/headers/mainnet_8_7.json deleted file mode 100644 index 26662ebe..00000000 --- a/axiom-eth/configs/headers/mainnet_8_7.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "params": { - "degree": 22, - "num_advice": 16, - "num_lookup_advice": 2, - "num_fixed": 1, - "lookup_bits": 21 - }, - "break_points": [ - [ - 4194294, - 4194293, - 4194292, - 4194294, - 4194293, - 4194292, - 4194293, - 4194293, - 4194294, - 4194292, - 4194294, - 4194293, - 4194294, - 4194292, - 4194294 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_9_7.json b/axiom-eth/configs/headers/mainnet_9_7.json deleted file mode 100644 index 9bef5275..00000000 --- a/axiom-eth/configs/headers/mainnet_9_7.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "params": { - "degree": 21, - "num_advice": 10, - "num_lookup_advice": 2, - "num_fixed": 1, - "lookup_bits": 20 - }, - "break_points": [ - [ - 2097142, - 2097142, - 2097140, - 2097142, - 2097142, - 2097142, - 2097142, - 2097140, - 2097141 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/storage/mainnet_10.json b/axiom-eth/configs/storage/mainnet_10.json deleted file mode 100644 index 95a3c953..00000000 --- a/axiom-eth/configs/storage/mainnet_10.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "params": { - "degree": 19, - "num_rlc_columns": 2, - "num_range_advice": [ - 17, - 10, - 0 - ], - "num_lookup_advice": [ - 1, - 1, - 0 - ], - "num_fixed": 1, - "unusable_rows": 77, - "keccak_rows_per_round": 25, - "lookup_bits": 8 - }, - "break_points": { - "gate": [ - [ - 524228, - 524227, - 524228, - 524228, - 524228, - 524226, - 524228, - 524228, - 524228, - 524228, - 524227, - 524228, - 524226, - 524226, - 524226, - 524226 - ], - [ - 524228, - 524228, - 524226, - 524226, - 524228, - 524228, - 524228, - 524228, - 524228 - ], - [] - ], - "rlc": [ - 524228 - ] - } -} \ No newline at end of file diff --git a/axiom-eth/configs/storage/mainnet_10_evm.json b/axiom-eth/configs/storage/mainnet_10_evm.json deleted file mode 100644 index afc20005..00000000 --- a/axiom-eth/configs/storage/mainnet_10_evm.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "params": { - "degree": 24, - "num_advice": 2, - "num_lookup_advice": 1, - "num_fixed": 1, - "lookup_bits": 22 - }, - "break_points": [ - [ - 16777205 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/account_query.json b/axiom-eth/configs/tests/account_query.json deleted file mode 100644 index d7ad86cb..00000000 --- a/axiom-eth/configs/tests/account_query.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "degree": 18, - "num_rlc_columns": 3, - "num_range_advice": [40, 23], - "num_lookup_advice": [1, 1], - "num_fixed": 1, - "unusable_rows": 69, - "keccak_rows_per_round": 10 -} diff --git a/axiom-eth/configs/tests/batch_query/account_3.json b/axiom-eth/configs/tests/batch_query/account_3.json deleted file mode 100644 index 14d90640..00000000 --- a/axiom-eth/configs/tests/batch_query/account_3.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "params": { - "degree": 19, - "num_rlc_columns": 1, - "num_range_advice": [ - 13, - 7, - 0 - ], - "num_lookup_advice": [ - 1, - 1, - 0 - ], - "num_fixed": 1, - "unusable_rows": 87, - "keccak_rows_per_round": 34, - "lookup_bits": 8 - }, - "break_points": { - "gate": [ - [ - 524178, - 524177, - 524178, - 524178, - 524176, - 524176, - 524177, - 524176, - 524177, - 524178, - 524178, - 524177 - ], - [ - 524176, - 524178, - 524176, - 524178, - 524178, - 524177 - ], - [] - ], - "rlc": [] - } -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/account_3_6_1.json b/axiom-eth/configs/tests/batch_query/account_3_6_1.json deleted file mode 100644 index d93a7d9b..00000000 --- a/axiom-eth/configs/tests/batch_query/account_3_6_1.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "params": { - "degree": 23, - "num_advice": 4, - "num_lookup_advice": 1, - "num_fixed": 1, - "lookup_bits": 22 - }, - "break_points": [ - [ - 8388598, - 8388597, - 8388598 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/account_3_6_2.json b/axiom-eth/configs/tests/batch_query/account_3_6_2.json deleted file mode 100644 index 20b4ebb5..00000000 --- a/axiom-eth/configs/tests/batch_query/account_3_6_2.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "params": { - "degree": 23, - "num_advice": 7, - "num_lookup_advice": 1, - "num_fixed": 1, - "lookup_bits": 22 - }, - "break_points": [ - [ - 8388598, - 8388598, - 8388597, - 8388598, - 8388597, - 8388597 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/account_4.json b/axiom-eth/configs/tests/batch_query/account_4.json deleted file mode 100644 index 6dea15fe..00000000 --- a/axiom-eth/configs/tests/batch_query/account_4.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "params": { - "degree": 20, - "num_rlc_columns": 1, - "num_range_advice": [ - 13, - 7, - 0 - ], - "num_lookup_advice": [ - 1, - 1, - 0 - ], - "num_fixed": 1, - "unusable_rows": 87, - "keccak_rows_per_round": 34, - "lookup_bits": 8 - }, - "break_points": { - "gate": [ - [ - 1048465, - 1048464, - 1048466, - 1048466, - 1048465, - 1048465, - 1048465, - 1048465, - 1048464, - 1048465, - 1048466, - 1048466 - ], - [ - 1048465, - 1048464, - 1048464, - 1048466, - 1048466, - 1048466 - ], - [] - ], - "rlc": [] - } -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/account_5.json b/axiom-eth/configs/tests/batch_query/account_5.json deleted file mode 100644 index 00f447b6..00000000 --- a/axiom-eth/configs/tests/batch_query/account_5.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "params": { - "degree": 21, - "num_rlc_columns": 1, - "num_range_advice": [ - 13, - 7, - 0 - ], - "num_lookup_advice": [ - 1, - 1, - 0 - ], - "num_fixed": 1, - "unusable_rows": 59, - "keccak_rows_per_round": 34, - "lookup_bits": 8 - }, - "break_points": { - "gate": [ - [ - 2097040, - 2097042, - 2097041, - 2097040, - 2097040, - 2097041, - 2097042, - 2097042, - 2097042, - 2097041, - 2097040, - 2097042 - ], - [ - 2097040, - 2097041, - 2097042, - 2097042, - 2097042, - 2097040 - ], - [] - ], - "rlc": [] - } -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/final_0.json b/axiom-eth/configs/tests/batch_query/final_0.json deleted file mode 100644 index fc3c50ea..00000000 --- a/axiom-eth/configs/tests/batch_query/final_0.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "params": { - "degree": 19, - "num_rlc_columns": 1, - "num_range_advice": [ - 97, - 0, - 0 - ], - "num_lookup_advice": [ - 12, - 0, - 0 - ], - "num_fixed": 2, - "unusable_rows": 109, - "keccak_rows_per_round": 50, - "lookup_bits": 18 - }, - "break_points": { - "gate": [ - [ - 524176, - 524177, - 524178, - 524178, - 524177, - 524176, - 524178, - 524178, - 524178, - 524178, - 524178, - 524176, - 524176, - 524178, - 524178, - 524178, - 524177, - 524178, - 524176, - 524177, - 524177, - 524177, - 524176, - 524177, - 524176, - 524178, - 524176, - 524178, - 524176, - 524177, - 524176, - 524177, - 524176, - 524176, - 524177, - 524176, - 524177, - 524177, - 524176, - 524176, - 524177, - 524178, - 524176, - 524178, - 524176, - 524178, - 524178, - 524176, - 524178, - 524176, - 524178, - 524178, - 524177, - 524177, - 524176, - 524178, - 524177, - 524178, - 524176, - 524178, - 524177, - 524178, - 524177, - 524176, - 524176, - 524178, - 524176, - 524176, - 524178, - 524177, - 524176, - 524176, - 524177, - 524178, - 524177, - 524176, - 524177, - 524177, - 524178, - 524176, - 524177, - 524176, - 524176, - 524176, - 524178, - 524178, - 524178, - 524177, - 524177, - 524177, - 524177, - 524176, - 524178, - 524176, - 524176, - 524176 - ], - [], - [] - ], - "rlc": [] - } -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/final_1.json b/axiom-eth/configs/tests/batch_query/final_1.json deleted file mode 100644 index f7c8e6aa..00000000 --- a/axiom-eth/configs/tests/batch_query/final_1.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "params": { - "degree": 21, - "num_advice": 28, - "num_lookup_advice": 3, - "num_fixed": 1, - "lookup_bits": 20 - }, - "break_points": [ - [ - 2097141, - 2097142, - 2097140, - 2097142, - 2097140, - 2097142, - 2097140, - 2097142, - 2097141, - 2097142, - 2097141, - 2097140, - 2097142, - 2097140, - 2097141, - 2097141, - 2097140, - 2097141, - 2097142, - 2097142, - 2097142, - 2097142, - 2097141, - 2097141, - 2097142, - 2097140, - 2097140 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/final_2.json b/axiom-eth/configs/tests/batch_query/final_2.json deleted file mode 100644 index 017f94d2..00000000 --- a/axiom-eth/configs/tests/batch_query/final_2.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "params": { - "degree": 22, - "num_advice": 4, - "num_lookup_advice": 1, - "num_fixed": 1, - "lookup_bits": 21 - }, - "break_points": [ - [ - 4194294, - 4194293, - 4194294 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/mainnet_block_5.json b/axiom-eth/configs/tests/batch_query/mainnet_block_5.json deleted file mode 100644 index c7f2152a..00000000 --- a/axiom-eth/configs/tests/batch_query/mainnet_block_5.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "params": { - "degree": 19, - "num_rlc_columns": 1, - "num_range_advice": [ - 17, - 2, - 0 - ], - "num_lookup_advice": [ - 1, - 1, - 0 - ], - "num_fixed": 1, - "unusable_rows": 63, - "keccak_rows_per_round": 17, - "lookup_bits": 8 - }, - "break_points": { - "gate": [ - [ - 524212, - 524212, - 524211, - 524211, - 524211, - 524210, - 524212, - 524212, - 524210, - 524210, - 524212, - 524212, - 524210, - 524210, - 524212, - 524210 - ], - [ - 524212 - ], - [] - ], - "rlc": [] - } -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/mainnet_block_5_1.json b/axiom-eth/configs/tests/batch_query/mainnet_block_5_1.json deleted file mode 100644 index 0286bb36..00000000 --- a/axiom-eth/configs/tests/batch_query/mainnet_block_5_1.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "params": { - "degree": 21, - "num_advice": 33, - "num_lookup_advice": 4, - "num_fixed": 1, - "lookup_bits": 20 - }, - "break_points": [ - [ - 2097141, - 2097140, - 2097142, - 2097141, - 2097142, - 2097142, - 2097140, - 2097140, - 2097141, - 2097140, - 2097142, - 2097142, - 2097142, - 2097142, - 2097142, - 2097142, - 2097141, - 2097141, - 2097140, - 2097142, - 2097142, - 2097142, - 2097141, - 2097140, - 2097142, - 2097142, - 2097142, - 2097142, - 2097142, - 2097140, - 2097140, - 2097141 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/mainnet_block_6.json b/axiom-eth/configs/tests/batch_query/mainnet_block_6.json deleted file mode 100644 index 92af3e1d..00000000 --- a/axiom-eth/configs/tests/batch_query/mainnet_block_6.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "params": { - "degree": 20, - "num_rlc_columns": 1, - "num_range_advice": [ - 17, - 2, - 0 - ], - "num_lookup_advice": [ - 1, - 1, - 0 - ], - "num_fixed": 1, - "unusable_rows": 63, - "keccak_rows_per_round": 17, - "lookup_bits": 8 - }, - "break_points": { - "gate": [ - [ - 1048499, - 1048499, - 1048498, - 1048498, - 1048498, - 1048498, - 1048500, - 1048498, - 1048500, - 1048498, - 1048499, - 1048499, - 1048499, - 1048500, - 1048500, - 1048500 - ], - [ - 1048500 - ], - [] - ], - "rlc": [] - } -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/row_6.json b/axiom-eth/configs/tests/batch_query/row_6.json deleted file mode 100644 index a6a2aac1..00000000 --- a/axiom-eth/configs/tests/batch_query/row_6.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "params": { - "degree": 22, - "num_advice": 2, - "num_lookup_advice": 0, - "num_fixed": 1, - "lookup_bits": 0 - }, - "break_points": [ - [ - 4194294 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/schema.account.json b/axiom-eth/configs/tests/batch_query/schema.account.json deleted file mode 100644 index d29666a5..00000000 --- a/axiom-eth/configs/tests/batch_query/schema.account.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "start_arity": 4, - "total_arity": 7, - "level": 0 -} diff --git a/axiom-eth/configs/tests/batch_query/schema.block.json b/axiom-eth/configs/tests/batch_query/schema.block.json deleted file mode 100644 index b69c2e72..00000000 --- a/axiom-eth/configs/tests/batch_query/schema.block.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "network": "Mainnet", - "arities": [5,1] -} diff --git a/axiom-eth/configs/tests/batch_query/schema.final.json b/axiom-eth/configs/tests/batch_query/schema.final.json deleted file mode 100644 index c9aa4dbf..00000000 --- a/axiom-eth/configs/tests/batch_query/schema.final.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "round": 2, - "network": "Mainnet", - "block_arities": [6], - "account_schema": { - "start_arity": 3, - "total_arity": 6, - "level": 0 - }, - "storage_schema": { - "start_arity": 3, - "total_arity": 6, - "level": 0 - }, - "row_schema": { - "start_arity": 6, - "total_arity": 6, - "level": 0 - } -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/schema.row.json b/axiom-eth/configs/tests/batch_query/schema.row.json deleted file mode 100644 index e8ce97cd..00000000 --- a/axiom-eth/configs/tests/batch_query/schema.row.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "arities": [7], - "merklelize": [true] -} diff --git a/axiom-eth/configs/tests/batch_query/schema.storage.json b/axiom-eth/configs/tests/batch_query/schema.storage.json deleted file mode 100644 index d29666a5..00000000 --- a/axiom-eth/configs/tests/batch_query/schema.storage.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "start_arity": 4, - "total_arity": 7, - "level": 0 -} diff --git a/axiom-eth/configs/tests/batch_query/storage_3.json b/axiom-eth/configs/tests/batch_query/storage_3.json deleted file mode 100644 index dd2ce077..00000000 --- a/axiom-eth/configs/tests/batch_query/storage_3.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "params": { - "degree": 19, - "num_rlc_columns": 1, - "num_range_advice": [ - 11, - 6, - 0 - ], - "num_lookup_advice": [ - 1, - 1, - 0 - ], - "num_fixed": 1, - "unusable_rows": 63, - "keccak_rows_per_round": 38, - "lookup_bits": 8 - }, - "break_points": { - "gate": [ - [ - 524178, - 524178, - 524178, - 524176, - 524178, - 524178, - 524176, - 524178, - 524176, - 524176 - ], - [ - 524178, - 524178, - 524176, - 524178, - 524178 - ], - [] - ], - "rlc": [] - } -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/storage_3_6_0.json b/axiom-eth/configs/tests/batch_query/storage_3_6_0.json deleted file mode 100644 index c53ca840..00000000 --- a/axiom-eth/configs/tests/batch_query/storage_3_6_0.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "params": { - "degree": 22, - "num_advice": 8, - "num_lookup_advice": 1, - "num_fixed": 1, - "lookup_bits": 21 - }, - "break_points": [ - [ - 4194294, - 4194294, - 4194294, - 4194292, - 4194292, - 4194293, - 4194293 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/storage_3_6_2.json b/axiom-eth/configs/tests/batch_query/storage_3_6_2.json deleted file mode 100644 index 8973f290..00000000 --- a/axiom-eth/configs/tests/batch_query/storage_3_6_2.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "params": { - "degree": 23, - "num_advice": 7, - "num_lookup_advice": 1, - "num_fixed": 1, - "lookup_bits": 22 - }, - "break_points": [ - [ - 8388596, - 8388597, - 8388596, - 8388598, - 8388598, - 8388597 - ], - [], - [] - ] -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/storage_4.json b/axiom-eth/configs/tests/batch_query/storage_4.json deleted file mode 100644 index 62adeddf..00000000 --- a/axiom-eth/configs/tests/batch_query/storage_4.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "params": { - "degree": 20, - "num_rlc_columns": 1, - "num_range_advice": [ - 11, - 6, - 0 - ], - "num_lookup_advice": [ - 1, - 1, - 0 - ], - "num_fixed": 1, - "unusable_rows": 63, - "keccak_rows_per_round": 38, - "lookup_bits": 8 - }, - "break_points": { - "gate": [ - [ - 1048466, - 1048464, - 1048466, - 1048464, - 1048464, - 1048466, - 1048465, - 1048465, - 1048464, - 1048464 - ], - [ - 1048466, - 1048464, - 1048466, - 1048466, - 1048464 - ], - [] - ], - "rlc": [] - } -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/batch_query/storage_5.json b/axiom-eth/configs/tests/batch_query/storage_5.json deleted file mode 100644 index c1bedfca..00000000 --- a/axiom-eth/configs/tests/batch_query/storage_5.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "params": { - "degree": 21, - "num_rlc_columns": 1, - "num_range_advice": [ - 11, - 6, - 0 - ], - "num_lookup_advice": [ - 1, - 1, - 0 - ], - "num_fixed": 1, - "unusable_rows": 63, - "keccak_rows_per_round": 38, - "lookup_bits": 8 - }, - "break_points": { - "gate": [ - [ - 2097042, - 2097042, - 2097040, - 2097042, - 2097042, - 2097041, - 2097040, - 2097040, - 2097042, - 2097040 - ], - [ - 2097042, - 2097040, - 2097042, - 2097042, - 2097041 - ], - [] - ], - "rlc": [] - } -} \ No newline at end of file diff --git a/axiom-eth/configs/tests/block_query.json b/axiom-eth/configs/tests/block_query.json deleted file mode 100644 index e19e5a1b..00000000 --- a/axiom-eth/configs/tests/block_query.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "degree": 19, - "num_rlc_columns": 3, - "num_range_advice": [40, 23], - "num_lookup_advice": [1, 1], - "num_fixed": 1, - "unusable_rows": 69, - "keccak_rows_per_round": 10 -} diff --git a/axiom-eth/configs/tests/keccak_shard.json b/axiom-eth/configs/tests/keccak_shard.json new file mode 100644 index 00000000..55a8e26a --- /dev/null +++ b/axiom-eth/configs/tests/keccak_shard.json @@ -0,0 +1,32 @@ +[ + { + "k": 18, + "num_unusable_row": 69, + "capacity": 50, + "publish_raw_outputs": false, + "keccak_circuit_params": { + "k": 18, + "rows_per_round": 206 + }, + "base_circuit_params": { + "k": 18, + "num_advice_per_phase": [ + 3 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 0, + 0, + 0 + ], + "lookup_bits": null, + "num_instance_columns": 1 + } + }, + [ + [ + 262034, + 262032 + ] + ] +] \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_16_7.json b/axiom-eth/configs/tests/keccak_shard2_merkle.json similarity index 60% rename from axiom-eth/configs/headers/mainnet_16_7.json rename to axiom-eth/configs/tests/keccak_shard2_merkle.json index a62b893b..1135bb18 100644 --- a/axiom-eth/configs/headers/mainnet_16_7.json +++ b/axiom-eth/configs/tests/keccak_shard2_merkle.json @@ -1,33 +1,39 @@ { "params": { "degree": 20, - "num_advice": 19, - "num_lookup_advice": 2, + "num_advice": 27, + "num_lookup_advice": 3, "num_fixed": 1, "lookup_bits": 19 }, "break_points": [ [ - 1048564, - 1048565, 1048566, 1048564, + 1048564, + 1048566, + 1048566, 1048566, 1048564, + 1048566, 1048565, + 1048564, 1048565, + 1048566, + 1048566, 1048564, + 1048565, + 1048566, 1048564, 1048564, - 1048565, 1048566, - 1048565, 1048566, + 1048566, + 1048564, 1048565, 1048566, - 1048566 - ], - [], - [] + 1048566, + 1048565 + ] ] } \ No newline at end of file diff --git a/axiom-eth/configs/tests/mpt.json b/axiom-eth/configs/tests/mpt.json deleted file mode 100644 index 2673f08d..00000000 --- a/axiom-eth/configs/tests/mpt.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "degree": 15, - "num_rlc_columns": 2, - "num_range_advice": [18, 10], - "num_lookup_advice": [1, 1], - "num_fixed": 1, - "unusable_rows": 65, - "keccak_rows_per_round": 22 -} diff --git a/axiom-eth/configs/tests/multi_block.json b/axiom-eth/configs/tests/multi_block.json deleted file mode 100644 index 2144bace..00000000 --- a/axiom-eth/configs/tests/multi_block.json +++ /dev/null @@ -1 +0,0 @@ -{"params":{"degree":14,"num_rlc_columns":3,"num_range_advice":[46,16,0],"num_lookup_advice":[1,1,0],"num_fixed":1,"unusable_rows":61,"keccak_rows_per_round":12,"lookup_bits":8},"break_points":{"gate":[[16322,16321,16320,16322,16320,16322,16322,16321,16320,16322,16320,16322,16322,16322,16322,16320,16322,16321,16321,16322,16322,16321,16321,16322,16322,16322,16320,16320,16321,16322,16320,16322,16322,16320,16320,16322,16322,16322,16321,16320,16322,16322,16322,16321,16322],[16322,16320,16322,16320,16320,16320,16322,16321,16322,16322,16322,16322,16321,16321,16320],[]],"rlc":[16322,16322]}} \ No newline at end of file diff --git a/axiom-eth/configs/tests/one_block.json b/axiom-eth/configs/tests/one_block.json index 6de53cd7..b10af31b 100644 --- a/axiom-eth/configs/tests/one_block.json +++ b/axiom-eth/configs/tests/one_block.json @@ -1,23 +1,11 @@ { - "params": { - "degree": 12, - "num_rlc_columns": 1, - "num_range_advice": [20, 8, 0], - "num_lookup_advice": [1, 1, 0], + "base": { + "k": 12, + "num_advice_per_phase": [23, 8], "num_fixed": 1, - "unusable_rows": 109, - "keccak_rows_per_round": 27, - "lookup_bits": 8 + "num_lookup_advice_per_phase": [1, 0, 0], + "lookup_bits": 8, + "num_instance_columns": 1 }, - "break_points": { - "gate": [ - [ - 4003, 4002, 4004, 4003, 4002, 4004, 4003, 4003, 4003, 4003, 4002, 4004, - 4004, 4003, 4004, 4002, 4004, 4004, 4004 - ], - [4004, 4004, 4004, 4004, 4003, 4004], - [] - ], - "rlc": [] - } + "num_rlc_columns": 1 } diff --git a/axiom-eth/configs/tests/storage.json b/axiom-eth/configs/tests/storage.json index d7ad86cb..d90a46da 100644 --- a/axiom-eth/configs/tests/storage.json +++ b/axiom-eth/configs/tests/storage.json @@ -1,9 +1,11 @@ { - "degree": 18, - "num_rlc_columns": 3, - "num_range_advice": [40, 23], - "num_lookup_advice": [1, 1], - "num_fixed": 1, - "unusable_rows": 69, - "keccak_rows_per_round": 10 + "base": { + "k": 15, + "num_advice_per_phase": [45, 24], + "num_fixed": 1, + "num_lookup_advice_per_phase": [1, 1, 0], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 3 } diff --git a/axiom-eth/configs/tests/storage_evm.json b/axiom-eth/configs/tests/storage_evm.json deleted file mode 100644 index e1a72b05..00000000 --- a/axiom-eth/configs/tests/storage_evm.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "strategy": "Simple", - "degree": 23, - "num_advice": [8], - "num_lookup_advice": [1], - "num_fixed": 1, - "lookup_bits": 22, - "limb_bits": 88, - "num_limbs": 3 -} diff --git a/axiom-eth/configs/tests/storage_mapping.json b/axiom-eth/configs/tests/storage_mapping.json new file mode 100644 index 00000000..d90a46da --- /dev/null +++ b/axiom-eth/configs/tests/storage_mapping.json @@ -0,0 +1,11 @@ +{ + "base": { + "k": 15, + "num_advice_per_phase": [45, 24], + "num_fixed": 1, + "num_lookup_advice_per_phase": [1, 1, 0], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 3 +} diff --git a/axiom-eth/configs/tests/storage_query.json b/axiom-eth/configs/tests/storage_query.json deleted file mode 100644 index d7ad86cb..00000000 --- a/axiom-eth/configs/tests/storage_query.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "degree": 18, - "num_rlc_columns": 3, - "num_range_advice": [40, 23], - "num_lookup_advice": [1, 1], - "num_fixed": 1, - "unusable_rows": 69, - "keccak_rows_per_round": 10 -} diff --git a/axiom-eth/configs/tests/transaction.json b/axiom-eth/configs/tests/transaction.json new file mode 100644 index 00000000..efa53e2e --- /dev/null +++ b/axiom-eth/configs/tests/transaction.json @@ -0,0 +1,11 @@ +{ + "base": { + "k": 16, + "num_advice_per_phase": [45, 24], + "num_fixed": 1, + "num_lookup_advice_per_phase": [1, 1, 0], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 3 +} diff --git a/axiom-eth/data/mappings/anvil_dynamic_uint.json b/axiom-eth/data/mappings/anvil_dynamic_uint.json new file mode 100644 index 00000000..52b5a106 --- /dev/null +++ b/axiom-eth/data/mappings/anvil_dynamic_uint.json @@ -0,0 +1,9 @@ +{ + "key": "0100", + "var_len": "1", + "max_var_len": "2", + "test_type": "DynamicArray", + "mapping_slot": "0000000000000000000000000000000000000000000000000000000000000000", + "ground_truth_concat_key": "01000000000000000000000000000000000000000000000000000000000000000000", + "ground_truth_slot": "0d678e31a4b2825b806fe160675cd01dab159802c7f94397ce45ed91b5f3aac6" +} \ No newline at end of file diff --git a/axiom-eth/data/mappings/block_mapping.t.json b/axiom-eth/data/mappings/block_mapping.t.json new file mode 100644 index 00000000..171cc70d --- /dev/null +++ b/axiom-eth/data/mappings/block_mapping.t.json @@ -0,0 +1,42 @@ +{ + "block_number": 14878854, + "query": [ + "1f98431c8ad98523631ae4a59f267346ea31f984", + "0000000000000000000000000000000000000000000000000000000000000005", + [ + [ + [ + "00000000000000000000000000000000000045166c45af0fc6e4cf31d9e14b9a", + "0000000000000000000000000000000000000000000000000000000000000020" + ], + [ + "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0000000000000000000000000000000000000000000000000000000000000020" + ], + [ + "0000000000000000000000000000000000000000000000000000000000002710", + "0000000000000000000000000000000000000000000000000000000000000020" + ] + + ], + [ + [ + "00000000000000000000000000000000000045166c45af0fc6e4cf31d9e14b9a", + "0000000000000000000000000000000000000000000000000000000000000020" + ], + [ + "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0000000000000000000000000000000000000000000000000000000000000020" + ], + [ + "0000000000000000000000000000000000000000000000000000000000002710", + "0000000000000000000000000000000000000000000000000000000000000020" + ] + ] + ] + ], + "not_empty": [ + true, + true + ] +} \ No newline at end of file diff --git a/axiom-eth/data/mappings/cryptopunks_balance_of_addr_uint.json b/axiom-eth/data/mappings/cryptopunks_balance_of_addr_uint.json new file mode 100644 index 00000000..af0f2c1a --- /dev/null +++ b/axiom-eth/data/mappings/cryptopunks_balance_of_addr_uint.json @@ -0,0 +1,9 @@ +{ + "key": "000000000000000000000000000000000000dead", + "var_len": "", + "max_var_len": "", + "test_type": "Address", + "mapping_slot": "000000000000000000000000000000000000000000000000000000000000000b", + "ground_truth_concat_key": "000000000000000000000000000000000000000000000000000000000000dead000000000000000000000000000000000000000000000000000000000000000b", + "ground_truth_slot": "44433eeeda1d04bdae79f62169cdb2ab0a6af287fa15706d3fafdbac5fac3415" +} \ No newline at end of file diff --git a/axiom-eth/data/mappings/mapping.t.json b/axiom-eth/data/mappings/mapping.t.json new file mode 100644 index 00000000..5aeb4e4d --- /dev/null +++ b/axiom-eth/data/mappings/mapping.t.json @@ -0,0 +1,42 @@ +{ + "block_response": 14878854, + "account_response": "1f98431c8ad98523631ae4a59f267346ea31f984", + "query": [ + "0000000000000000000000000000000000000000000000000000000000000005", + [ + [ + [ + "00000000000000000000000000000000000045166c45af0fc6e4cf31d9e14b9a", + "0000000000000000000000000000000000000000000000000000000000000020" + ], + [ + "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0000000000000000000000000000000000000000000000000000000000000020" + ], + [ + "0000000000000000000000000000000000000000000000000000000000002710", + "0000000000000000000000000000000000000000000000000000000000000020" + ] + + ], + [ + [ + "00000000000000000000000000000000000045166c45af0fc6e4cf31d9e14b9a", + "0000000000000000000000000000000000000000000000000000000000000020" + ], + [ + "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0000000000000000000000000000000000000000000000000000000000000020" + ], + [ + "0000000000000000000000000000000000000000000000000000000000002710", + "0000000000000000000000000000000000000000000000000000000000000020" + ] + ] + ] + ], + "not_empty": [ + true, + true + ] +} \ No newline at end of file diff --git a/axiom-eth/data/mappings/uni_v3_factory_fake.json b/axiom-eth/data/mappings/uni_v3_factory_fake.json new file mode 100644 index 00000000..32d529af --- /dev/null +++ b/axiom-eth/data/mappings/uni_v3_factory_fake.json @@ -0,0 +1,9 @@ +{ + "key": "00000000000000000000000000000000000003e8", + "var_len": "", + "max_var_len": "", + "test_type": "Address", + "mapping_slot": "125b34c9effbe1283a941860855143fc893f1fe5abe557e1626df746d9f19d16", + "ground_truth_concat_key": "00000000000000000000000000000000000000000000000000000000000003e8125b34c9effbe1283a941860855143fc893f1fe5abe557e1626df746d9f19d16", + "ground_truth_slot": "64479ced6d1ebcf1b3db1573104fc23b42e527cfb9bdf09e3085da4adad6fb28" +} \ No newline at end of file diff --git a/axiom-eth/data/mappings/uni_v3_factory_get_pool_addr_addr.json b/axiom-eth/data/mappings/uni_v3_factory_get_pool_addr_addr.json new file mode 100644 index 00000000..c5519323 --- /dev/null +++ b/axiom-eth/data/mappings/uni_v3_factory_get_pool_addr_addr.json @@ -0,0 +1,9 @@ + +{ + "key": "00000000000045166c45af0fc6e4cf31d9e14b9a", + "var_len": "", + "max_var_len": "", + "mapping_slot": "0000000000000000000000000000000000000000000000000000000000000005", + "ground_truth_concat_key": "00000000000000000000000000000000000045166c45af0fc6e4cf31d9e14b9a0000000000000000000000000000000000000000000000000000000000000005", + "ground_truth_slot": "43ae34693ee6dbe9779e719b5579311057a431d9890ba5cceb265b8a21e95990" +} \ No newline at end of file diff --git a/axiom-eth/data/mappings/uni_v3_factory_get_pool_addr_uint.json b/axiom-eth/data/mappings/uni_v3_factory_get_pool_addr_uint.json new file mode 100644 index 00000000..34386c2d --- /dev/null +++ b/axiom-eth/data/mappings/uni_v3_factory_get_pool_addr_uint.json @@ -0,0 +1,8 @@ +{ + "key": "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "var_len": "", + "max_var_len": "", + "mapping_slot": "43ae34693ee6dbe9779e719b5579311057a431d9890ba5cceb265b8a21e95990", + "ground_truth_concat_key": "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc243ae34693ee6dbe9779e719b5579311057a431d9890ba5cceb265b8a21e95990", + "ground_truth_slot": "af01b659268ea8b268c9c7ca07bf047651afe5aecef23fa3e9b9e2a8140a32b7" +} \ No newline at end of file diff --git a/axiom-eth/data/mappings/uni_v3_factory_get_pool_uint_addr.json b/axiom-eth/data/mappings/uni_v3_factory_get_pool_uint_addr.json new file mode 100644 index 00000000..4acee4c8 --- /dev/null +++ b/axiom-eth/data/mappings/uni_v3_factory_get_pool_uint_addr.json @@ -0,0 +1,8 @@ +{ + "key": "0000000000000000000000000000000000000000000000000000000000002710", + "var_len": "", + "max_var_len": "", + "mapping_slot": "af01b659268ea8b268c9c7ca07bf047651afe5aecef23fa3e9b9e2a8140a32b7", + "ground_truth_concat_key": "0000000000000000000000000000000000000000000000000000000000002710af01b659268ea8b268c9c7ca07bf047651afe5aecef23fa3e9b9e2a8140a32b7", + "ground_truth_slot": "125b34c9effbe1283a941860855143fc893f1fe5abe557e1626df746d9f19d16" +} \ No newline at end of file diff --git a/axiom-eth/data/mappings/unisocks_erc20_balance_of_addr_uint.json b/axiom-eth/data/mappings/unisocks_erc20_balance_of_addr_uint.json new file mode 100644 index 00000000..0621991c --- /dev/null +++ b/axiom-eth/data/mappings/unisocks_erc20_balance_of_addr_uint.json @@ -0,0 +1,9 @@ +{ + "key": "b9fddbd225b6c8cc24ce193e5fb95db76d783f2d", + "var_len": "", + "max_var_len": "", + "test_type": "Address", + "mapping_slot": "0000000000000000000000000000000000000000000000000000000000000005", + "ground_truth_concat_key": "000000000000000000000000b9fddbd225b6c8cc24ce193e5fb95db76d783f2d0000000000000000000000000000000000000000000000000000000000000005", + "ground_truth_slot": "751c4389cc6b086faa41a8f84e863555076c3c8323f4d14f98d9e76cbba041fc" +} \ No newline at end of file diff --git a/axiom-eth/data/mappings/unisocks_erc721_balance_of_addr_uint.json b/axiom-eth/data/mappings/unisocks_erc721_balance_of_addr_uint.json new file mode 100644 index 00000000..beb6f9c6 --- /dev/null +++ b/axiom-eth/data/mappings/unisocks_erc721_balance_of_addr_uint.json @@ -0,0 +1,9 @@ +{ + "key": "8688a84fcfd84d8f78020d0fc0b35987cc58911f", + "var_len": "", + "max_var_len": "", + "test_type": "Address", + "mapping_slot": "0000000000000000000000000000000000000000000000000000000000000007", + "ground_truth_concat_key": "0000000000000000000000008688a84fcfd84d8f78020d0fc0b35987cc58911f0000000000000000000000000000000000000000000000000000000000000007", + "ground_truth_slot": "900630798574b2f89ae13e25e9c1a75520672892d1a0b503abcb2bae8f12432f" +} \ No newline at end of file diff --git a/axiom-eth/data/mappings/weth_allowance_addr_addr.json b/axiom-eth/data/mappings/weth_allowance_addr_addr.json new file mode 100644 index 00000000..e7bde531 --- /dev/null +++ b/axiom-eth/data/mappings/weth_allowance_addr_addr.json @@ -0,0 +1,9 @@ +{ + "key": "000000000000006f6502b7f2bbac8c30a3f67e9a", + "var_len": "", + "max_var_len": "", + "test_type": "Address", + "mapping_slot": "0000000000000000000000000000000000000000000000000000000000000004", + "ground_truth_concat_key": "000000000000000000000000000000000000006f6502b7f2bbac8c30a3f67e9a0000000000000000000000000000000000000000000000000000000000000004", + "ground_truth_slot": "1c2339d1c4bfaf58b8f0d2638d9dae656c10932e13c9305c240770ec2cf1f659" +} \ No newline at end of file diff --git a/axiom-eth/data/mappings/weth_allowance_addr_uint.json b/axiom-eth/data/mappings/weth_allowance_addr_uint.json new file mode 100644 index 00000000..389f8b60 --- /dev/null +++ b/axiom-eth/data/mappings/weth_allowance_addr_uint.json @@ -0,0 +1,9 @@ +{ + "key": "01dfeaf2f6b648296b4c9dc4f0c5fddcb0984c0d", + "var_len": "", + "max_var_len": "", + "test_type": "Address", + "mapping_slot": "1c2339d1c4bfaf58b8f0d2638d9dae656c10932e13c9305c240770ec2cf1f659", + "ground_truth_concat_key": "00000000000000000000000001dfeaf2f6b648296b4c9dc4f0c5fddcb0984c0d1c2339d1c4bfaf58b8f0d2638d9dae656c10932e13c9305c240770ec2cf1f659", + "ground_truth_slot": "edb07cd504a790e8a8e63503d585d5c2408101fafac413177b1bdf3e0c3086e3" +} \ No newline at end of file diff --git a/axiom-eth/data/mappings/weth_balance_of_addr_uint.json b/axiom-eth/data/mappings/weth_balance_of_addr_uint.json new file mode 100644 index 00000000..e47e0526 --- /dev/null +++ b/axiom-eth/data/mappings/weth_balance_of_addr_uint.json @@ -0,0 +1,9 @@ +{ + "key": "f04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e", + "var_len": "", + "max_var_len": "", + "test_type": "Address", + "mapping_slot": "0000000000000000000000000000000000000000000000000000000000000003", + "ground_truth_concat_key": "000000000000000000000000f04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e0000000000000000000000000000000000000000000000000000000000000003", + "ground_truth_slot": "1f8193c3f94e8840dc3a6dfc0bc012432d338ef33c4f3e4b3aca0d6d3c5a09b6" +} \ No newline at end of file diff --git a/axiom-eth/data/mappings/weth_balance_of_bytes32_uint.json b/axiom-eth/data/mappings/weth_balance_of_bytes32_uint.json new file mode 100644 index 00000000..8d853009 --- /dev/null +++ b/axiom-eth/data/mappings/weth_balance_of_bytes32_uint.json @@ -0,0 +1,9 @@ +{ + "key": "000000000000000000000000f04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e", + "var_len": "", + "max_var_len": "", + "test_type": "Bytes32", + "mapping_slot": "0000000000000000000000000000000000000000000000000000000000000003", + "ground_truth_concat_key": "000000000000000000000000f04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e0000000000000000000000000000000000000000000000000000000000000003", + "ground_truth_slot": "1f8193c3f94e8840dc3a6dfc0bc012432d338ef33c4f3e4b3aca0d6d3c5a09b6" +} \ No newline at end of file diff --git a/axiom-eth/data/receipts/task.goerli.json b/axiom-eth/data/receipts/task.goerli.json new file mode 100644 index 00000000..4523150f --- /dev/null +++ b/axiom-eth/data/receipts/task.goerli.json @@ -0,0 +1,93 @@ +{ + "queries": [ + { + "txHash": "0x1320047d683efdf132b7939418a9f0061f9dfd7348a2f347f32745fb46cb6ec6", + "fieldIdx": 0 + }, + { + "txHash": "0x1320047d683efdf132b7939418a9f0061f9dfd7348a2f347f32745fb46cb6ec6", + "fieldIdx": 3, + "logIdx": 0 + }, + { + "txHash": "0x1320047d683efdf132b7939418a9f0061f9dfd7348a2f347f32745fb46cb6ec6", + "fieldIdx": 1 + } + ], + "mmr": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xe8d6970591102bd6e43961d0d4c6a3cef99a18b493cb3bda636ca7ec2409a86e", + "0xaaa2e7f7056a5e9e0275083d1e49b17a7415788c3fe0b7506aeca39386d95825", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xec3c6c5338ca0bddd0c7925aef2835b9ffb2ac4ac2b0d5f5ce38f59f06d387c7", + "0xa01781a978c22545f26063dd0a8f0a7c052b19e1f3706169c1b71d627a7ea13c", + "0xf8d8f6319c2dd17fb421463eb8d0c86881d0afaf8e08e95c49b5a9068727cae7", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x2caa414cfbca54154d3a8815f9e1a6d410f4e1d0f76df14f9eecebb0ea05b59d", + "0x33d65445a72205b92572a182e3867c9c66fca3a086d02dda622122df063b38c8", + "0xdde7f249a13c0fef92a9aa7abd559586732c0398241813253fcf07d956859bce", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xf6bcd969602fef0c36ec84ee377e592ec3b5311534ab3c1efcf0646acd96eead" + ], + "mmrProofs": [ + [ + "0xf7c9fdba1d27d4d66b2085ef041e97c5b9bddf0262f03cb75fff4ae5342da7fc", + "0xf01d543b87c11402616226836455ed3b03a00b9c3feebb478eb9e5a97be07392", + "0x4bd8b09a6fd10f09f920c3aa6aab942d63d55fdd1f093301b5fcb8f7fd59ce8a", + "0x79423f92140f3e6698b8d38394170fcd19c452282674c30835edf7c06dff49ef", + "0x4c39bed02253ac79744c1ff0134c82eecda62341048e9edc7f48caa0b06d2d1e", + "0x28805385c643dfc4f15251f848d32fe536cccc605b6f36312f8f1210c88bc4e5", + "0x3b14201f7ea94e76ec305487b1c5e3ec56715eafbe57a5ea3253b75f3625a772", + "0x22ac80faa27cba53fc2ca042902eb8c55d0b3599294ee004186a962ca8d9cc6f", + "0x73bc39a9b31be50ecdf05fa2b8e1826a72c59abffdbe010f135e0a4089440603", + "0xbb5e3fb8c83f0237baa615239a5372c98c9efd2c3a41d289b6ce134ee7a3bc50", + "0x4be9154bca6cb87ee928b0cc41bf29b1043ba1265fde3b2dad297d4d2b198f29", + "0x464ec85779dcb2a11b64e28a6aada9563b9a24803cf0250bf073768ed75e1c5f", + "0xe76931d3a2a46f459cf169a0e0e6d44af220c98835a9fd406e82ef9b167fffa1", + "0x7faeb24db369709c70ae2c41259d2216bc320e9dc29ee0d7e358b18a375ca113" + ], + [ + "0xf7c9fdba1d27d4d66b2085ef041e97c5b9bddf0262f03cb75fff4ae5342da7fc", + "0xf01d543b87c11402616226836455ed3b03a00b9c3feebb478eb9e5a97be07392", + "0x4bd8b09a6fd10f09f920c3aa6aab942d63d55fdd1f093301b5fcb8f7fd59ce8a", + "0x79423f92140f3e6698b8d38394170fcd19c452282674c30835edf7c06dff49ef", + "0x4c39bed02253ac79744c1ff0134c82eecda62341048e9edc7f48caa0b06d2d1e", + "0x28805385c643dfc4f15251f848d32fe536cccc605b6f36312f8f1210c88bc4e5", + "0x3b14201f7ea94e76ec305487b1c5e3ec56715eafbe57a5ea3253b75f3625a772", + "0x22ac80faa27cba53fc2ca042902eb8c55d0b3599294ee004186a962ca8d9cc6f", + "0x73bc39a9b31be50ecdf05fa2b8e1826a72c59abffdbe010f135e0a4089440603", + "0xbb5e3fb8c83f0237baa615239a5372c98c9efd2c3a41d289b6ce134ee7a3bc50", + "0x4be9154bca6cb87ee928b0cc41bf29b1043ba1265fde3b2dad297d4d2b198f29", + "0x464ec85779dcb2a11b64e28a6aada9563b9a24803cf0250bf073768ed75e1c5f", + "0xe76931d3a2a46f459cf169a0e0e6d44af220c98835a9fd406e82ef9b167fffa1", + "0x7faeb24db369709c70ae2c41259d2216bc320e9dc29ee0d7e358b18a375ca113" + ], + [ + "0xf7c9fdba1d27d4d66b2085ef041e97c5b9bddf0262f03cb75fff4ae5342da7fc", + "0xf01d543b87c11402616226836455ed3b03a00b9c3feebb478eb9e5a97be07392", + "0x4bd8b09a6fd10f09f920c3aa6aab942d63d55fdd1f093301b5fcb8f7fd59ce8a", + "0x79423f92140f3e6698b8d38394170fcd19c452282674c30835edf7c06dff49ef", + "0x4c39bed02253ac79744c1ff0134c82eecda62341048e9edc7f48caa0b06d2d1e", + "0x28805385c643dfc4f15251f848d32fe536cccc605b6f36312f8f1210c88bc4e5", + "0x3b14201f7ea94e76ec305487b1c5e3ec56715eafbe57a5ea3253b75f3625a772", + "0x22ac80faa27cba53fc2ca042902eb8c55d0b3599294ee004186a962ca8d9cc6f", + "0x73bc39a9b31be50ecdf05fa2b8e1826a72c59abffdbe010f135e0a4089440603", + "0xbb5e3fb8c83f0237baa615239a5372c98c9efd2c3a41d289b6ce134ee7a3bc50", + "0x4be9154bca6cb87ee928b0cc41bf29b1043ba1265fde3b2dad297d4d2b198f29", + "0x464ec85779dcb2a11b64e28a6aada9563b9a24803cf0250bf073768ed75e1c5f", + "0xe76931d3a2a46f459cf169a0e0e6d44af220c98835a9fd406e82ef9b167fffa1", + "0x7faeb24db369709c70ae2c41259d2216bc320e9dc29ee0d7e358b18a375ca113" + ] + ] +} diff --git a/axiom-eth/data/receipts/task.mainnet.json b/axiom-eth/data/receipts/task.mainnet.json new file mode 100644 index 00000000..0732b67d --- /dev/null +++ b/axiom-eth/data/receipts/task.mainnet.json @@ -0,0 +1,64 @@ +{ + "queries": [ + { + "txHash": "0x725187f197cbc28766304299191171891b72fa7ccfd1d70255c6296daf5212a7", + "fieldIdx": 3, + "logIdx": 0 + } + ], + "mmr": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xd2fcfe5ff5e4c3509468fa208708f7de5abe4894b16bfe36c4bdb465ae134bdd", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x577956894541f12e359ba6428306edbdc4e16f30eb33aea225412a4c619fbcd3", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xe553f451efe36070afa3e1bec68987919dbdb8b01153ff51b2c8ed222f95595a", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x37e7688219e9ad6573641ecda977fffa02efb76340ccd2f0726832bc9af0b0c0", + "0xd917e847ca54394b03475e17edde6598f4ac6772f3d517eea1e9f8134a8a89c2", + "0xbfed61dc28eb972a55ce01242ce04921e59e35bf83b5b26cbbbec7f23ddabe56", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x4755084f1c80a73395852c7ede586dddb3b8db7828e4ad2cf3259f693ac4f875" + ], + "mmrProofs": [ + [ + "0x0c49a468469a9c0af040376aeecc1a6625ccd188005ada6202c9349e0b2b1c7e", + "0xad2cf9b6a35ec994afd298cfda444dd3bc858bd98a72cae19d6746c4d43a378f", + "0x3860d4943d890e2120af1e764ce10060b2a9d348f6f89e183f155dd1f1c0b3f6", + "0x37a4e5a773a65924ec281ad1bb6f9006530d0a6d2dd557365f75628da046e4e7", + "0x3adc9d22627ff5c298f506e0dfbb83745ba889f747f4888410aa9be05b883beb", + "0x1fe33fae325abc07a8fe265103338699d0e0fae4b740b53a182556809bc45f13", + "0xef7a1b2657b1e90c747536ed1edf90cdb552022841fec94fbe5735fd9b8bf113", + "0x0f62ca6c4928d159b968e69708d7ccc3b704b8d7c4192d29d5513d3c12ea3492", + "0x22f91717e2361a8269b8b7f2c5296ea91d93da651457ad7ffc67e8216e45d61c", + "0x9ea188da4461fbcff96a42e44a4a6526528508b037e884e895bf9ff181a0a631", + "0x42d225f31886410369452490a02a0d54ad311ccec6e2ea617942f3de0e63c2df", + "0x2ea5b5d642306fb61a646a56621f8e17a5c8c29d1bc382cfc1ef5567e5ee6842", + "0x9032d519db6cae76bd6bfba6ce210ce36d208791ee1157413ad0465be74b3770", + "0xa7f4d56dd1fb81828199e77a205c86e2ed8d41e87d2f733c1ae171a04012bed0", + "0xdf69cb8c19b513b888aee79d2dfaa03ef4128f5628e0dae02a3f0348a87c6e5f", + "0x677f0ec9ecef0a39918979673c0e1286818509e8b0b24718d548927e60b38ca7", + "0xddebf1eaa86f2500698cf1de0b1b5c8f1963991a97af381afd27d5c9e6545a49", + "0x3643992767358e54ed59b4c36f7356be744c1c8234f383c79b970088c0e90872", + "0x43062e083f5f40510f30723d9cebb51c4ae67c35d86c5b791a043bae317350e3", + "0x6cddc980f4c3b403d99080c32b3f0a6205e39560b9021d5f233c04d96c23381e", + "0x6a42052cabd8d66a584b8823c6aadf64dd2755321210c66a2b7acd1da5bdeacf", + "0xebf08ca711cbab09109bb51277c545ee43073d8fa8b46c0cbbedd46ce85e73be", + "0x477c055e69de14e3bbfe2af0389e6c3ac28ffb5b0cc8fa26b543ac47857fd646", + "0xf47e6584af17667881494720b50dd063d0900b288438df7a37e2e91440aedd23" + ] + ] +} diff --git a/axiom-eth/data/tests/batch_query/final_2.yul b/axiom-eth/data/tests/batch_query/final_2.yul deleted file mode 100644 index 3c5d177a..00000000 --- a/axiom-eth/data/tests/batch_query/final_2.yul +++ /dev/null @@ -1,1790 +0,0 @@ - - object "plonk_verifier" { - code { - function allocate(size) -> ptr { - ptr := mload(0x40) - if eq(ptr, 0) { ptr := 0x60 } - mstore(0x40, add(ptr, size)) - } - let size := datasize("Runtime") - let offset := allocate(size) - datacopy(offset, dataoffset("Runtime"), size) - return(offset, size) - } - object "Runtime" { - code { - let success:bool := true - let f_p := 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47 - let f_q := 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001 - function validate_ec_point(x, y) -> valid:bool { - { - let x_lt_p:bool := lt(x, 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47) - let y_lt_p:bool := lt(y, 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47) - valid := and(x_lt_p, y_lt_p) - } - { - let y_square := mulmod(y, y, 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47) - let x_square := mulmod(x, x, 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47) - let x_cube := mulmod(x_square, x, 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47) - let x_cube_plus_3 := addmod(x_cube, 3, 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47) - let is_affine:bool := eq(x_cube_plus_3, y_square) - valid := and(valid, is_affine) - } - } - mstore(0x20, mod(calldataload(0x0), f_q)) -mstore(0x40, mod(calldataload(0x20), f_q)) -mstore(0x60, mod(calldataload(0x40), f_q)) -mstore(0x80, mod(calldataload(0x60), f_q)) -mstore(0xa0, mod(calldataload(0x80), f_q)) -mstore(0xc0, mod(calldataload(0xa0), f_q)) -mstore(0xe0, mod(calldataload(0xc0), f_q)) -mstore(0x100, mod(calldataload(0xe0), f_q)) -mstore(0x120, mod(calldataload(0x100), f_q)) -mstore(0x140, mod(calldataload(0x120), f_q)) -mstore(0x160, mod(calldataload(0x140), f_q)) -mstore(0x180, mod(calldataload(0x160), f_q)) -mstore(0x1a0, mod(calldataload(0x180), f_q)) -mstore(0x1c0, mod(calldataload(0x1a0), f_q)) -mstore(0x1e0, mod(calldataload(0x1c0), f_q)) -mstore(0x200, mod(calldataload(0x1e0), f_q)) -mstore(0x220, mod(calldataload(0x200), f_q)) -mstore(0x240, mod(calldataload(0x220), f_q)) -mstore(0x260, mod(calldataload(0x240), f_q)) -mstore(0x280, mod(calldataload(0x260), f_q)) -mstore(0x2a0, mod(calldataload(0x280), f_q)) -mstore(0x2c0, mod(calldataload(0x2a0), f_q)) -mstore(0x2e0, mod(calldataload(0x2c0), f_q)) -mstore(0x300, mod(calldataload(0x2e0), f_q)) -mstore(0x320, mod(calldataload(0x300), f_q)) -mstore(0x0, 18785606250233129449375664622106018768209554682525334786816107630995751978567) - - { - let x := calldataload(0x320) - mstore(0x340, x) - let y := calldataload(0x340) - mstore(0x360, y) - success := and(validate_ec_point(x, y), success) - } - - { - let x := calldataload(0x360) - mstore(0x380, x) - let y := calldataload(0x380) - mstore(0x3a0, y) - success := and(validate_ec_point(x, y), success) - } - - { - let x := calldataload(0x3a0) - mstore(0x3c0, x) - let y := calldataload(0x3c0) - mstore(0x3e0, y) - success := and(validate_ec_point(x, y), success) - } - - { - let x := calldataload(0x3e0) - mstore(0x400, x) - let y := calldataload(0x400) - mstore(0x420, y) - success := and(validate_ec_point(x, y), success) - } - - { - let x := calldataload(0x420) - mstore(0x440, x) - let y := calldataload(0x440) - mstore(0x460, y) - success := and(validate_ec_point(x, y), success) - } -mstore(0x480, keccak256(0x0, 1152)) -{ - let hash := mload(0x480) - mstore(0x4a0, mod(hash, f_q)) - mstore(0x4c0, hash) - } - - { - let x := calldataload(0x460) - mstore(0x4e0, x) - let y := calldataload(0x480) - mstore(0x500, y) - success := and(validate_ec_point(x, y), success) - } - - { - let x := calldataload(0x4a0) - mstore(0x520, x) - let y := calldataload(0x4c0) - mstore(0x540, y) - success := and(validate_ec_point(x, y), success) - } -mstore(0x560, keccak256(0x4c0, 160)) -{ - let hash := mload(0x560) - mstore(0x580, mod(hash, f_q)) - mstore(0x5a0, hash) - } -mstore8(1472, 1) -mstore(0x5c0, keccak256(0x5a0, 33)) -{ - let hash := mload(0x5c0) - mstore(0x5e0, mod(hash, f_q)) - mstore(0x600, hash) - } - - { - let x := calldataload(0x4e0) - mstore(0x620, x) - let y := calldataload(0x500) - mstore(0x640, y) - success := and(validate_ec_point(x, y), success) - } - - { - let x := calldataload(0x520) - mstore(0x660, x) - let y := calldataload(0x540) - mstore(0x680, y) - success := and(validate_ec_point(x, y), success) - } - - { - let x := calldataload(0x560) - mstore(0x6a0, x) - let y := calldataload(0x580) - mstore(0x6c0, y) - success := and(validate_ec_point(x, y), success) - } - - { - let x := calldataload(0x5a0) - mstore(0x6e0, x) - let y := calldataload(0x5c0) - mstore(0x700, y) - success := and(validate_ec_point(x, y), success) - } - - { - let x := calldataload(0x5e0) - mstore(0x720, x) - let y := calldataload(0x600) - mstore(0x740, y) - success := and(validate_ec_point(x, y), success) - } - - { - let x := calldataload(0x620) - mstore(0x760, x) - let y := calldataload(0x640) - mstore(0x780, y) - success := and(validate_ec_point(x, y), success) - } -mstore(0x7a0, keccak256(0x600, 416)) -{ - let hash := mload(0x7a0) - mstore(0x7c0, mod(hash, f_q)) - mstore(0x7e0, hash) - } - - { - let x := calldataload(0x660) - mstore(0x800, x) - let y := calldataload(0x680) - mstore(0x820, y) - success := and(validate_ec_point(x, y), success) - } - - { - let x := calldataload(0x6a0) - mstore(0x840, x) - let y := calldataload(0x6c0) - mstore(0x860, y) - success := and(validate_ec_point(x, y), success) - } - - { - let x := calldataload(0x6e0) - mstore(0x880, x) - let y := calldataload(0x700) - mstore(0x8a0, y) - success := and(validate_ec_point(x, y), success) - } -mstore(0x8c0, keccak256(0x7e0, 224)) -{ - let hash := mload(0x8c0) - mstore(0x8e0, mod(hash, f_q)) - mstore(0x900, hash) - } -mstore(0x920, mod(calldataload(0x720), f_q)) -mstore(0x940, mod(calldataload(0x740), f_q)) -mstore(0x960, mod(calldataload(0x760), f_q)) -mstore(0x980, mod(calldataload(0x780), f_q)) -mstore(0x9a0, mod(calldataload(0x7a0), f_q)) -mstore(0x9c0, mod(calldataload(0x7c0), f_q)) -mstore(0x9e0, mod(calldataload(0x7e0), f_q)) -mstore(0xa00, mod(calldataload(0x800), f_q)) -mstore(0xa20, mod(calldataload(0x820), f_q)) -mstore(0xa40, mod(calldataload(0x840), f_q)) -mstore(0xa60, mod(calldataload(0x860), f_q)) -mstore(0xa80, mod(calldataload(0x880), f_q)) -mstore(0xaa0, mod(calldataload(0x8a0), f_q)) -mstore(0xac0, mod(calldataload(0x8c0), f_q)) -mstore(0xae0, mod(calldataload(0x8e0), f_q)) -mstore(0xb00, mod(calldataload(0x900), f_q)) -mstore(0xb20, mod(calldataload(0x920), f_q)) -mstore(0xb40, mod(calldataload(0x940), f_q)) -mstore(0xb60, mod(calldataload(0x960), f_q)) -mstore(0xb80, mod(calldataload(0x980), f_q)) -mstore(0xba0, mod(calldataload(0x9a0), f_q)) -mstore(0xbc0, mod(calldataload(0x9c0), f_q)) -mstore(0xbe0, mod(calldataload(0x9e0), f_q)) -mstore(0xc00, mod(calldataload(0xa00), f_q)) -mstore(0xc20, mod(calldataload(0xa20), f_q)) -mstore(0xc40, mod(calldataload(0xa40), f_q)) -mstore(0xc60, mod(calldataload(0xa60), f_q)) -mstore(0xc80, mod(calldataload(0xa80), f_q)) -mstore(0xca0, mod(calldataload(0xaa0), f_q)) -mstore(0xcc0, mod(calldataload(0xac0), f_q)) -mstore(0xce0, mod(calldataload(0xae0), f_q)) -mstore(0xd00, mod(calldataload(0xb00), f_q)) -mstore(0xd20, mod(calldataload(0xb20), f_q)) -mstore(0xd40, mod(calldataload(0xb40), f_q)) -mstore(0xd60, mod(calldataload(0xb60), f_q)) -mstore(0xd80, mod(calldataload(0xb80), f_q)) -mstore(0xda0, mod(calldataload(0xba0), f_q)) -mstore(0xdc0, mod(calldataload(0xbc0), f_q)) -mstore(0xde0, mod(calldataload(0xbe0), f_q)) -mstore(0xe00, mod(calldataload(0xc00), f_q)) -mstore(0xe20, mod(calldataload(0xc20), f_q)) -mstore(0xe40, mod(calldataload(0xc40), f_q)) -mstore(0xe60, mod(calldataload(0xc60), f_q)) -mstore(0xe80, mod(calldataload(0xc80), f_q)) -mstore(0xea0, mod(calldataload(0xca0), f_q)) -mstore(0xec0, mod(calldataload(0xcc0), f_q)) -mstore(0xee0, mod(calldataload(0xce0), f_q)) -mstore(0xf00, keccak256(0x900, 1536)) -{ - let hash := mload(0xf00) - mstore(0xf20, mod(hash, f_q)) - mstore(0xf40, hash) - } -mstore8(3936, 1) -mstore(0xf60, keccak256(0xf40, 33)) -{ - let hash := mload(0xf60) - mstore(0xf80, mod(hash, f_q)) - mstore(0xfa0, hash) - } - - { - let x := calldataload(0xd00) - mstore(0xfc0, x) - let y := calldataload(0xd20) - mstore(0xfe0, y) - success := and(validate_ec_point(x, y), success) - } -mstore(0x1000, keccak256(0xfa0, 96)) -{ - let hash := mload(0x1000) - mstore(0x1020, mod(hash, f_q)) - mstore(0x1040, hash) - } - - { - let x := calldataload(0xd40) - mstore(0x1060, x) - let y := calldataload(0xd60) - mstore(0x1080, y) - success := and(validate_ec_point(x, y), success) - } -{ - let x := mload(0x20) -x := add(x, shl(88, mload(0x40))) -x := add(x, shl(176, mload(0x60))) -mstore(4256, x) -let y := mload(0x80) -y := add(y, shl(88, mload(0xa0))) -y := add(y, shl(176, mload(0xc0))) -mstore(4288, y) - - success := and(validate_ec_point(x, y), success) - } -{ - let x := mload(0xe0) -x := add(x, shl(88, mload(0x100))) -x := add(x, shl(176, mload(0x120))) -mstore(4320, x) -let y := mload(0x140) -y := add(y, shl(88, mload(0x160))) -y := add(y, shl(176, mload(0x180))) -mstore(4352, y) - - success := and(validate_ec_point(x, y), success) - } -mstore(0x1120, mulmod(mload(0x8e0), mload(0x8e0), f_q)) -mstore(0x1140, mulmod(mload(0x1120), mload(0x1120), f_q)) -mstore(0x1160, mulmod(mload(0x1140), mload(0x1140), f_q)) -mstore(0x1180, mulmod(mload(0x1160), mload(0x1160), f_q)) -mstore(0x11a0, mulmod(mload(0x1180), mload(0x1180), f_q)) -mstore(0x11c0, mulmod(mload(0x11a0), mload(0x11a0), f_q)) -mstore(0x11e0, mulmod(mload(0x11c0), mload(0x11c0), f_q)) -mstore(0x1200, mulmod(mload(0x11e0), mload(0x11e0), f_q)) -mstore(0x1220, mulmod(mload(0x1200), mload(0x1200), f_q)) -mstore(0x1240, mulmod(mload(0x1220), mload(0x1220), f_q)) -mstore(0x1260, mulmod(mload(0x1240), mload(0x1240), f_q)) -mstore(0x1280, mulmod(mload(0x1260), mload(0x1260), f_q)) -mstore(0x12a0, mulmod(mload(0x1280), mload(0x1280), f_q)) -mstore(0x12c0, mulmod(mload(0x12a0), mload(0x12a0), f_q)) -mstore(0x12e0, mulmod(mload(0x12c0), mload(0x12c0), f_q)) -mstore(0x1300, mulmod(mload(0x12e0), mload(0x12e0), f_q)) -mstore(0x1320, mulmod(mload(0x1300), mload(0x1300), f_q)) -mstore(0x1340, mulmod(mload(0x1320), mload(0x1320), f_q)) -mstore(0x1360, mulmod(mload(0x1340), mload(0x1340), f_q)) -mstore(0x1380, mulmod(mload(0x1360), mload(0x1360), f_q)) -mstore(0x13a0, mulmod(mload(0x1380), mload(0x1380), f_q)) -mstore(0x13c0, mulmod(mload(0x13a0), mload(0x13a0), f_q)) -mstore(0x13e0, addmod(mload(0x13c0), 21888242871839275222246405745257275088548364400416034343698204186575808495616, f_q)) -mstore(0x1400, mulmod(mload(0x13e0), 21888237653275510688422624196183639687472264873923820041627027729598873448513, f_q)) -mstore(0x1420, mulmod(mload(0x1400), 13225785879531581993054172815365636627224369411478295502904397545373139154045, f_q)) -mstore(0x1440, addmod(mload(0x8e0), 8662456992307693229192232929891638461323994988937738840793806641202669341572, f_q)) -mstore(0x1460, mulmod(mload(0x1400), 10939663269433627367777756708678102241564365262857670666700619874077960926249, f_q)) -mstore(0x1480, addmod(mload(0x8e0), 10948579602405647854468649036579172846983999137558363676997584312497847569368, f_q)) -mstore(0x14a0, mulmod(mload(0x1400), 11016257578652593686382655500910603527869149377564754001549454008164059876499, f_q)) -mstore(0x14c0, addmod(mload(0x8e0), 10871985293186681535863750244346671560679215022851280342148750178411748619118, f_q)) -mstore(0x14e0, mulmod(mload(0x1400), 15402826414547299628414612080036060696555554914079673875872749760617770134879, f_q)) -mstore(0x1500, addmod(mload(0x8e0), 6485416457291975593831793665221214391992809486336360467825454425958038360738, f_q)) -mstore(0x1520, mulmod(mload(0x1400), 21710372849001950800533397158415938114909991150039389063546734567764856596059, f_q)) -mstore(0x1540, addmod(mload(0x8e0), 177870022837324421713008586841336973638373250376645280151469618810951899558, f_q)) -mstore(0x1560, mulmod(mload(0x1400), 2785514556381676080176937710880804108647911392478702105860685610379369825016, f_q)) -mstore(0x1580, addmod(mload(0x8e0), 19102728315457599142069468034376470979900453007937332237837518576196438670601, f_q)) -mstore(0x15a0, mulmod(mload(0x1400), 8734126352828345679573237859165904705806588461301144420590422589042130041188, f_q)) -mstore(0x15c0, addmod(mload(0x8e0), 13154116519010929542673167886091370382741775939114889923107781597533678454429, f_q)) -mstore(0x15e0, mulmod(mload(0x1400), 1, f_q)) -mstore(0x1600, addmod(mload(0x8e0), 21888242871839275222246405745257275088548364400416034343698204186575808495616, f_q)) -mstore(0x1620, mulmod(mload(0x1400), 11211301017135681023579411905410872569206244553457844956874280139879520583390, f_q)) -mstore(0x1640, addmod(mload(0x8e0), 10676941854703594198666993839846402519342119846958189386823924046696287912227, f_q)) -mstore(0x1660, mulmod(mload(0x1400), 1426404432721484388505361748317961535523355871255605456897797744433766488507, f_q)) -mstore(0x1680, addmod(mload(0x8e0), 20461838439117790833741043996939313553025008529160428886800406442142042007110, f_q)) -mstore(0x16a0, mulmod(mload(0x1400), 12619617507853212586156872920672483948819476989779550311307282715684870266992, f_q)) -mstore(0x16c0, addmod(mload(0x8e0), 9268625363986062636089532824584791139728887410636484032390921470890938228625, f_q)) -mstore(0x16e0, mulmod(mload(0x1400), 19032961837237948602743626455740240236231119053033140765040043513661803148152, f_q)) -mstore(0x1700, addmod(mload(0x8e0), 2855281034601326619502779289517034852317245347382893578658160672914005347465, f_q)) -mstore(0x1720, mulmod(mload(0x1400), 915149353520972163646494413843788069594022902357002628455555785223409501882, f_q)) -mstore(0x1740, addmod(mload(0x8e0), 20973093518318303058599911331413487018954341498059031715242648401352398993735, f_q)) -mstore(0x1760, mulmod(mload(0x1400), 3766081621734395783232337525162072736827576297943013392955872170138036189193, f_q)) -mstore(0x1780, addmod(mload(0x8e0), 18122161250104879439014068220095202351720788102473020950742332016437772306424, f_q)) -mstore(0x17a0, mulmod(mload(0x1400), 4245441013247250116003069945606352967193023389718465410501109428393342802981, f_q)) -mstore(0x17c0, addmod(mload(0x8e0), 17642801858592025106243335799650922121355341010697568933197094758182465692636, f_q)) -mstore(0x17e0, mulmod(mload(0x1400), 5854133144571823792863860130267644613802765696134002830362054821530146160770, f_q)) -mstore(0x1800, addmod(mload(0x8e0), 16034109727267451429382545614989630474745598704282031513336149365045662334847, f_q)) -mstore(0x1820, mulmod(mload(0x1400), 5980488956150442207659150513163747165544364597008566989111579977672498964212, f_q)) -mstore(0x1840, addmod(mload(0x8e0), 15907753915688833014587255232093527923003999803407467354586624208903309531405, f_q)) -mstore(0x1860, mulmod(mload(0x1400), 14557038802599140430182096396825290815503940951075961210638273254419942783582, f_q)) -mstore(0x1880, addmod(mload(0x8e0), 7331204069240134792064309348431984273044423449340073133059930932155865712035, f_q)) -mstore(0x18a0, mulmod(mload(0x1400), 13553911191894110065493137367144919847521088405945523452288398666974237857208, f_q)) -mstore(0x18c0, addmod(mload(0x8e0), 8334331679945165156753268378112355241027275994470510891409805519601570638409, f_q)) -mstore(0x18e0, mulmod(mload(0x1400), 9697063347556872083384215826199993067635178715531258559890418744774301211662, f_q)) -mstore(0x1900, addmod(mload(0x8e0), 12191179524282403138862189919057282020913185684884775783807785441801507283955, f_q)) -mstore(0x1920, mulmod(mload(0x1400), 10807735674816066981985242612061336605021639643453679977988966079770672437131, f_q)) -mstore(0x1940, addmod(mload(0x8e0), 11080507197023208240261163133195938483526724756962354365709238106805136058486, f_q)) -mstore(0x1960, mulmod(mload(0x1400), 12459868075641381822485233712013080087763946065665469821362892189399541605692, f_q)) -mstore(0x1980, addmod(mload(0x8e0), 9428374796197893399761172033244195000784418334750564522335311997176266889925, f_q)) -mstore(0x19a0, mulmod(mload(0x1400), 16038300751658239075779628684257016433412502747804121525056508685985277092575, f_q)) -mstore(0x19c0, addmod(mload(0x8e0), 5849942120181036146466777061000258655135861652611912818641695500590531403042, f_q)) -mstore(0x19e0, mulmod(mload(0x1400), 6955697244493336113861667751840378876927906302623587437721024018233754910398, f_q)) -mstore(0x1a00, addmod(mload(0x8e0), 14932545627345939108384737993416896211620458097792446905977180168342053585219, f_q)) -mstore(0x1a20, mulmod(mload(0x1400), 13498745591877810872211159461644682954739332524336278910448604883789771736885, f_q)) -mstore(0x1a40, addmod(mload(0x8e0), 8389497279961464350035246283612592133809031876079755433249599302786036758732, f_q)) -mstore(0x1a60, mulmod(mload(0x1400), 20345677989844117909528750049476969581182118546166966482506114734614108237981, f_q)) -mstore(0x1a80, addmod(mload(0x8e0), 1542564881995157312717655695780305507366245854249067861192089451961700257636, f_q)) -mstore(0x1aa0, mulmod(mload(0x1400), 790608022292213379425324383664216541739009722347092850716054055768832299157, f_q)) -mstore(0x1ac0, addmod(mload(0x8e0), 21097634849547061842821081361593058546809354678068941492982150130806976196460, f_q)) -mstore(0x1ae0, mulmod(mload(0x1400), 5289443209903185443361862148540090689648485914368835830972895623576469023722, f_q)) -mstore(0x1b00, addmod(mload(0x8e0), 16598799661936089778884543596717184398899878486047198512725308562999339471895, f_q)) -mstore(0x1b20, mulmod(mload(0x1400), 15161189183906287273290738379431332336600234154579306802151507052820126345529, f_q)) -mstore(0x1b40, addmod(mload(0x8e0), 6727053687932987948955667365825942751948130245836727541546697133755682150088, f_q)) -mstore(0x1b60, mulmod(mload(0x1400), 557567375339945239933617516585967620814823575807691402619711360028043331811, f_q)) -mstore(0x1b80, addmod(mload(0x8e0), 21330675496499329982312788228671307467733540824608342941078492826547765163806, f_q)) -mstore(0x1ba0, mulmod(mload(0x1400), 16611719114775828483319365659907682366622074960672212059891361227499450055959, f_q)) -mstore(0x1bc0, addmod(mload(0x8e0), 5276523757063446738927040085349592721926289439743822283806842959076358439658, f_q)) -mstore(0x1be0, mulmod(mload(0x1400), 4509404676247677387317362072810231899718070082381452255950861037254608304934, f_q)) -mstore(0x1c00, addmod(mload(0x8e0), 17378838195591597834929043672447043188830294318034582087747343149321200190683, f_q)) -{ - let prod := mload(0x1440) - - prod := mulmod(mload(0x1480), prod, f_q) - mstore(0x1c20, prod) - - prod := mulmod(mload(0x14c0), prod, f_q) - mstore(0x1c40, prod) - - prod := mulmod(mload(0x1500), prod, f_q) - mstore(0x1c60, prod) - - prod := mulmod(mload(0x1540), prod, f_q) - mstore(0x1c80, prod) - - prod := mulmod(mload(0x1580), prod, f_q) - mstore(0x1ca0, prod) - - prod := mulmod(mload(0x15c0), prod, f_q) - mstore(0x1cc0, prod) - - prod := mulmod(mload(0x1600), prod, f_q) - mstore(0x1ce0, prod) - - prod := mulmod(mload(0x1640), prod, f_q) - mstore(0x1d00, prod) - - prod := mulmod(mload(0x1680), prod, f_q) - mstore(0x1d20, prod) - - prod := mulmod(mload(0x16c0), prod, f_q) - mstore(0x1d40, prod) - - prod := mulmod(mload(0x1700), prod, f_q) - mstore(0x1d60, prod) - - prod := mulmod(mload(0x1740), prod, f_q) - mstore(0x1d80, prod) - - prod := mulmod(mload(0x1780), prod, f_q) - mstore(0x1da0, prod) - - prod := mulmod(mload(0x17c0), prod, f_q) - mstore(0x1dc0, prod) - - prod := mulmod(mload(0x1800), prod, f_q) - mstore(0x1de0, prod) - - prod := mulmod(mload(0x1840), prod, f_q) - mstore(0x1e00, prod) - - prod := mulmod(mload(0x1880), prod, f_q) - mstore(0x1e20, prod) - - prod := mulmod(mload(0x18c0), prod, f_q) - mstore(0x1e40, prod) - - prod := mulmod(mload(0x1900), prod, f_q) - mstore(0x1e60, prod) - - prod := mulmod(mload(0x1940), prod, f_q) - mstore(0x1e80, prod) - - prod := mulmod(mload(0x1980), prod, f_q) - mstore(0x1ea0, prod) - - prod := mulmod(mload(0x19c0), prod, f_q) - mstore(0x1ec0, prod) - - prod := mulmod(mload(0x1a00), prod, f_q) - mstore(0x1ee0, prod) - - prod := mulmod(mload(0x1a40), prod, f_q) - mstore(0x1f00, prod) - - prod := mulmod(mload(0x1a80), prod, f_q) - mstore(0x1f20, prod) - - prod := mulmod(mload(0x1ac0), prod, f_q) - mstore(0x1f40, prod) - - prod := mulmod(mload(0x1b00), prod, f_q) - mstore(0x1f60, prod) - - prod := mulmod(mload(0x1b40), prod, f_q) - mstore(0x1f80, prod) - - prod := mulmod(mload(0x1b80), prod, f_q) - mstore(0x1fa0, prod) - - prod := mulmod(mload(0x1bc0), prod, f_q) - mstore(0x1fc0, prod) - - prod := mulmod(mload(0x1c00), prod, f_q) - mstore(0x1fe0, prod) - - prod := mulmod(mload(0x13e0), prod, f_q) - mstore(0x2000, prod) - - } -mstore(0x2040, 32) -mstore(0x2060, 32) -mstore(0x2080, 32) -mstore(0x20a0, mload(0x2000)) -mstore(0x20c0, 21888242871839275222246405745257275088548364400416034343698204186575808495615) -mstore(0x20e0, 21888242871839275222246405745257275088548364400416034343698204186575808495617) -success := and(eq(staticcall(gas(), 0x5, 0x2040, 0xc0, 0x2020, 0x20), 1), success) -{ - - let inv := mload(0x2020) - let v - - v := mload(0x13e0) - mstore(5088, mulmod(mload(0x1fe0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1c00) - mstore(7168, mulmod(mload(0x1fc0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1bc0) - mstore(7104, mulmod(mload(0x1fa0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1b80) - mstore(7040, mulmod(mload(0x1f80), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1b40) - mstore(6976, mulmod(mload(0x1f60), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1b00) - mstore(6912, mulmod(mload(0x1f40), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1ac0) - mstore(6848, mulmod(mload(0x1f20), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1a80) - mstore(6784, mulmod(mload(0x1f00), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1a40) - mstore(6720, mulmod(mload(0x1ee0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1a00) - mstore(6656, mulmod(mload(0x1ec0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x19c0) - mstore(6592, mulmod(mload(0x1ea0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1980) - mstore(6528, mulmod(mload(0x1e80), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1940) - mstore(6464, mulmod(mload(0x1e60), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1900) - mstore(6400, mulmod(mload(0x1e40), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x18c0) - mstore(6336, mulmod(mload(0x1e20), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1880) - mstore(6272, mulmod(mload(0x1e00), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1840) - mstore(6208, mulmod(mload(0x1de0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1800) - mstore(6144, mulmod(mload(0x1dc0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x17c0) - mstore(6080, mulmod(mload(0x1da0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1780) - mstore(6016, mulmod(mload(0x1d80), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1740) - mstore(5952, mulmod(mload(0x1d60), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1700) - mstore(5888, mulmod(mload(0x1d40), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x16c0) - mstore(5824, mulmod(mload(0x1d20), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1680) - mstore(5760, mulmod(mload(0x1d00), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1640) - mstore(5696, mulmod(mload(0x1ce0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1600) - mstore(5632, mulmod(mload(0x1cc0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x15c0) - mstore(5568, mulmod(mload(0x1ca0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1580) - mstore(5504, mulmod(mload(0x1c80), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1540) - mstore(5440, mulmod(mload(0x1c60), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1500) - mstore(5376, mulmod(mload(0x1c40), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x14c0) - mstore(5312, mulmod(mload(0x1c20), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x1480) - mstore(5248, mulmod(mload(0x1440), inv, f_q)) - inv := mulmod(v, inv, f_q) - mstore(0x1440, inv) - - } -mstore(0x2100, mulmod(mload(0x1420), mload(0x1440), f_q)) -mstore(0x2120, mulmod(mload(0x1460), mload(0x1480), f_q)) -mstore(0x2140, mulmod(mload(0x14a0), mload(0x14c0), f_q)) -mstore(0x2160, mulmod(mload(0x14e0), mload(0x1500), f_q)) -mstore(0x2180, mulmod(mload(0x1520), mload(0x1540), f_q)) -mstore(0x21a0, mulmod(mload(0x1560), mload(0x1580), f_q)) -mstore(0x21c0, mulmod(mload(0x15a0), mload(0x15c0), f_q)) -mstore(0x21e0, mulmod(mload(0x15e0), mload(0x1600), f_q)) -mstore(0x2200, mulmod(mload(0x1620), mload(0x1640), f_q)) -mstore(0x2220, mulmod(mload(0x1660), mload(0x1680), f_q)) -mstore(0x2240, mulmod(mload(0x16a0), mload(0x16c0), f_q)) -mstore(0x2260, mulmod(mload(0x16e0), mload(0x1700), f_q)) -mstore(0x2280, mulmod(mload(0x1720), mload(0x1740), f_q)) -mstore(0x22a0, mulmod(mload(0x1760), mload(0x1780), f_q)) -mstore(0x22c0, mulmod(mload(0x17a0), mload(0x17c0), f_q)) -mstore(0x22e0, mulmod(mload(0x17e0), mload(0x1800), f_q)) -mstore(0x2300, mulmod(mload(0x1820), mload(0x1840), f_q)) -mstore(0x2320, mulmod(mload(0x1860), mload(0x1880), f_q)) -mstore(0x2340, mulmod(mload(0x18a0), mload(0x18c0), f_q)) -mstore(0x2360, mulmod(mload(0x18e0), mload(0x1900), f_q)) -mstore(0x2380, mulmod(mload(0x1920), mload(0x1940), f_q)) -mstore(0x23a0, mulmod(mload(0x1960), mload(0x1980), f_q)) -mstore(0x23c0, mulmod(mload(0x19a0), mload(0x19c0), f_q)) -mstore(0x23e0, mulmod(mload(0x19e0), mload(0x1a00), f_q)) -mstore(0x2400, mulmod(mload(0x1a20), mload(0x1a40), f_q)) -mstore(0x2420, mulmod(mload(0x1a60), mload(0x1a80), f_q)) -mstore(0x2440, mulmod(mload(0x1aa0), mload(0x1ac0), f_q)) -mstore(0x2460, mulmod(mload(0x1ae0), mload(0x1b00), f_q)) -mstore(0x2480, mulmod(mload(0x1b20), mload(0x1b40), f_q)) -mstore(0x24a0, mulmod(mload(0x1b60), mload(0x1b80), f_q)) -mstore(0x24c0, mulmod(mload(0x1ba0), mload(0x1bc0), f_q)) -mstore(0x24e0, mulmod(mload(0x1be0), mload(0x1c00), f_q)) -{ - let result := mulmod(mload(0x21e0), mload(0x20), f_q) -result := addmod(mulmod(mload(0x2200), mload(0x40), f_q), result, f_q) -result := addmod(mulmod(mload(0x2220), mload(0x60), f_q), result, f_q) -result := addmod(mulmod(mload(0x2240), mload(0x80), f_q), result, f_q) -result := addmod(mulmod(mload(0x2260), mload(0xa0), f_q), result, f_q) -result := addmod(mulmod(mload(0x2280), mload(0xc0), f_q), result, f_q) -result := addmod(mulmod(mload(0x22a0), mload(0xe0), f_q), result, f_q) -result := addmod(mulmod(mload(0x22c0), mload(0x100), f_q), result, f_q) -result := addmod(mulmod(mload(0x22e0), mload(0x120), f_q), result, f_q) -result := addmod(mulmod(mload(0x2300), mload(0x140), f_q), result, f_q) -result := addmod(mulmod(mload(0x2320), mload(0x160), f_q), result, f_q) -result := addmod(mulmod(mload(0x2340), mload(0x180), f_q), result, f_q) -result := addmod(mulmod(mload(0x2360), mload(0x1a0), f_q), result, f_q) -result := addmod(mulmod(mload(0x2380), mload(0x1c0), f_q), result, f_q) -result := addmod(mulmod(mload(0x23a0), mload(0x1e0), f_q), result, f_q) -result := addmod(mulmod(mload(0x23c0), mload(0x200), f_q), result, f_q) -result := addmod(mulmod(mload(0x23e0), mload(0x220), f_q), result, f_q) -result := addmod(mulmod(mload(0x2400), mload(0x240), f_q), result, f_q) -result := addmod(mulmod(mload(0x2420), mload(0x260), f_q), result, f_q) -result := addmod(mulmod(mload(0x2440), mload(0x280), f_q), result, f_q) -result := addmod(mulmod(mload(0x2460), mload(0x2a0), f_q), result, f_q) -result := addmod(mulmod(mload(0x2480), mload(0x2c0), f_q), result, f_q) -result := addmod(mulmod(mload(0x24a0), mload(0x2e0), f_q), result, f_q) -result := addmod(mulmod(mload(0x24c0), mload(0x300), f_q), result, f_q) -result := addmod(mulmod(mload(0x24e0), mload(0x320), f_q), result, f_q) -mstore(9472, result) - } -mstore(0x2520, mulmod(mload(0x960), mload(0x940), f_q)) -mstore(0x2540, addmod(mload(0x920), mload(0x2520), f_q)) -mstore(0x2560, addmod(mload(0x2540), sub(f_q, mload(0x980)), f_q)) -mstore(0x2580, mulmod(mload(0x2560), mload(0xb80), f_q)) -mstore(0x25a0, mulmod(mload(0x7c0), mload(0x2580), f_q)) -mstore(0x25c0, mulmod(mload(0x9e0), mload(0x9c0), f_q)) -mstore(0x25e0, addmod(mload(0x9a0), mload(0x25c0), f_q)) -mstore(0x2600, addmod(mload(0x25e0), sub(f_q, mload(0xa00)), f_q)) -mstore(0x2620, mulmod(mload(0x2600), mload(0xba0), f_q)) -mstore(0x2640, addmod(mload(0x25a0), mload(0x2620), f_q)) -mstore(0x2660, mulmod(mload(0x7c0), mload(0x2640), f_q)) -mstore(0x2680, mulmod(mload(0xa60), mload(0xa40), f_q)) -mstore(0x26a0, addmod(mload(0xa20), mload(0x2680), f_q)) -mstore(0x26c0, addmod(mload(0x26a0), sub(f_q, mload(0xa80)), f_q)) -mstore(0x26e0, mulmod(mload(0x26c0), mload(0xbc0), f_q)) -mstore(0x2700, addmod(mload(0x2660), mload(0x26e0), f_q)) -mstore(0x2720, mulmod(mload(0x7c0), mload(0x2700), f_q)) -mstore(0x2740, mulmod(mload(0xae0), mload(0xac0), f_q)) -mstore(0x2760, addmod(mload(0xaa0), mload(0x2740), f_q)) -mstore(0x2780, addmod(mload(0x2760), sub(f_q, mload(0xb00)), f_q)) -mstore(0x27a0, mulmod(mload(0x2780), mload(0xbe0), f_q)) -mstore(0x27c0, addmod(mload(0x2720), mload(0x27a0), f_q)) -mstore(0x27e0, mulmod(mload(0x7c0), mload(0x27c0), f_q)) -mstore(0x2800, addmod(1, sub(f_q, mload(0xd00)), f_q)) -mstore(0x2820, mulmod(mload(0x2800), mload(0x21e0), f_q)) -mstore(0x2840, addmod(mload(0x27e0), mload(0x2820), f_q)) -mstore(0x2860, mulmod(mload(0x7c0), mload(0x2840), f_q)) -mstore(0x2880, mulmod(mload(0xe20), mload(0xe20), f_q)) -mstore(0x28a0, addmod(mload(0x2880), sub(f_q, mload(0xe20)), f_q)) -mstore(0x28c0, mulmod(mload(0x28a0), mload(0x2100), f_q)) -mstore(0x28e0, addmod(mload(0x2860), mload(0x28c0), f_q)) -mstore(0x2900, mulmod(mload(0x7c0), mload(0x28e0), f_q)) -mstore(0x2920, addmod(mload(0xd60), sub(f_q, mload(0xd40)), f_q)) -mstore(0x2940, mulmod(mload(0x2920), mload(0x21e0), f_q)) -mstore(0x2960, addmod(mload(0x2900), mload(0x2940), f_q)) -mstore(0x2980, mulmod(mload(0x7c0), mload(0x2960), f_q)) -mstore(0x29a0, addmod(mload(0xdc0), sub(f_q, mload(0xda0)), f_q)) -mstore(0x29c0, mulmod(mload(0x29a0), mload(0x21e0), f_q)) -mstore(0x29e0, addmod(mload(0x2980), mload(0x29c0), f_q)) -mstore(0x2a00, mulmod(mload(0x7c0), mload(0x29e0), f_q)) -mstore(0x2a20, addmod(mload(0xe20), sub(f_q, mload(0xe00)), f_q)) -mstore(0x2a40, mulmod(mload(0x2a20), mload(0x21e0), f_q)) -mstore(0x2a60, addmod(mload(0x2a00), mload(0x2a40), f_q)) -mstore(0x2a80, mulmod(mload(0x7c0), mload(0x2a60), f_q)) -mstore(0x2aa0, addmod(1, sub(f_q, mload(0x2100)), f_q)) -mstore(0x2ac0, addmod(mload(0x2120), mload(0x2140), f_q)) -mstore(0x2ae0, addmod(mload(0x2ac0), mload(0x2160), f_q)) -mstore(0x2b00, addmod(mload(0x2ae0), mload(0x2180), f_q)) -mstore(0x2b20, addmod(mload(0x2b00), mload(0x21a0), f_q)) -mstore(0x2b40, addmod(mload(0x2b20), mload(0x21c0), f_q)) -mstore(0x2b60, addmod(mload(0x2aa0), sub(f_q, mload(0x2b40)), f_q)) -mstore(0x2b80, mulmod(mload(0xc20), mload(0x580), f_q)) -mstore(0x2ba0, addmod(mload(0xb40), mload(0x2b80), f_q)) -mstore(0x2bc0, addmod(mload(0x2ba0), mload(0x5e0), f_q)) -mstore(0x2be0, mulmod(mload(0xc40), mload(0x580), f_q)) -mstore(0x2c00, addmod(mload(0x920), mload(0x2be0), f_q)) -mstore(0x2c20, addmod(mload(0x2c00), mload(0x5e0), f_q)) -mstore(0x2c40, mulmod(mload(0x2c20), mload(0x2bc0), f_q)) -mstore(0x2c60, mulmod(mload(0x2c40), mload(0xd20), f_q)) -mstore(0x2c80, mulmod(1, mload(0x580), f_q)) -mstore(0x2ca0, mulmod(mload(0x8e0), mload(0x2c80), f_q)) -mstore(0x2cc0, addmod(mload(0xb40), mload(0x2ca0), f_q)) -mstore(0x2ce0, addmod(mload(0x2cc0), mload(0x5e0), f_q)) -mstore(0x2d00, mulmod(4131629893567559867359510883348571134090853742863529169391034518566172092834, mload(0x580), f_q)) -mstore(0x2d20, mulmod(mload(0x8e0), mload(0x2d00), f_q)) -mstore(0x2d40, addmod(mload(0x920), mload(0x2d20), f_q)) -mstore(0x2d60, addmod(mload(0x2d40), mload(0x5e0), f_q)) -mstore(0x2d80, mulmod(mload(0x2d60), mload(0x2ce0), f_q)) -mstore(0x2da0, mulmod(mload(0x2d80), mload(0xd00), f_q)) -mstore(0x2dc0, addmod(mload(0x2c60), sub(f_q, mload(0x2da0)), f_q)) -mstore(0x2de0, mulmod(mload(0x2dc0), mload(0x2b60), f_q)) -mstore(0x2e00, addmod(mload(0x2a80), mload(0x2de0), f_q)) -mstore(0x2e20, mulmod(mload(0x7c0), mload(0x2e00), f_q)) -mstore(0x2e40, mulmod(mload(0xc60), mload(0x580), f_q)) -mstore(0x2e60, addmod(mload(0x9a0), mload(0x2e40), f_q)) -mstore(0x2e80, addmod(mload(0x2e60), mload(0x5e0), f_q)) -mstore(0x2ea0, mulmod(mload(0xc80), mload(0x580), f_q)) -mstore(0x2ec0, addmod(mload(0xa20), mload(0x2ea0), f_q)) -mstore(0x2ee0, addmod(mload(0x2ec0), mload(0x5e0), f_q)) -mstore(0x2f00, mulmod(mload(0x2ee0), mload(0x2e80), f_q)) -mstore(0x2f20, mulmod(mload(0x2f00), mload(0xd80), f_q)) -mstore(0x2f40, mulmod(8910878055287538404433155982483128285667088683464058436815641868457422632747, mload(0x580), f_q)) -mstore(0x2f60, mulmod(mload(0x8e0), mload(0x2f40), f_q)) -mstore(0x2f80, addmod(mload(0x9a0), mload(0x2f60), f_q)) -mstore(0x2fa0, addmod(mload(0x2f80), mload(0x5e0), f_q)) -mstore(0x2fc0, mulmod(11166246659983828508719468090013646171463329086121580628794302409516816350802, mload(0x580), f_q)) -mstore(0x2fe0, mulmod(mload(0x8e0), mload(0x2fc0), f_q)) -mstore(0x3000, addmod(mload(0xa20), mload(0x2fe0), f_q)) -mstore(0x3020, addmod(mload(0x3000), mload(0x5e0), f_q)) -mstore(0x3040, mulmod(mload(0x3020), mload(0x2fa0), f_q)) -mstore(0x3060, mulmod(mload(0x3040), mload(0xd60), f_q)) -mstore(0x3080, addmod(mload(0x2f20), sub(f_q, mload(0x3060)), f_q)) -mstore(0x30a0, mulmod(mload(0x3080), mload(0x2b60), f_q)) -mstore(0x30c0, addmod(mload(0x2e20), mload(0x30a0), f_q)) -mstore(0x30e0, mulmod(mload(0x7c0), mload(0x30c0), f_q)) -mstore(0x3100, mulmod(mload(0xca0), mload(0x580), f_q)) -mstore(0x3120, addmod(mload(0xaa0), mload(0x3100), f_q)) -mstore(0x3140, addmod(mload(0x3120), mload(0x5e0), f_q)) -mstore(0x3160, mulmod(mload(0xcc0), mload(0x580), f_q)) -mstore(0x3180, addmod(mload(0xb20), mload(0x3160), f_q)) -mstore(0x31a0, addmod(mload(0x3180), mload(0x5e0), f_q)) -mstore(0x31c0, mulmod(mload(0x31a0), mload(0x3140), f_q)) -mstore(0x31e0, mulmod(mload(0x31c0), mload(0xde0), f_q)) -mstore(0x3200, mulmod(284840088355319032285349970403338060113257071685626700086398481893096618818, mload(0x580), f_q)) -mstore(0x3220, mulmod(mload(0x8e0), mload(0x3200), f_q)) -mstore(0x3240, addmod(mload(0xaa0), mload(0x3220), f_q)) -mstore(0x3260, addmod(mload(0x3240), mload(0x5e0), f_q)) -mstore(0x3280, mulmod(21134065618345176623193549882539580312263652408302468683943992798037078993309, mload(0x580), f_q)) -mstore(0x32a0, mulmod(mload(0x8e0), mload(0x3280), f_q)) -mstore(0x32c0, addmod(mload(0xb20), mload(0x32a0), f_q)) -mstore(0x32e0, addmod(mload(0x32c0), mload(0x5e0), f_q)) -mstore(0x3300, mulmod(mload(0x32e0), mload(0x3260), f_q)) -mstore(0x3320, mulmod(mload(0x3300), mload(0xdc0), f_q)) -mstore(0x3340, addmod(mload(0x31e0), sub(f_q, mload(0x3320)), f_q)) -mstore(0x3360, mulmod(mload(0x3340), mload(0x2b60), f_q)) -mstore(0x3380, addmod(mload(0x30e0), mload(0x3360), f_q)) -mstore(0x33a0, mulmod(mload(0x7c0), mload(0x3380), f_q)) -mstore(0x33c0, mulmod(mload(0xce0), mload(0x580), f_q)) -mstore(0x33e0, addmod(mload(0x2500), mload(0x33c0), f_q)) -mstore(0x3400, addmod(mload(0x33e0), mload(0x5e0), f_q)) -mstore(0x3420, mulmod(mload(0x3400), mload(0xe40), f_q)) -mstore(0x3440, mulmod(5625741653535312224677218588085279924365897425605943700675464992185016992283, mload(0x580), f_q)) -mstore(0x3460, mulmod(mload(0x8e0), mload(0x3440), f_q)) -mstore(0x3480, addmod(mload(0x2500), mload(0x3460), f_q)) -mstore(0x34a0, addmod(mload(0x3480), mload(0x5e0), f_q)) -mstore(0x34c0, mulmod(mload(0x34a0), mload(0xe20), f_q)) -mstore(0x34e0, addmod(mload(0x3420), sub(f_q, mload(0x34c0)), f_q)) -mstore(0x3500, mulmod(mload(0x34e0), mload(0x2b60), f_q)) -mstore(0x3520, addmod(mload(0x33a0), mload(0x3500), f_q)) -mstore(0x3540, mulmod(mload(0x7c0), mload(0x3520), f_q)) -mstore(0x3560, addmod(1, sub(f_q, mload(0xe60)), f_q)) -mstore(0x3580, mulmod(mload(0x3560), mload(0x21e0), f_q)) -mstore(0x35a0, addmod(mload(0x3540), mload(0x3580), f_q)) -mstore(0x35c0, mulmod(mload(0x7c0), mload(0x35a0), f_q)) -mstore(0x35e0, mulmod(mload(0xe60), mload(0xe60), f_q)) -mstore(0x3600, addmod(mload(0x35e0), sub(f_q, mload(0xe60)), f_q)) -mstore(0x3620, mulmod(mload(0x3600), mload(0x2100), f_q)) -mstore(0x3640, addmod(mload(0x35c0), mload(0x3620), f_q)) -mstore(0x3660, mulmod(mload(0x7c0), mload(0x3640), f_q)) -mstore(0x3680, addmod(mload(0xea0), mload(0x580), f_q)) -mstore(0x36a0, mulmod(mload(0x3680), mload(0xe80), f_q)) -mstore(0x36c0, addmod(mload(0xee0), mload(0x5e0), f_q)) -mstore(0x36e0, mulmod(mload(0x36c0), mload(0x36a0), f_q)) -mstore(0x3700, addmod(mload(0xb20), mload(0x580), f_q)) -mstore(0x3720, mulmod(mload(0x3700), mload(0xe60), f_q)) -mstore(0x3740, addmod(mload(0xb60), mload(0x5e0), f_q)) -mstore(0x3760, mulmod(mload(0x3740), mload(0x3720), f_q)) -mstore(0x3780, addmod(mload(0x36e0), sub(f_q, mload(0x3760)), f_q)) -mstore(0x37a0, mulmod(mload(0x3780), mload(0x2b60), f_q)) -mstore(0x37c0, addmod(mload(0x3660), mload(0x37a0), f_q)) -mstore(0x37e0, mulmod(mload(0x7c0), mload(0x37c0), f_q)) -mstore(0x3800, addmod(mload(0xea0), sub(f_q, mload(0xee0)), f_q)) -mstore(0x3820, mulmod(mload(0x3800), mload(0x21e0), f_q)) -mstore(0x3840, addmod(mload(0x37e0), mload(0x3820), f_q)) -mstore(0x3860, mulmod(mload(0x7c0), mload(0x3840), f_q)) -mstore(0x3880, mulmod(mload(0x3800), mload(0x2b60), f_q)) -mstore(0x38a0, addmod(mload(0xea0), sub(f_q, mload(0xec0)), f_q)) -mstore(0x38c0, mulmod(mload(0x38a0), mload(0x3880), f_q)) -mstore(0x38e0, addmod(mload(0x3860), mload(0x38c0), f_q)) -mstore(0x3900, mulmod(mload(0x13c0), mload(0x13c0), f_q)) -mstore(0x3920, mulmod(mload(0x3900), mload(0x13c0), f_q)) -mstore(0x3940, mulmod(1, mload(0x13c0), f_q)) -mstore(0x3960, mulmod(1, mload(0x3900), f_q)) -mstore(0x3980, mulmod(mload(0x38e0), mload(0x13e0), f_q)) -mstore(0x39a0, mulmod(mload(0x1120), mload(0x8e0), f_q)) -mstore(0x39c0, mulmod(mload(0x39a0), mload(0x8e0), f_q)) -mstore(0x39e0, mulmod(mload(0x8e0), 1, f_q)) -mstore(0x3a00, addmod(mload(0x1020), sub(f_q, mload(0x39e0)), f_q)) -mstore(0x3a20, mulmod(mload(0x8e0), 1426404432721484388505361748317961535523355871255605456897797744433766488507, f_q)) -mstore(0x3a40, addmod(mload(0x1020), sub(f_q, mload(0x3a20)), f_q)) -mstore(0x3a60, mulmod(mload(0x8e0), 8734126352828345679573237859165904705806588461301144420590422589042130041188, f_q)) -mstore(0x3a80, addmod(mload(0x1020), sub(f_q, mload(0x3a60)), f_q)) -mstore(0x3aa0, mulmod(mload(0x8e0), 11211301017135681023579411905410872569206244553457844956874280139879520583390, f_q)) -mstore(0x3ac0, addmod(mload(0x1020), sub(f_q, mload(0x3aa0)), f_q)) -mstore(0x3ae0, mulmod(mload(0x8e0), 12619617507853212586156872920672483948819476989779550311307282715684870266992, f_q)) -mstore(0x3b00, addmod(mload(0x1020), sub(f_q, mload(0x3ae0)), f_q)) -mstore(0x3b20, mulmod(mload(0x8e0), 13225785879531581993054172815365636627224369411478295502904397545373139154045, f_q)) -mstore(0x3b40, addmod(mload(0x1020), sub(f_q, mload(0x3b20)), f_q)) -{ - let result := mulmod(mload(0x1020), mulmod(mload(0x39a0), 3544324119167359571073009690693121464267965232733679586767649244433889388945, f_q), f_q) -result := addmod(mulmod(mload(0x8e0), mulmod(mload(0x39a0), 18343918752671915651173396054564153624280399167682354756930554942141919106672, f_q), f_q), result, f_q) -mstore(15200, result) - } -{ - let result := mulmod(mload(0x1020), mulmod(mload(0x39a0), 3860370625838117017501327045244227871206764201116468958063324100051382735289, f_q), f_q) -result := addmod(mulmod(mload(0x8e0), mulmod(mload(0x39a0), 21616901807277407275624036604424346159916096890712898844034238973395610537327, f_q), f_q), result, f_q) -mstore(15232, result) - } -{ - let result := mulmod(mload(0x1020), mulmod(mload(0x39a0), 21616901807277407275624036604424346159916096890712898844034238973395610537327, f_q), f_q) -result := addmod(mulmod(mload(0x8e0), mulmod(mload(0x39a0), 889236556954614024749610889108815341999962898269585485843658889664869519176, f_q), f_q), result, f_q) -mstore(15264, result) - } -{ - let result := mulmod(mload(0x1020), mulmod(mload(0x39a0), 3209408481237076479025468386201293941554240476766691830436732310949352383503, f_q), f_q) -result := addmod(mulmod(mload(0x8e0), mulmod(mload(0x39a0), 12080394110851700286656425387058292751221637853580771255128961096834426654570, f_q), f_q), result, f_q) -mstore(15296, result) - } -mstore(0x3be0, mulmod(1, mload(0x3a00), f_q)) -mstore(0x3c00, mulmod(mload(0x3be0), mload(0x3ac0), f_q)) -mstore(0x3c20, mulmod(mload(0x3c00), mload(0x3a40), f_q)) -mstore(0x3c40, mulmod(mload(0x3c20), mload(0x3b00), f_q)) -{ - let result := mulmod(mload(0x1020), 1, f_q) -result := addmod(mulmod(mload(0x8e0), 21888242871839275222246405745257275088548364400416034343698204186575808495616, f_q), result, f_q) -mstore(15456, result) - } -{ - let result := mulmod(mload(0x1020), mulmod(mload(0x1120), 8390819244605639573390577733158868133682115698337564550620146375401109684432, f_q), f_q) -result := addmod(mulmod(mload(0x8e0), mulmod(mload(0x1120), 13497423627233635648855828012098406954866248702078469793078057811174698811185, f_q), f_q), result, f_q) -mstore(15488, result) - } -{ - let result := mulmod(mload(0x1020), mulmod(mload(0x1120), 14389468897523033212448771694851898440525479866834419679925499462425232628530, f_q), f_q) -result := addmod(mulmod(mload(0x8e0), mulmod(mload(0x1120), 10771624105926513343199793365135253961557027396599172824137553349410803667382, f_q), f_q), result, f_q) -mstore(15520, result) - } -{ - let result := mulmod(mload(0x1020), mulmod(mload(0x1120), 8021781111580269725587432039983408559403601261632071736490564397134126857583, f_q), f_q) -result := addmod(mulmod(mload(0x8e0), mulmod(mload(0x1120), 13263758384809315129424392494083758423780924407584659157289746760747196496964, f_q), f_q), result, f_q) -mstore(15552, result) - } -mstore(0x3ce0, mulmod(mload(0x3c00), mload(0x3b40), f_q)) -{ - let result := mulmod(mload(0x1020), mulmod(mload(0x8e0), 10676941854703594198666993839846402519342119846958189386823924046696287912228, f_q), f_q) -result := addmod(mulmod(mload(0x8e0), mulmod(mload(0x8e0), 11211301017135681023579411905410872569206244553457844956874280139879520583389, f_q), f_q), result, f_q) -mstore(15616, result) - } -{ - let result := mulmod(mload(0x1020), mulmod(mload(0x8e0), 11211301017135681023579411905410872569206244553457844956874280139879520583389, f_q), f_q) -result := addmod(mulmod(mload(0x8e0), mulmod(mload(0x8e0), 9784896584414196635074050157092911033682888682202239499976482395445754094883, f_q), f_q), result, f_q) -mstore(15648, result) - } -{ - let result := mulmod(mload(0x1020), mulmod(mload(0x8e0), 13154116519010929542673167886091370382741775939114889923107781597533678454430, f_q), f_q) -result := addmod(mulmod(mload(0x8e0), mulmod(mload(0x8e0), 8734126352828345679573237859165904705806588461301144420590422589042130041187, f_q), f_q), result, f_q) -mstore(15680, result) - } -{ - let result := mulmod(mload(0x1020), mulmod(mload(0x8e0), 8734126352828345679573237859165904705806588461301144420590422589042130041187, f_q), f_q) -result := addmod(mulmod(mload(0x8e0), mulmod(mload(0x8e0), 5948611796446669599396300148285100597158677068822442314729736978662760216172, f_q), f_q), result, f_q) -mstore(15712, result) - } -mstore(0x3d80, mulmod(mload(0x3be0), mload(0x3a80), f_q)) -{ - let prod := mload(0x3b60) - - prod := mulmod(mload(0x3b80), prod, f_q) - mstore(0x3da0, prod) - - prod := mulmod(mload(0x3ba0), prod, f_q) - mstore(0x3dc0, prod) - - prod := mulmod(mload(0x3bc0), prod, f_q) - mstore(0x3de0, prod) - - prod := mulmod(mload(0x3c60), prod, f_q) - mstore(0x3e00, prod) - - prod := mulmod(mload(0x3be0), prod, f_q) - mstore(0x3e20, prod) - - prod := mulmod(mload(0x3c80), prod, f_q) - mstore(0x3e40, prod) - - prod := mulmod(mload(0x3ca0), prod, f_q) - mstore(0x3e60, prod) - - prod := mulmod(mload(0x3cc0), prod, f_q) - mstore(0x3e80, prod) - - prod := mulmod(mload(0x3ce0), prod, f_q) - mstore(0x3ea0, prod) - - prod := mulmod(mload(0x3d00), prod, f_q) - mstore(0x3ec0, prod) - - prod := mulmod(mload(0x3d20), prod, f_q) - mstore(0x3ee0, prod) - - prod := mulmod(mload(0x3c00), prod, f_q) - mstore(0x3f00, prod) - - prod := mulmod(mload(0x3d40), prod, f_q) - mstore(0x3f20, prod) - - prod := mulmod(mload(0x3d60), prod, f_q) - mstore(0x3f40, prod) - - prod := mulmod(mload(0x3d80), prod, f_q) - mstore(0x3f60, prod) - - } -mstore(0x3fa0, 32) -mstore(0x3fc0, 32) -mstore(0x3fe0, 32) -mstore(0x4000, mload(0x3f60)) -mstore(0x4020, 21888242871839275222246405745257275088548364400416034343698204186575808495615) -mstore(0x4040, 21888242871839275222246405745257275088548364400416034343698204186575808495617) -success := and(eq(staticcall(gas(), 0x5, 0x3fa0, 0xc0, 0x3f80, 0x20), 1), success) -{ - - let inv := mload(0x3f80) - let v - - v := mload(0x3d80) - mstore(15744, mulmod(mload(0x3f40), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x3d60) - mstore(15712, mulmod(mload(0x3f20), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x3d40) - mstore(15680, mulmod(mload(0x3f00), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x3c00) - mstore(15360, mulmod(mload(0x3ee0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x3d20) - mstore(15648, mulmod(mload(0x3ec0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x3d00) - mstore(15616, mulmod(mload(0x3ea0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x3ce0) - mstore(15584, mulmod(mload(0x3e80), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x3cc0) - mstore(15552, mulmod(mload(0x3e60), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x3ca0) - mstore(15520, mulmod(mload(0x3e40), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x3c80) - mstore(15488, mulmod(mload(0x3e20), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x3be0) - mstore(15328, mulmod(mload(0x3e00), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x3c60) - mstore(15456, mulmod(mload(0x3de0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x3bc0) - mstore(15296, mulmod(mload(0x3dc0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x3ba0) - mstore(15264, mulmod(mload(0x3da0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x3b80) - mstore(15232, mulmod(mload(0x3b60), inv, f_q)) - inv := mulmod(v, inv, f_q) - mstore(0x3b60, inv) - - } -{ - let result := mload(0x3b60) -result := addmod(mload(0x3b80), result, f_q) -result := addmod(mload(0x3ba0), result, f_q) -result := addmod(mload(0x3bc0), result, f_q) -mstore(16480, result) - } -mstore(0x4080, mulmod(mload(0x3c40), mload(0x3be0), f_q)) -{ - let result := mload(0x3c60) -mstore(16544, result) - } -mstore(0x40c0, mulmod(mload(0x3c40), mload(0x3ce0), f_q)) -{ - let result := mload(0x3c80) -result := addmod(mload(0x3ca0), result, f_q) -result := addmod(mload(0x3cc0), result, f_q) -mstore(16608, result) - } -mstore(0x4100, mulmod(mload(0x3c40), mload(0x3c00), f_q)) -{ - let result := mload(0x3d00) -result := addmod(mload(0x3d20), result, f_q) -mstore(16672, result) - } -mstore(0x4140, mulmod(mload(0x3c40), mload(0x3d80), f_q)) -{ - let result := mload(0x3d40) -result := addmod(mload(0x3d60), result, f_q) -mstore(16736, result) - } -{ - let prod := mload(0x4060) - - prod := mulmod(mload(0x40a0), prod, f_q) - mstore(0x4180, prod) - - prod := mulmod(mload(0x40e0), prod, f_q) - mstore(0x41a0, prod) - - prod := mulmod(mload(0x4120), prod, f_q) - mstore(0x41c0, prod) - - prod := mulmod(mload(0x4160), prod, f_q) - mstore(0x41e0, prod) - - } -mstore(0x4220, 32) -mstore(0x4240, 32) -mstore(0x4260, 32) -mstore(0x4280, mload(0x41e0)) -mstore(0x42a0, 21888242871839275222246405745257275088548364400416034343698204186575808495615) -mstore(0x42c0, 21888242871839275222246405745257275088548364400416034343698204186575808495617) -success := and(eq(staticcall(gas(), 0x5, 0x4220, 0xc0, 0x4200, 0x20), 1), success) -{ - - let inv := mload(0x4200) - let v - - v := mload(0x4160) - mstore(16736, mulmod(mload(0x41c0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x4120) - mstore(16672, mulmod(mload(0x41a0), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x40e0) - mstore(16608, mulmod(mload(0x4180), inv, f_q)) - inv := mulmod(v, inv, f_q) - - v := mload(0x40a0) - mstore(16544, mulmod(mload(0x4060), inv, f_q)) - inv := mulmod(v, inv, f_q) - mstore(0x4060, inv) - - } -mstore(0x42e0, mulmod(mload(0x4080), mload(0x40a0), f_q)) -mstore(0x4300, mulmod(mload(0x40c0), mload(0x40e0), f_q)) -mstore(0x4320, mulmod(mload(0x4100), mload(0x4120), f_q)) -mstore(0x4340, mulmod(mload(0x4140), mload(0x4160), f_q)) -mstore(0x4360, mulmod(mload(0xf20), mload(0xf20), f_q)) -mstore(0x4380, mulmod(mload(0x4360), mload(0xf20), f_q)) -mstore(0x43a0, mulmod(mload(0x4380), mload(0xf20), f_q)) -mstore(0x43c0, mulmod(mload(0x43a0), mload(0xf20), f_q)) -mstore(0x43e0, mulmod(mload(0x43c0), mload(0xf20), f_q)) -mstore(0x4400, mulmod(mload(0x43e0), mload(0xf20), f_q)) -mstore(0x4420, mulmod(mload(0x4400), mload(0xf20), f_q)) -mstore(0x4440, mulmod(mload(0x4420), mload(0xf20), f_q)) -mstore(0x4460, mulmod(mload(0x4440), mload(0xf20), f_q)) -mstore(0x4480, mulmod(mload(0x4460), mload(0xf20), f_q)) -mstore(0x44a0, mulmod(mload(0x4480), mload(0xf20), f_q)) -mstore(0x44c0, mulmod(mload(0x44a0), mload(0xf20), f_q)) -mstore(0x44e0, mulmod(mload(0x44c0), mload(0xf20), f_q)) -mstore(0x4500, mulmod(mload(0x44e0), mload(0xf20), f_q)) -mstore(0x4520, mulmod(mload(0x4500), mload(0xf20), f_q)) -mstore(0x4540, mulmod(mload(0x4520), mload(0xf20), f_q)) -mstore(0x4560, mulmod(mload(0xf80), mload(0xf80), f_q)) -mstore(0x4580, mulmod(mload(0x4560), mload(0xf80), f_q)) -mstore(0x45a0, mulmod(mload(0x4580), mload(0xf80), f_q)) -mstore(0x45c0, mulmod(mload(0x45a0), mload(0xf80), f_q)) -{ - let result := mulmod(mload(0x920), mload(0x3b60), f_q) -result := addmod(mulmod(mload(0x940), mload(0x3b80), f_q), result, f_q) -result := addmod(mulmod(mload(0x960), mload(0x3ba0), f_q), result, f_q) -result := addmod(mulmod(mload(0x980), mload(0x3bc0), f_q), result, f_q) -mstore(17888, result) - } -mstore(0x4600, mulmod(mload(0x45e0), mload(0x4060), f_q)) -mstore(0x4620, mulmod(sub(f_q, mload(0x4600)), 1, f_q)) -{ - let result := mulmod(mload(0x9a0), mload(0x3b60), f_q) -result := addmod(mulmod(mload(0x9c0), mload(0x3b80), f_q), result, f_q) -result := addmod(mulmod(mload(0x9e0), mload(0x3ba0), f_q), result, f_q) -result := addmod(mulmod(mload(0xa00), mload(0x3bc0), f_q), result, f_q) -mstore(17984, result) - } -mstore(0x4660, mulmod(mload(0x4640), mload(0x4060), f_q)) -mstore(0x4680, mulmod(sub(f_q, mload(0x4660)), mload(0xf20), f_q)) -mstore(0x46a0, mulmod(1, mload(0xf20), f_q)) -mstore(0x46c0, addmod(mload(0x4620), mload(0x4680), f_q)) -{ - let result := mulmod(mload(0xa20), mload(0x3b60), f_q) -result := addmod(mulmod(mload(0xa40), mload(0x3b80), f_q), result, f_q) -result := addmod(mulmod(mload(0xa60), mload(0x3ba0), f_q), result, f_q) -result := addmod(mulmod(mload(0xa80), mload(0x3bc0), f_q), result, f_q) -mstore(18144, result) - } -mstore(0x4700, mulmod(mload(0x46e0), mload(0x4060), f_q)) -mstore(0x4720, mulmod(sub(f_q, mload(0x4700)), mload(0x4360), f_q)) -mstore(0x4740, mulmod(1, mload(0x4360), f_q)) -mstore(0x4760, addmod(mload(0x46c0), mload(0x4720), f_q)) -{ - let result := mulmod(mload(0xaa0), mload(0x3b60), f_q) -result := addmod(mulmod(mload(0xac0), mload(0x3b80), f_q), result, f_q) -result := addmod(mulmod(mload(0xae0), mload(0x3ba0), f_q), result, f_q) -result := addmod(mulmod(mload(0xb00), mload(0x3bc0), f_q), result, f_q) -mstore(18304, result) - } -mstore(0x47a0, mulmod(mload(0x4780), mload(0x4060), f_q)) -mstore(0x47c0, mulmod(sub(f_q, mload(0x47a0)), mload(0x4380), f_q)) -mstore(0x47e0, mulmod(1, mload(0x4380), f_q)) -mstore(0x4800, addmod(mload(0x4760), mload(0x47c0), f_q)) -mstore(0x4820, mulmod(mload(0x4800), 1, f_q)) -mstore(0x4840, mulmod(mload(0x46a0), 1, f_q)) -mstore(0x4860, mulmod(mload(0x4740), 1, f_q)) -mstore(0x4880, mulmod(mload(0x47e0), 1, f_q)) -mstore(0x48a0, mulmod(1, mload(0x4080), f_q)) -{ - let result := mulmod(mload(0xb20), mload(0x3c60), f_q) -mstore(18624, result) - } -mstore(0x48e0, mulmod(mload(0x48c0), mload(0x42e0), f_q)) -mstore(0x4900, mulmod(sub(f_q, mload(0x48e0)), 1, f_q)) -mstore(0x4920, mulmod(mload(0x48a0), 1, f_q)) -{ - let result := mulmod(mload(0xee0), mload(0x3c60), f_q) -mstore(18752, result) - } -mstore(0x4960, mulmod(mload(0x4940), mload(0x42e0), f_q)) -mstore(0x4980, mulmod(sub(f_q, mload(0x4960)), mload(0xf20), f_q)) -mstore(0x49a0, mulmod(mload(0x48a0), mload(0xf20), f_q)) -mstore(0x49c0, addmod(mload(0x4900), mload(0x4980), f_q)) -{ - let result := mulmod(mload(0xb40), mload(0x3c60), f_q) -mstore(18912, result) - } -mstore(0x4a00, mulmod(mload(0x49e0), mload(0x42e0), f_q)) -mstore(0x4a20, mulmod(sub(f_q, mload(0x4a00)), mload(0x4360), f_q)) -mstore(0x4a40, mulmod(mload(0x48a0), mload(0x4360), f_q)) -mstore(0x4a60, addmod(mload(0x49c0), mload(0x4a20), f_q)) -{ - let result := mulmod(mload(0xb60), mload(0x3c60), f_q) -mstore(19072, result) - } -mstore(0x4aa0, mulmod(mload(0x4a80), mload(0x42e0), f_q)) -mstore(0x4ac0, mulmod(sub(f_q, mload(0x4aa0)), mload(0x4380), f_q)) -mstore(0x4ae0, mulmod(mload(0x48a0), mload(0x4380), f_q)) -mstore(0x4b00, addmod(mload(0x4a60), mload(0x4ac0), f_q)) -{ - let result := mulmod(mload(0xb80), mload(0x3c60), f_q) -mstore(19232, result) - } -mstore(0x4b40, mulmod(mload(0x4b20), mload(0x42e0), f_q)) -mstore(0x4b60, mulmod(sub(f_q, mload(0x4b40)), mload(0x43a0), f_q)) -mstore(0x4b80, mulmod(mload(0x48a0), mload(0x43a0), f_q)) -mstore(0x4ba0, addmod(mload(0x4b00), mload(0x4b60), f_q)) -{ - let result := mulmod(mload(0xba0), mload(0x3c60), f_q) -mstore(19392, result) - } -mstore(0x4be0, mulmod(mload(0x4bc0), mload(0x42e0), f_q)) -mstore(0x4c00, mulmod(sub(f_q, mload(0x4be0)), mload(0x43c0), f_q)) -mstore(0x4c20, mulmod(mload(0x48a0), mload(0x43c0), f_q)) -mstore(0x4c40, addmod(mload(0x4ba0), mload(0x4c00), f_q)) -{ - let result := mulmod(mload(0xbc0), mload(0x3c60), f_q) -mstore(19552, result) - } -mstore(0x4c80, mulmod(mload(0x4c60), mload(0x42e0), f_q)) -mstore(0x4ca0, mulmod(sub(f_q, mload(0x4c80)), mload(0x43e0), f_q)) -mstore(0x4cc0, mulmod(mload(0x48a0), mload(0x43e0), f_q)) -mstore(0x4ce0, addmod(mload(0x4c40), mload(0x4ca0), f_q)) -{ - let result := mulmod(mload(0xbe0), mload(0x3c60), f_q) -mstore(19712, result) - } -mstore(0x4d20, mulmod(mload(0x4d00), mload(0x42e0), f_q)) -mstore(0x4d40, mulmod(sub(f_q, mload(0x4d20)), mload(0x4400), f_q)) -mstore(0x4d60, mulmod(mload(0x48a0), mload(0x4400), f_q)) -mstore(0x4d80, addmod(mload(0x4ce0), mload(0x4d40), f_q)) -{ - let result := mulmod(mload(0xc20), mload(0x3c60), f_q) -mstore(19872, result) - } -mstore(0x4dc0, mulmod(mload(0x4da0), mload(0x42e0), f_q)) -mstore(0x4de0, mulmod(sub(f_q, mload(0x4dc0)), mload(0x4420), f_q)) -mstore(0x4e00, mulmod(mload(0x48a0), mload(0x4420), f_q)) -mstore(0x4e20, addmod(mload(0x4d80), mload(0x4de0), f_q)) -{ - let result := mulmod(mload(0xc40), mload(0x3c60), f_q) -mstore(20032, result) - } -mstore(0x4e60, mulmod(mload(0x4e40), mload(0x42e0), f_q)) -mstore(0x4e80, mulmod(sub(f_q, mload(0x4e60)), mload(0x4440), f_q)) -mstore(0x4ea0, mulmod(mload(0x48a0), mload(0x4440), f_q)) -mstore(0x4ec0, addmod(mload(0x4e20), mload(0x4e80), f_q)) -{ - let result := mulmod(mload(0xc60), mload(0x3c60), f_q) -mstore(20192, result) - } -mstore(0x4f00, mulmod(mload(0x4ee0), mload(0x42e0), f_q)) -mstore(0x4f20, mulmod(sub(f_q, mload(0x4f00)), mload(0x4460), f_q)) -mstore(0x4f40, mulmod(mload(0x48a0), mload(0x4460), f_q)) -mstore(0x4f60, addmod(mload(0x4ec0), mload(0x4f20), f_q)) -{ - let result := mulmod(mload(0xc80), mload(0x3c60), f_q) -mstore(20352, result) - } -mstore(0x4fa0, mulmod(mload(0x4f80), mload(0x42e0), f_q)) -mstore(0x4fc0, mulmod(sub(f_q, mload(0x4fa0)), mload(0x4480), f_q)) -mstore(0x4fe0, mulmod(mload(0x48a0), mload(0x4480), f_q)) -mstore(0x5000, addmod(mload(0x4f60), mload(0x4fc0), f_q)) -{ - let result := mulmod(mload(0xca0), mload(0x3c60), f_q) -mstore(20512, result) - } -mstore(0x5040, mulmod(mload(0x5020), mload(0x42e0), f_q)) -mstore(0x5060, mulmod(sub(f_q, mload(0x5040)), mload(0x44a0), f_q)) -mstore(0x5080, mulmod(mload(0x48a0), mload(0x44a0), f_q)) -mstore(0x50a0, addmod(mload(0x5000), mload(0x5060), f_q)) -{ - let result := mulmod(mload(0xcc0), mload(0x3c60), f_q) -mstore(20672, result) - } -mstore(0x50e0, mulmod(mload(0x50c0), mload(0x42e0), f_q)) -mstore(0x5100, mulmod(sub(f_q, mload(0x50e0)), mload(0x44c0), f_q)) -mstore(0x5120, mulmod(mload(0x48a0), mload(0x44c0), f_q)) -mstore(0x5140, addmod(mload(0x50a0), mload(0x5100), f_q)) -{ - let result := mulmod(mload(0xce0), mload(0x3c60), f_q) -mstore(20832, result) - } -mstore(0x5180, mulmod(mload(0x5160), mload(0x42e0), f_q)) -mstore(0x51a0, mulmod(sub(f_q, mload(0x5180)), mload(0x44e0), f_q)) -mstore(0x51c0, mulmod(mload(0x48a0), mload(0x44e0), f_q)) -mstore(0x51e0, addmod(mload(0x5140), mload(0x51a0), f_q)) -mstore(0x5200, mulmod(mload(0x3940), mload(0x4080), f_q)) -mstore(0x5220, mulmod(mload(0x3960), mload(0x4080), f_q)) -{ - let result := mulmod(mload(0x3980), mload(0x3c60), f_q) -mstore(21056, result) - } -mstore(0x5260, mulmod(mload(0x5240), mload(0x42e0), f_q)) -mstore(0x5280, mulmod(sub(f_q, mload(0x5260)), mload(0x4500), f_q)) -mstore(0x52a0, mulmod(mload(0x48a0), mload(0x4500), f_q)) -mstore(0x52c0, mulmod(mload(0x5200), mload(0x4500), f_q)) -mstore(0x52e0, mulmod(mload(0x5220), mload(0x4500), f_q)) -mstore(0x5300, addmod(mload(0x51e0), mload(0x5280), f_q)) -{ - let result := mulmod(mload(0xc00), mload(0x3c60), f_q) -mstore(21280, result) - } -mstore(0x5340, mulmod(mload(0x5320), mload(0x42e0), f_q)) -mstore(0x5360, mulmod(sub(f_q, mload(0x5340)), mload(0x4520), f_q)) -mstore(0x5380, mulmod(mload(0x48a0), mload(0x4520), f_q)) -mstore(0x53a0, addmod(mload(0x5300), mload(0x5360), f_q)) -mstore(0x53c0, mulmod(mload(0x53a0), mload(0xf80), f_q)) -mstore(0x53e0, mulmod(mload(0x4920), mload(0xf80), f_q)) -mstore(0x5400, mulmod(mload(0x49a0), mload(0xf80), f_q)) -mstore(0x5420, mulmod(mload(0x4a40), mload(0xf80), f_q)) -mstore(0x5440, mulmod(mload(0x4ae0), mload(0xf80), f_q)) -mstore(0x5460, mulmod(mload(0x4b80), mload(0xf80), f_q)) -mstore(0x5480, mulmod(mload(0x4c20), mload(0xf80), f_q)) -mstore(0x54a0, mulmod(mload(0x4cc0), mload(0xf80), f_q)) -mstore(0x54c0, mulmod(mload(0x4d60), mload(0xf80), f_q)) -mstore(0x54e0, mulmod(mload(0x4e00), mload(0xf80), f_q)) -mstore(0x5500, mulmod(mload(0x4ea0), mload(0xf80), f_q)) -mstore(0x5520, mulmod(mload(0x4f40), mload(0xf80), f_q)) -mstore(0x5540, mulmod(mload(0x4fe0), mload(0xf80), f_q)) -mstore(0x5560, mulmod(mload(0x5080), mload(0xf80), f_q)) -mstore(0x5580, mulmod(mload(0x5120), mload(0xf80), f_q)) -mstore(0x55a0, mulmod(mload(0x51c0), mload(0xf80), f_q)) -mstore(0x55c0, mulmod(mload(0x52a0), mload(0xf80), f_q)) -mstore(0x55e0, mulmod(mload(0x52c0), mload(0xf80), f_q)) -mstore(0x5600, mulmod(mload(0x52e0), mload(0xf80), f_q)) -mstore(0x5620, mulmod(mload(0x5380), mload(0xf80), f_q)) -mstore(0x5640, addmod(mload(0x4820), mload(0x53c0), f_q)) -mstore(0x5660, mulmod(1, mload(0x40c0), f_q)) -{ - let result := mulmod(mload(0xd00), mload(0x3c80), f_q) -result := addmod(mulmod(mload(0xd20), mload(0x3ca0), f_q), result, f_q) -result := addmod(mulmod(mload(0xd40), mload(0x3cc0), f_q), result, f_q) -mstore(22144, result) - } -mstore(0x56a0, mulmod(mload(0x5680), mload(0x4300), f_q)) -mstore(0x56c0, mulmod(sub(f_q, mload(0x56a0)), 1, f_q)) -mstore(0x56e0, mulmod(mload(0x5660), 1, f_q)) -{ - let result := mulmod(mload(0xd60), mload(0x3c80), f_q) -result := addmod(mulmod(mload(0xd80), mload(0x3ca0), f_q), result, f_q) -result := addmod(mulmod(mload(0xda0), mload(0x3cc0), f_q), result, f_q) -mstore(22272, result) - } -mstore(0x5720, mulmod(mload(0x5700), mload(0x4300), f_q)) -mstore(0x5740, mulmod(sub(f_q, mload(0x5720)), mload(0xf20), f_q)) -mstore(0x5760, mulmod(mload(0x5660), mload(0xf20), f_q)) -mstore(0x5780, addmod(mload(0x56c0), mload(0x5740), f_q)) -{ - let result := mulmod(mload(0xdc0), mload(0x3c80), f_q) -result := addmod(mulmod(mload(0xde0), mload(0x3ca0), f_q), result, f_q) -result := addmod(mulmod(mload(0xe00), mload(0x3cc0), f_q), result, f_q) -mstore(22432, result) - } -mstore(0x57c0, mulmod(mload(0x57a0), mload(0x4300), f_q)) -mstore(0x57e0, mulmod(sub(f_q, mload(0x57c0)), mload(0x4360), f_q)) -mstore(0x5800, mulmod(mload(0x5660), mload(0x4360), f_q)) -mstore(0x5820, addmod(mload(0x5780), mload(0x57e0), f_q)) -mstore(0x5840, mulmod(mload(0x5820), mload(0x4560), f_q)) -mstore(0x5860, mulmod(mload(0x56e0), mload(0x4560), f_q)) -mstore(0x5880, mulmod(mload(0x5760), mload(0x4560), f_q)) -mstore(0x58a0, mulmod(mload(0x5800), mload(0x4560), f_q)) -mstore(0x58c0, addmod(mload(0x5640), mload(0x5840), f_q)) -mstore(0x58e0, mulmod(1, mload(0x4100), f_q)) -{ - let result := mulmod(mload(0xe20), mload(0x3d00), f_q) -result := addmod(mulmod(mload(0xe40), mload(0x3d20), f_q), result, f_q) -mstore(22784, result) - } -mstore(0x5920, mulmod(mload(0x5900), mload(0x4320), f_q)) -mstore(0x5940, mulmod(sub(f_q, mload(0x5920)), 1, f_q)) -mstore(0x5960, mulmod(mload(0x58e0), 1, f_q)) -{ - let result := mulmod(mload(0xe60), mload(0x3d00), f_q) -result := addmod(mulmod(mload(0xe80), mload(0x3d20), f_q), result, f_q) -mstore(22912, result) - } -mstore(0x59a0, mulmod(mload(0x5980), mload(0x4320), f_q)) -mstore(0x59c0, mulmod(sub(f_q, mload(0x59a0)), mload(0xf20), f_q)) -mstore(0x59e0, mulmod(mload(0x58e0), mload(0xf20), f_q)) -mstore(0x5a00, addmod(mload(0x5940), mload(0x59c0), f_q)) -mstore(0x5a20, mulmod(mload(0x5a00), mload(0x4580), f_q)) -mstore(0x5a40, mulmod(mload(0x5960), mload(0x4580), f_q)) -mstore(0x5a60, mulmod(mload(0x59e0), mload(0x4580), f_q)) -mstore(0x5a80, addmod(mload(0x58c0), mload(0x5a20), f_q)) -mstore(0x5aa0, mulmod(1, mload(0x4140), f_q)) -{ - let result := mulmod(mload(0xea0), mload(0x3d40), f_q) -result := addmod(mulmod(mload(0xec0), mload(0x3d60), f_q), result, f_q) -mstore(23232, result) - } -mstore(0x5ae0, mulmod(mload(0x5ac0), mload(0x4340), f_q)) -mstore(0x5b00, mulmod(sub(f_q, mload(0x5ae0)), 1, f_q)) -mstore(0x5b20, mulmod(mload(0x5aa0), 1, f_q)) -mstore(0x5b40, mulmod(mload(0x5b00), mload(0x45a0), f_q)) -mstore(0x5b60, mulmod(mload(0x5b20), mload(0x45a0), f_q)) -mstore(0x5b80, addmod(mload(0x5a80), mload(0x5b40), f_q)) -mstore(0x5ba0, mulmod(1, mload(0x3c40), f_q)) -mstore(0x5bc0, mulmod(1, mload(0x1020), f_q)) -mstore(0x5be0, 0x0000000000000000000000000000000000000000000000000000000000000001) - mstore(0x5c00, 0x0000000000000000000000000000000000000000000000000000000000000002) -mstore(0x5c20, mload(0x5b80)) -success := and(eq(staticcall(gas(), 0x7, 0x5be0, 0x60, 0x5be0, 0x40), 1), success) -mstore(0x5c40, mload(0x5be0)) - mstore(0x5c60, mload(0x5c00)) -mstore(0x5c80, mload(0x340)) - mstore(0x5ca0, mload(0x360)) -success := and(eq(staticcall(gas(), 0x6, 0x5c40, 0x80, 0x5c40, 0x40), 1), success) -mstore(0x5cc0, mload(0x380)) - mstore(0x5ce0, mload(0x3a0)) -mstore(0x5d00, mload(0x4840)) -success := and(eq(staticcall(gas(), 0x7, 0x5cc0, 0x60, 0x5cc0, 0x40), 1), success) -mstore(0x5d20, mload(0x5c40)) - mstore(0x5d40, mload(0x5c60)) -mstore(0x5d60, mload(0x5cc0)) - mstore(0x5d80, mload(0x5ce0)) -success := and(eq(staticcall(gas(), 0x6, 0x5d20, 0x80, 0x5d20, 0x40), 1), success) -mstore(0x5da0, mload(0x3c0)) - mstore(0x5dc0, mload(0x3e0)) -mstore(0x5de0, mload(0x4860)) -success := and(eq(staticcall(gas(), 0x7, 0x5da0, 0x60, 0x5da0, 0x40), 1), success) -mstore(0x5e00, mload(0x5d20)) - mstore(0x5e20, mload(0x5d40)) -mstore(0x5e40, mload(0x5da0)) - mstore(0x5e60, mload(0x5dc0)) -success := and(eq(staticcall(gas(), 0x6, 0x5e00, 0x80, 0x5e00, 0x40), 1), success) -mstore(0x5e80, mload(0x400)) - mstore(0x5ea0, mload(0x420)) -mstore(0x5ec0, mload(0x4880)) -success := and(eq(staticcall(gas(), 0x7, 0x5e80, 0x60, 0x5e80, 0x40), 1), success) -mstore(0x5ee0, mload(0x5e00)) - mstore(0x5f00, mload(0x5e20)) -mstore(0x5f20, mload(0x5e80)) - mstore(0x5f40, mload(0x5ea0)) -success := and(eq(staticcall(gas(), 0x6, 0x5ee0, 0x80, 0x5ee0, 0x40), 1), success) -mstore(0x5f60, mload(0x440)) - mstore(0x5f80, mload(0x460)) -mstore(0x5fa0, mload(0x53e0)) -success := and(eq(staticcall(gas(), 0x7, 0x5f60, 0x60, 0x5f60, 0x40), 1), success) -mstore(0x5fc0, mload(0x5ee0)) - mstore(0x5fe0, mload(0x5f00)) -mstore(0x6000, mload(0x5f60)) - mstore(0x6020, mload(0x5f80)) -success := and(eq(staticcall(gas(), 0x6, 0x5fc0, 0x80, 0x5fc0, 0x40), 1), success) -mstore(0x6040, mload(0x520)) - mstore(0x6060, mload(0x540)) -mstore(0x6080, mload(0x5400)) -success := and(eq(staticcall(gas(), 0x7, 0x6040, 0x60, 0x6040, 0x40), 1), success) -mstore(0x60a0, mload(0x5fc0)) - mstore(0x60c0, mload(0x5fe0)) -mstore(0x60e0, mload(0x6040)) - mstore(0x6100, mload(0x6060)) -success := and(eq(staticcall(gas(), 0x6, 0x60a0, 0x80, 0x60a0, 0x40), 1), success) -mstore(0x6120, 0x0d312cc48aa0f99b7d45cdbb3b0c065f59c375ce6906b49db395fe10321c5538) - mstore(0x6140, 0x2f84b4e64a0c57b3a721355991e2522c5f0caa9d0d4cb93a11dc638fd085581a) -mstore(0x6160, mload(0x5420)) -success := and(eq(staticcall(gas(), 0x7, 0x6120, 0x60, 0x6120, 0x40), 1), success) -mstore(0x6180, mload(0x60a0)) - mstore(0x61a0, mload(0x60c0)) -mstore(0x61c0, mload(0x6120)) - mstore(0x61e0, mload(0x6140)) -success := and(eq(staticcall(gas(), 0x6, 0x6180, 0x80, 0x6180, 0x40), 1), success) -mstore(0x6200, 0x0e2cfd6511d4d1c61e962f12c418e57becff05ca67c40fd45569857815f2f5cf) - mstore(0x6220, 0x2244b468d846e5da4bf8421f60829c7d3b5ea7be1d05fc80b74e1304c3c54889) -mstore(0x6240, mload(0x5440)) -success := and(eq(staticcall(gas(), 0x7, 0x6200, 0x60, 0x6200, 0x40), 1), success) -mstore(0x6260, mload(0x6180)) - mstore(0x6280, mload(0x61a0)) -mstore(0x62a0, mload(0x6200)) - mstore(0x62c0, mload(0x6220)) -success := and(eq(staticcall(gas(), 0x6, 0x6260, 0x80, 0x6260, 0x40), 1), success) -mstore(0x62e0, 0x213e9b5c40c6d7ab1e319be843af1c99a1da1a199212e0e7b495f1294b32d165) - mstore(0x6300, 0x22bc14a27bb6f51d9bbc1886857eea3590a5558b8fd6095734bbf14ae88a565f) -mstore(0x6320, mload(0x5460)) -success := and(eq(staticcall(gas(), 0x7, 0x62e0, 0x60, 0x62e0, 0x40), 1), success) -mstore(0x6340, mload(0x6260)) - mstore(0x6360, mload(0x6280)) -mstore(0x6380, mload(0x62e0)) - mstore(0x63a0, mload(0x6300)) -success := and(eq(staticcall(gas(), 0x6, 0x6340, 0x80, 0x6340, 0x40), 1), success) -mstore(0x63c0, 0x1d881272547a28d65138142b349a63846b30ba0ccfed12e1d4a650ffdc9ef069) - mstore(0x63e0, 0x04c1bbc1c2e96c0970f73b47d506c78fb0234cbe1b63e45f8d150a4244500dcd) -mstore(0x6400, mload(0x5480)) -success := and(eq(staticcall(gas(), 0x7, 0x63c0, 0x60, 0x63c0, 0x40), 1), success) -mstore(0x6420, mload(0x6340)) - mstore(0x6440, mload(0x6360)) -mstore(0x6460, mload(0x63c0)) - mstore(0x6480, mload(0x63e0)) -success := and(eq(staticcall(gas(), 0x6, 0x6420, 0x80, 0x6420, 0x40), 1), success) -mstore(0x64a0, 0x0d26c5d7f4890bde7d349ce1160ccc9da16c0715113a57f91e420f491dad8f22) - mstore(0x64c0, 0x094e76f5de64f6b50318df604dcbb188f4b1abfdc88aec0ddb68679fff30c129) -mstore(0x64e0, mload(0x54a0)) -success := and(eq(staticcall(gas(), 0x7, 0x64a0, 0x60, 0x64a0, 0x40), 1), success) -mstore(0x6500, mload(0x6420)) - mstore(0x6520, mload(0x6440)) -mstore(0x6540, mload(0x64a0)) - mstore(0x6560, mload(0x64c0)) -success := and(eq(staticcall(gas(), 0x6, 0x6500, 0x80, 0x6500, 0x40), 1), success) -mstore(0x6580, 0x178cda6eece071601ddca6840f562ff46832453c8cdf478b55b9e523a78027ce) - mstore(0x65a0, 0x04f828dec2a0e137a9cf3e950a69df39da1320911552da0a5c589f4e6f0492ca) -mstore(0x65c0, mload(0x54c0)) -success := and(eq(staticcall(gas(), 0x7, 0x6580, 0x60, 0x6580, 0x40), 1), success) -mstore(0x65e0, mload(0x6500)) - mstore(0x6600, mload(0x6520)) -mstore(0x6620, mload(0x6580)) - mstore(0x6640, mload(0x65a0)) -success := and(eq(staticcall(gas(), 0x6, 0x65e0, 0x80, 0x65e0, 0x40), 1), success) -mstore(0x6660, 0x26397f679e4c79e9bb06cef725ce017fc6b8afe1a92420483b333876c5548c2e) - mstore(0x6680, 0x1f4acd941c8113494f1f7ecb22d328f16daf98fd46a172de5c4e1333d75d0e87) -mstore(0x66a0, mload(0x54e0)) -success := and(eq(staticcall(gas(), 0x7, 0x6660, 0x60, 0x6660, 0x40), 1), success) -mstore(0x66c0, mload(0x65e0)) - mstore(0x66e0, mload(0x6600)) -mstore(0x6700, mload(0x6660)) - mstore(0x6720, mload(0x6680)) -success := and(eq(staticcall(gas(), 0x6, 0x66c0, 0x80, 0x66c0, 0x40), 1), success) -mstore(0x6740, 0x1b0282ef1b006c1168a05018aee6ed05febe98a963c5d8164e783b5c5a263ac9) - mstore(0x6760, 0x18d722e4fb14e021718d63b2e36e46ea40639ec3cb404dd1c8d4ff5b7745fc15) -mstore(0x6780, mload(0x5500)) -success := and(eq(staticcall(gas(), 0x7, 0x6740, 0x60, 0x6740, 0x40), 1), success) -mstore(0x67a0, mload(0x66c0)) - mstore(0x67c0, mload(0x66e0)) -mstore(0x67e0, mload(0x6740)) - mstore(0x6800, mload(0x6760)) -success := and(eq(staticcall(gas(), 0x6, 0x67a0, 0x80, 0x67a0, 0x40), 1), success) -mstore(0x6820, 0x24b8713bd1d4e117d7574333f59b3005fd149a1e296e0343cff3fbf5dac48476) - mstore(0x6840, 0x28a20357bcd214278d6d3ea712959c8c677e90f8881563eecd8737885f7b5d36) -mstore(0x6860, mload(0x5520)) -success := and(eq(staticcall(gas(), 0x7, 0x6820, 0x60, 0x6820, 0x40), 1), success) -mstore(0x6880, mload(0x67a0)) - mstore(0x68a0, mload(0x67c0)) -mstore(0x68c0, mload(0x6820)) - mstore(0x68e0, mload(0x6840)) -success := and(eq(staticcall(gas(), 0x6, 0x6880, 0x80, 0x6880, 0x40), 1), success) -mstore(0x6900, 0x22716eab9a52b238d2b2bebddffbcacbc4446f1b278d1ce5eb5273cfd8c6eb07) - mstore(0x6920, 0x1f4ab95a9d5d4f4f9d08e6d47417288c4a830e27b48d02b01ac90d0007cf4a5c) -mstore(0x6940, mload(0x5540)) -success := and(eq(staticcall(gas(), 0x7, 0x6900, 0x60, 0x6900, 0x40), 1), success) -mstore(0x6960, mload(0x6880)) - mstore(0x6980, mload(0x68a0)) -mstore(0x69a0, mload(0x6900)) - mstore(0x69c0, mload(0x6920)) -success := and(eq(staticcall(gas(), 0x6, 0x6960, 0x80, 0x6960, 0x40), 1), success) -mstore(0x69e0, 0x142b840183f331d08e3f15836f6859f923592228e91499804db01ff960387dd6) - mstore(0x6a00, 0x09b7f62dd3b5f489195c10aaf84b69e222dc3ac096c3009dfe40bb38e3ca7086) -mstore(0x6a20, mload(0x5560)) -success := and(eq(staticcall(gas(), 0x7, 0x69e0, 0x60, 0x69e0, 0x40), 1), success) -mstore(0x6a40, mload(0x6960)) - mstore(0x6a60, mload(0x6980)) -mstore(0x6a80, mload(0x69e0)) - mstore(0x6aa0, mload(0x6a00)) -success := and(eq(staticcall(gas(), 0x6, 0x6a40, 0x80, 0x6a40, 0x40), 1), success) -mstore(0x6ac0, 0x2d75ed8054668bb4e6b779881ce4d0e3510c80c25479d3b4fe4d613b0febf149) - mstore(0x6ae0, 0x141cf41139c5907be1e1bdd004cc2314b2867e563044f7468c95a53b1d956d0c) -mstore(0x6b00, mload(0x5580)) -success := and(eq(staticcall(gas(), 0x7, 0x6ac0, 0x60, 0x6ac0, 0x40), 1), success) -mstore(0x6b20, mload(0x6a40)) - mstore(0x6b40, mload(0x6a60)) -mstore(0x6b60, mload(0x6ac0)) - mstore(0x6b80, mload(0x6ae0)) -success := and(eq(staticcall(gas(), 0x6, 0x6b20, 0x80, 0x6b20, 0x40), 1), success) -mstore(0x6ba0, 0x037ff8ee2356bc855124c7330f5aba3993b778fe9cc86ff9ec7a2656e667a1f6) - mstore(0x6bc0, 0x1d8f0ec8c0bdace10b8857d43fccf3238c2e068931a65e5f15aa9adb9f2f5b54) -mstore(0x6be0, mload(0x55a0)) -success := and(eq(staticcall(gas(), 0x7, 0x6ba0, 0x60, 0x6ba0, 0x40), 1), success) -mstore(0x6c00, mload(0x6b20)) - mstore(0x6c20, mload(0x6b40)) -mstore(0x6c40, mload(0x6ba0)) - mstore(0x6c60, mload(0x6bc0)) -success := and(eq(staticcall(gas(), 0x6, 0x6c00, 0x80, 0x6c00, 0x40), 1), success) -mstore(0x6c80, mload(0x800)) - mstore(0x6ca0, mload(0x820)) -mstore(0x6cc0, mload(0x55c0)) -success := and(eq(staticcall(gas(), 0x7, 0x6c80, 0x60, 0x6c80, 0x40), 1), success) -mstore(0x6ce0, mload(0x6c00)) - mstore(0x6d00, mload(0x6c20)) -mstore(0x6d20, mload(0x6c80)) - mstore(0x6d40, mload(0x6ca0)) -success := and(eq(staticcall(gas(), 0x6, 0x6ce0, 0x80, 0x6ce0, 0x40), 1), success) -mstore(0x6d60, mload(0x840)) - mstore(0x6d80, mload(0x860)) -mstore(0x6da0, mload(0x55e0)) -success := and(eq(staticcall(gas(), 0x7, 0x6d60, 0x60, 0x6d60, 0x40), 1), success) -mstore(0x6dc0, mload(0x6ce0)) - mstore(0x6de0, mload(0x6d00)) -mstore(0x6e00, mload(0x6d60)) - mstore(0x6e20, mload(0x6d80)) -success := and(eq(staticcall(gas(), 0x6, 0x6dc0, 0x80, 0x6dc0, 0x40), 1), success) -mstore(0x6e40, mload(0x880)) - mstore(0x6e60, mload(0x8a0)) -mstore(0x6e80, mload(0x5600)) -success := and(eq(staticcall(gas(), 0x7, 0x6e40, 0x60, 0x6e40, 0x40), 1), success) -mstore(0x6ea0, mload(0x6dc0)) - mstore(0x6ec0, mload(0x6de0)) -mstore(0x6ee0, mload(0x6e40)) - mstore(0x6f00, mload(0x6e60)) -success := and(eq(staticcall(gas(), 0x6, 0x6ea0, 0x80, 0x6ea0, 0x40), 1), success) -mstore(0x6f20, mload(0x760)) - mstore(0x6f40, mload(0x780)) -mstore(0x6f60, mload(0x5620)) -success := and(eq(staticcall(gas(), 0x7, 0x6f20, 0x60, 0x6f20, 0x40), 1), success) -mstore(0x6f80, mload(0x6ea0)) - mstore(0x6fa0, mload(0x6ec0)) -mstore(0x6fc0, mload(0x6f20)) - mstore(0x6fe0, mload(0x6f40)) -success := and(eq(staticcall(gas(), 0x6, 0x6f80, 0x80, 0x6f80, 0x40), 1), success) -mstore(0x7000, mload(0x620)) - mstore(0x7020, mload(0x640)) -mstore(0x7040, mload(0x5860)) -success := and(eq(staticcall(gas(), 0x7, 0x7000, 0x60, 0x7000, 0x40), 1), success) -mstore(0x7060, mload(0x6f80)) - mstore(0x7080, mload(0x6fa0)) -mstore(0x70a0, mload(0x7000)) - mstore(0x70c0, mload(0x7020)) -success := and(eq(staticcall(gas(), 0x6, 0x7060, 0x80, 0x7060, 0x40), 1), success) -mstore(0x70e0, mload(0x660)) - mstore(0x7100, mload(0x680)) -mstore(0x7120, mload(0x5880)) -success := and(eq(staticcall(gas(), 0x7, 0x70e0, 0x60, 0x70e0, 0x40), 1), success) -mstore(0x7140, mload(0x7060)) - mstore(0x7160, mload(0x7080)) -mstore(0x7180, mload(0x70e0)) - mstore(0x71a0, mload(0x7100)) -success := and(eq(staticcall(gas(), 0x6, 0x7140, 0x80, 0x7140, 0x40), 1), success) -mstore(0x71c0, mload(0x6a0)) - mstore(0x71e0, mload(0x6c0)) -mstore(0x7200, mload(0x58a0)) -success := and(eq(staticcall(gas(), 0x7, 0x71c0, 0x60, 0x71c0, 0x40), 1), success) -mstore(0x7220, mload(0x7140)) - mstore(0x7240, mload(0x7160)) -mstore(0x7260, mload(0x71c0)) - mstore(0x7280, mload(0x71e0)) -success := and(eq(staticcall(gas(), 0x6, 0x7220, 0x80, 0x7220, 0x40), 1), success) -mstore(0x72a0, mload(0x6e0)) - mstore(0x72c0, mload(0x700)) -mstore(0x72e0, mload(0x5a40)) -success := and(eq(staticcall(gas(), 0x7, 0x72a0, 0x60, 0x72a0, 0x40), 1), success) -mstore(0x7300, mload(0x7220)) - mstore(0x7320, mload(0x7240)) -mstore(0x7340, mload(0x72a0)) - mstore(0x7360, mload(0x72c0)) -success := and(eq(staticcall(gas(), 0x6, 0x7300, 0x80, 0x7300, 0x40), 1), success) -mstore(0x7380, mload(0x720)) - mstore(0x73a0, mload(0x740)) -mstore(0x73c0, mload(0x5a60)) -success := and(eq(staticcall(gas(), 0x7, 0x7380, 0x60, 0x7380, 0x40), 1), success) -mstore(0x73e0, mload(0x7300)) - mstore(0x7400, mload(0x7320)) -mstore(0x7420, mload(0x7380)) - mstore(0x7440, mload(0x73a0)) -success := and(eq(staticcall(gas(), 0x6, 0x73e0, 0x80, 0x73e0, 0x40), 1), success) -mstore(0x7460, mload(0x4e0)) - mstore(0x7480, mload(0x500)) -mstore(0x74a0, mload(0x5b60)) -success := and(eq(staticcall(gas(), 0x7, 0x7460, 0x60, 0x7460, 0x40), 1), success) -mstore(0x74c0, mload(0x73e0)) - mstore(0x74e0, mload(0x7400)) -mstore(0x7500, mload(0x7460)) - mstore(0x7520, mload(0x7480)) -success := and(eq(staticcall(gas(), 0x6, 0x74c0, 0x80, 0x74c0, 0x40), 1), success) -mstore(0x7540, mload(0xfc0)) - mstore(0x7560, mload(0xfe0)) -mstore(0x7580, sub(f_q, mload(0x5ba0))) -success := and(eq(staticcall(gas(), 0x7, 0x7540, 0x60, 0x7540, 0x40), 1), success) -mstore(0x75a0, mload(0x74c0)) - mstore(0x75c0, mload(0x74e0)) -mstore(0x75e0, mload(0x7540)) - mstore(0x7600, mload(0x7560)) -success := and(eq(staticcall(gas(), 0x6, 0x75a0, 0x80, 0x75a0, 0x40), 1), success) -mstore(0x7620, mload(0x1060)) - mstore(0x7640, mload(0x1080)) -mstore(0x7660, mload(0x5bc0)) -success := and(eq(staticcall(gas(), 0x7, 0x7620, 0x60, 0x7620, 0x40), 1), success) -mstore(0x7680, mload(0x75a0)) - mstore(0x76a0, mload(0x75c0)) -mstore(0x76c0, mload(0x7620)) - mstore(0x76e0, mload(0x7640)) -success := and(eq(staticcall(gas(), 0x6, 0x7680, 0x80, 0x7680, 0x40), 1), success) -mstore(0x7700, mload(0x7680)) - mstore(0x7720, mload(0x76a0)) -mstore(0x7740, mload(0x1060)) - mstore(0x7760, mload(0x1080)) -mstore(0x7780, mload(0x10a0)) - mstore(0x77a0, mload(0x10c0)) -mstore(0x77c0, mload(0x10e0)) - mstore(0x77e0, mload(0x1100)) -mstore(0x7800, keccak256(0x7700, 256)) -mstore(30752, mod(mload(30720), f_q)) -mstore(0x7840, mulmod(mload(0x7820), mload(0x7820), f_q)) -mstore(0x7860, mulmod(1, mload(0x7820), f_q)) -mstore(0x7880, mload(0x7780)) - mstore(0x78a0, mload(0x77a0)) -mstore(0x78c0, mload(0x7860)) -success := and(eq(staticcall(gas(), 0x7, 0x7880, 0x60, 0x7880, 0x40), 1), success) -mstore(0x78e0, mload(0x7700)) - mstore(0x7900, mload(0x7720)) -mstore(0x7920, mload(0x7880)) - mstore(0x7940, mload(0x78a0)) -success := and(eq(staticcall(gas(), 0x6, 0x78e0, 0x80, 0x78e0, 0x40), 1), success) -mstore(0x7960, mload(0x77c0)) - mstore(0x7980, mload(0x77e0)) -mstore(0x79a0, mload(0x7860)) -success := and(eq(staticcall(gas(), 0x7, 0x7960, 0x60, 0x7960, 0x40), 1), success) -mstore(0x79c0, mload(0x7740)) - mstore(0x79e0, mload(0x7760)) -mstore(0x7a00, mload(0x7960)) - mstore(0x7a20, mload(0x7980)) -success := and(eq(staticcall(gas(), 0x6, 0x79c0, 0x80, 0x79c0, 0x40), 1), success) -mstore(0x7a40, mload(0x78e0)) - mstore(0x7a60, mload(0x7900)) -mstore(0x7a80, 0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2) - mstore(0x7aa0, 0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed) - mstore(0x7ac0, 0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b) - mstore(0x7ae0, 0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa) -mstore(0x7b00, mload(0x79c0)) - mstore(0x7b20, mload(0x79e0)) -mstore(0x7b40, 0x138d5863615c12d3bd7d3fd007776d281a337f9d7f6dce23532100bb4bb5839d) - mstore(0x7b60, 0x0a3bb881671ee4e9238366e87f6598f0de356372ed3dc870766ec8ac005211e4) - mstore(0x7b80, 0x19c9d7d9c6e7ad2d9a0d5847ebdd2687c668939a202553ded2760d3eb8dbf559) - mstore(0x7ba0, 0x198adb441818c42721c88c532ed13a5da1ebb78b85574d0b7326d8e6f4c1e25a) -success := and(eq(staticcall(gas(), 0x8, 0x7a40, 0x180, 0x7a40, 0x20), 1), success) -success := and(eq(mload(0x7a40), 1), success) - - if not(success) { revert(0, 0) } - return(0, 0) - - } - } - } \ No newline at end of file diff --git a/axiom-eth/data/tests/batch_query/shasums.txt b/axiom-eth/data/tests/batch_query/shasums.txt deleted file mode 100644 index a156857b..00000000 --- a/axiom-eth/data/tests/batch_query/shasums.txt +++ /dev/null @@ -1,21 +0,0 @@ -// These are generated via `sha256sum` on Ubuntu. On macOS use `shasum -a 256`. - -43ac70e81f3ac38f6875ef4ad7b6b2280d5dfef3e6228a73732659f4b8a9608e data/tests/batch_query/account_3.pk -8e58b7f7781913b92d957d35a49ab329a7b81572d4d2a8a6f93990c8a59c427b data/tests/batch_query/account_3_6_0.pk -1fb77be0c528d4faa60c155a2eba09dc80a360a983f9a18788eb54430970b787 data/tests/batch_query/account_3_6_1.pk -451e9181972851521d93fa8b4161fea5472f25a014ca55f38e99d2dc67ed3d73 data/tests/batch_query/account_3_6_2.pk -4e0aed9f382d1eb1c18a0d30240ceeea5b5af918050fad4c21d8dd8663835fb7 data/tests/batch_query/account_4.pk -cfc849730078fa3281b09dbed8d7f516e8813a58ddecbecf646ad61009f44b39 data/tests/batch_query/account_5.pk -0d97ba6aeb9aef83581dca85efd7ade0aa153ab95c2f5aa5d9a91eede4b54083 data/tests/batch_query/final_0.pk -5870a59ac4909857f9371a3c5750eef6c48a9a851ef14b5d7e262316b28e3645 data/tests/batch_query/final_1.pk -b450910797d24e92693e9f491990ef3b4d04ff4c40c175be448f72daa65b3538 data/tests/batch_query/final_2.pk -9a0fb5d78af7dffed150686389c23007f351e06f31d26b58a2251ee547245aa0 data/tests/batch_query/mainnet_block_6.pk -461d5b39193d54e90708927677a8dab151257124ff35361a8961825300d8ee24 data/tests/batch_query/row_6.pk -cc38f49d880dfba2eb13a87d6da6fd93127b82865cc594f9e01618c3fa9ba125 data/tests/batch_query/storage_3.pk -474c13b4c67660eadc4a856499395d918986824315a63d0d70f9cae5d6b8cbaa data/tests/batch_query/storage_3_6_0.pk -de69177ac1b65e87b6c9a7c0b3d591a669a537bd32caf671a0d45e978f779bee data/tests/batch_query/storage_3_6_1.pk -d83994d25f52b5c2c299f77874e860398b849dde9808141db4f66def01183e2f data/tests/batch_query/storage_3_6_2.pk -237fcc11e6dc51e0456e35d06e5a4f8729bec3edab452e877238407716a5a8f3 data/tests/batch_query/storage_4.pk -31164e15403255ae2091ef117fc7fde8a7a8dcf708ce3df65893c67ec2b86235 data/tests/batch_query/storage_5.pk - -30b83ac7101eec9e57d98c0663deb4908db910db254bd3f73e3bd789f6197ed3 data/tests/batch_query/final_2.yul \ No newline at end of file diff --git a/axiom-eth/data/tests/batch_query/test_scheduler_final_assembly_old.calldata b/axiom-eth/data/tests/batch_query/test_scheduler_final_assembly_old.calldata deleted file mode 100644 index 4ff565e1..00000000 --- a/axiom-eth/data/tests/batch_query/test_scheduler_final_assembly_old.calldata +++ /dev/null @@ -1 +0,0 @@ -00000000000000000000000000000000000000000028f738971d8237e9a5ebd90000000000000000000000000000000000000000003bb9124d5ab8c876e92a130000000000000000000000000000000000000000000002a211444230ae9b8eaa0000000000000000000000000000000000000000000842a2c77a52706940448f000000000000000000000000000000000000000000ec6c98a16e26ed9ca7e8e000000000000000000000000000000000000000000000253376ec30198129f97b00000000000000000000000000000000000000000086b62569543e0d20846e1700000000000000000000000000000000000000000026edf13f5fb57f4b095d6000000000000000000000000000000000000000000000144575f40f7c3ea176e10000000000000000000000000000000000000000007eb5368cdaee36be18d867000000000000000000000000000000000000000000e7089cc254e340d7b88a6b000000000000000000000000000000000000000000001cc9b0e088f83adf39da2a8a5879fc5648c7be24f15e69eb96c2be738222bfac9581d2c97968be6a18bb00000000000000000000000000000000322d1ad245a73637a4731299672fdf3b000000000000000000000000000000006d2608dc223f1669bfdcd306fd304ce924cc26563ed6861bf3e5bbbacda1c3a2bd92c17deab83c4787132a1e39a1ca4000000000000000000000000000000000082df7ffd0116d06a60b47987dd4f670000000000000000000000000000000008a2211b976df2d5374aff1614ab6a6441f8d632b3e4e554acee7d33597d61a7c1c91778364a23eebac9242c59919a30c0000000000000000000000000000000033637ca1c7b3c31311cef488b2f31a8300000000000000000000000000000000c3ab61cbb1e5615b62a460ec032f239800000000000000000000000000000000bfe45f2d685be849578a95bea4b8b05300000000000000000000000000000000c63329084db7b06fee20ca2ad3c6556a000000000000000000000000000000002d77d208db8c601b953c1a107b63322600000000000000000000000000000000835576967fe4dec12375b07a6c6a38cf0c415283aee1a93f63263cacb2d77c00e53ea1492ea7ee918fd9a6df20b946202e91ff1ce49542b64826524231eb868b619448b5fa24b0be7a4103e467b1b7e12b04759dea5e35d5634be432550c82399c2370ade6c11c381b9a5f3c738ab04800ad67df290dcfa6bb7c20a46b261c4bf1a5f01e37eb7d33ec6e9563ce0c0f8a079e513720022d7e2999cd6eecd2a63e35dd944680c1b63c2ca71ac7a295765c1ddf24701f7832a23a946e8fb325f3bbffe22c550a1adf5e4e1f26162a5e42a30238bda98001448851bf779413947ec38a28d1755c650278e06f2d4b9ff428c708f8a868c5316b578b45b9fed56d18b0f4f9f94768054e7082e72fb3582cac9a19a02f2c264d74e3759a86d203035cc7b7a127b99ed34cbb708da6074a0be4ef08720f52aba070ddd7b0fb5d3426e30ba84f907710dabb27d071d5c7b2e8b6c82fbe36bbf12dbc1e778118dec1b6b282a126c82caf0ba3dc10421913fdd6e728288e89525b269705ccf726b1e9f675c79ed1e68fc01b3db676dd7d22515e1fcd21d3c36bb6cef7b0a4d498fc4c86032dfaece58ea45c2225ea54e5e26891f4921eb2587b4f0de7378a63fb500eb101553910a13c851bb21fa586be793b01fc89243bd1b1a33c83235459d5586d664a54f73da749278df9e80f6ea1c27fbeb680085ff30433455508a91e38b6aa5167ec819b19892bd1245d85c30e699f1deb612aaef20b5776977d0b7a3dc3fb8f79d7703798b45b6bc7fdb140c656719ec1da02cd0d9c9b845fa8696b2cdbef36c99abad00469041cde8b9bc64a937d56432e244aaedf0a4e891284389daaeb36b467b2578e78809668eb6630bee7a8e381a912d465a124f285278c0599ae404d2314d4e52fb19ac73366ecde9abf924d1be72f84b25830e2f31db59a0f1d7f1425aa8e91c5836f6eb8d225d2688ce35c8ff820da41209a173f6ca151c8e89f6c39e0a28fc74e0595dfe2dc55c8b540e7b1e5288e7499151958fb1b570e873a39b483c5594b76df8d233f1ecb71f36138a01d267c000b87896f763fcab889b853f87381e96e89f8a1029f29dc714ababcd2510e388d73bebcb30e09226fbb3be43b06dcadaa3be4c2b88fef56a0b1097c28df01dde23978bd576a1c23e6daf07e0697a5f6113b5d60f5f28703fee91d6de77b2a2196e92166049387e14e4f5b460bccff4ad8e138dec5dccb25881c871d8df605c3b6919a6ad411f0739c585b666588fcb30da6e7b49e323a590ddf0a8d575701f5a2a63324f3411cecae118016a6111bc322ae9c5dfd0a90d294be181a9d572ab217bed6b0e53ba3e518ef0ad03b3d7531968a58ab170884c8a72a4e69d9e224f30a90a636c8d8af213412453566cce5d9adcd3cd81fced3d44027c42e5e6c276d1cd3c52fa6d766a398eeffafabffb491e2ffc4122ccf704ecb66ebe96d5b1fcc97c0bb0c9bd3a9a43ba4627b0daf5252f018243120636feba7b3daa17086185183afd00afe0ec892bfedfe4cc7cb80d02a83c661fea6d92a1f2d7e645589260b70f97e4a09e604b53525e5be671988937453174aa8b4b3e400cf946d59ee15396792a17c1eb787500871d4d795c44cdc2cd08afd5cd237ed90016a4c6ace2c4ec8f0ce490d7edf5d8f343b0387b4e5dd37aa5f295a30abc2f578283fc029187aac6cd1fc44ac0bd7cd99b225cd2c29714797ce69b078915990f30550fbda250519ee2f65ce987e1a2247b3a8de8d694130a5ead60ca5760aa996f29149ce302c96d02cf4a32ce8a9c0fb453bdc2737dcdff66944b69870cd9e80cdcdce4b2308bafa2f833ae2d9430de7e58d40c291ac0408863f918171588b96943a36d91883131803e42e0f409a07355249b677155dc8985523e047dd6f311212d97f8e0b6daac8df5178c96b8378beba737618d861eb3e1ee082cac68ed200764652aa212a9be70b458c1d623687207618484be47dd1f40b1bfa855c4beb4fc3dde84e22f4169be7c8bc043e3c5815e7b20828daafa463f4a3edaff12da15daf7a6ab217579b13453071bbcb189f438f5fa121d6ea21239007458ad45bfb7075f6611302573bb46e0ffcaecbaf4cfcd486397e6333b63e3d64fb8e9f75f333e8f21ce10c4cce74c2536a957814e9427d202b1b1c125cfbe557226e8d6ac7cef75ddea900cf8ae3529dcff3b43440ca3220d046c73c3f160c6342f3a8a232bdd33a24382da97bd251fc6df6b90bcce4d861f70a949f3e7ac6f53877f1e7c15e8b089d9e27aa2b1782d28ce07163832331fc17e88e6a31f47f017be67bd65c08e1ae52610cb8cd427e9b12eaa68b348ce89f0060da04a4fdd54afabdf6ea71c15d690bd719281dbb9cb41b294b0abf4a4a0c7f444e8502a665a36dd36e8a6689645527931d06e16790c2286876f1e3a8a319b7e599b3c18d13352517096abad2c384d1012bc3f85365ccab0756dbbc63c545298f00f653eef1f720d990bf4fe7583cab5b0978db3d3c1812f6ea8b179cfafeb9a96439f4372ee57d1990ab80dfc86b7ef82528e7f2e484dae262bd6440aefd9c295d67477b1c6d8720ab0c9889e01d4709264c1486d29c0a8e2bd724d6688f5b9f53db9beb1832351f921e861a0fee7181046e61e5120809e3070c5ad90506db856865b16f376b1ffcafafc0262a9915a50ccc0020bf81ed1866fbe15af2b0d1d76a16359779d2ad24655edb6a0aacbfbf18477aa7d6e1edef9c4f8b9b33783224666b18c4363384b1b2af9a34e0db4b94098b9d83a499a192514dde93b9ff99475feaf045f015435c46b86219da10fffc2e30250200687053ba7a524070c141606bcecabe7a7a298de838dd7d5d040a352129f5b7a4163271bf924999fc353485bfc24743288dc6de58e36c825c9b13bf12a75508e93f0d3eba94be16232992702ffba1dc52103a7fa63e7c65d01684b1019bd28713fe9064de0d71855cd7a94f37fdaaf6975631e45c3c22d140d9c4d5093d768995cf44182794ffd109e90ee11b99c646267e2783ee5e1a27e5a3307e0eb5f3aa21e2d6b2f14839dc4a801a80e3a6c77bbf6d005d7cd477291ebf57d92e22df4f1a1427b69a8dd7e9ce548c4c71e6eae0a217b2d229067b1362b5fc0b2d8e5c601b09de9ba778e442b6636c7158bf05263ddf4d721aa0c79c8786fdc3252657f02a0f92256665ad7f1a45cde363b13289e7211b9a478b6368a21aed94062a754cfba90e1eb001af89efc7a2b2ed8bd1425cc416fc9f4d788f9fe9d8d3072bae3ec487ea7c1ed325c5e28d745dcc5de6b69d829870fef3ae31e3777bac24aa68dbc0ccf1dc9f4eb39fad6eb4911a4a9ddaa6d176a6ce11a1fce6d5de6d303f86cb519018a19957b0a56fac7ad43c041c68d78fa5327b5737c03fbf263b1e0429e187be209ade484925f78d13a934e76f2ad7b291e4417fc315cd0b300c1f0a0d6ab76cf2dfabdab4003be80a1bf12b0aab061926ec117a6067a59c5a1a24d31f7605b228f8925a5c78ab3ac8d03772f235333d595b68207985b7e887440f68865983550abe1f3eaacb8b83451e37f826faca77051b9a584203f92f9f24149ad314ce92c6a5beca758a0b4e0e5750442134b5b3738fe9c5e5fb50d6517c12494403201f9d5effccf9dbdbe8016650d2d5f4d4e883f5f9133a0913b5259a07d1ffb2c936ac8e33f44cd9f94250c7d770d29f6fd1a71e553d177638ccab4d02e6795fdfd10ba19dcd817d99610e61bd23947c8d36c00c8cd690d86f325aef \ No newline at end of file diff --git a/axiom-eth/data/tests/batch_query/test_scheduler_final_assembly_recent.calldata b/axiom-eth/data/tests/batch_query/test_scheduler_final_assembly_recent.calldata deleted file mode 100644 index 99c69360..00000000 --- a/axiom-eth/data/tests/batch_query/test_scheduler_final_assembly_recent.calldata +++ /dev/null @@ -1 +0,0 @@ -000000000000000000000000000000000000000000665fc4858acce681fc7f8300000000000000000000000000000000000000000010151bb1b7a42aeb4bec9f000000000000000000000000000000000000000000000074f0eec9c7548ff3d1000000000000000000000000000000000000000000c76a3f19562759d594da3d000000000000000000000000000000000000000000d412295120ed06f45b0a6c0000000000000000000000000000000000000000000009821e2ff3124c21d8d3000000000000000000000000000000000000000000eab8d8615ef8cc3659f76a00000000000000000000000000000000000000000021a4ff9691b86cfe2ebe84000000000000000000000000000000000000000000000dc2caf1ffb76fd2b29300000000000000000000000000000000000000000081f66ab9c22fc629fcd6e700000000000000000000000000000000000000000007756639370de7c3cd22a6000000000000000000000000000000000000000000000b4ccbdb4293690af3ba091ecc3df9cfe52bdbf1bad5c6a7f8aa4416fab9b6a61152692d30d6c510c64700000000000000000000000000000000434bf7672b7657411e1824edbbea4cb90000000000000000000000000000000090a1a6ee56d9be0e659774cbe8f956dd0c79375f0e6e921f718640d65d36b4c0881ef22c9b245769236145725556c5b700000000000000000000000000000000ac95e2258648f8f86b3624bbc04b521f00000000000000000000000000000000df4b6ff03783b272e9861bff623530c72e1b4dac392cebfa1bbf4bb424a53a798ddaa129e15cc86f46e66630d4d7cb73000000000000000000000000000000007e3cd333fb85a005b3eabed6bfcb276000000000000000000000000000000000966650d0a5377a80e3d2ed449f4bb63f00000000000000000000000000000000bfe45f2d685be849578a95bea4b8b05300000000000000000000000000000000c63329084db7b06fee20ca2ad3c6556a000000000000000000000000000000008aa035eb40cf8dbc6305e610e3db64fa00000000000000000000000000000000f829076b70470a14fa0189f0dad2e5510b64cf2763d70fae32cfc516284eea23884e8477acf90e6424201c02a837b51108ca273096eb539a4e7f6b2b4f3eae5038a290ff4f6d878c96c77af824748c522b318dc72213f2e594c395c011da3a272d8633635e703652db9daa2dfa504a21156b8984efee03048221a46225cdb4a2d730792bda28febda3d29dc237c26d04275a57faf05f1c913c65229581586a129c84373192dda10cb5c2d2b6f21b4cf921e6783ec55bd850799c09ed3a3b0f4f97c0161afd9519d8d1397b722c5b6b5109630e258e674a2fde947a5b57699fb4e09e8cc80326e36c63cdbc23506d687015c6ca9cf818a54b84633d64bd516fb7b1f461005bd88b94130e5d82e97c4c84184911b1f58f166760923a130f4aad311571284881a478e86cf816c0656f2ce603a1b95c5237dfa7e73ff8d6273ac11b46a924f02d264658aa0d354b6fcb542123c35841c46c05f564e1a40da038c95ebd92dca4f7a67eafffa0bdc1f60254e200cd3a1b4613799397853eafc90a42ec71b01eaf539ec47ae0951e519c05c4ea148def78e84b08ba373ca28bd7c926bc656cac457a7b8858f007cbac1e7e3e5913cf20c2fcc0d09c0a91ab20cc830d7d8a87f7e574cb2a4e0524c0555f5ba27407863ecb297514a2a5e0333b4a35656400106db2d032fd23b0f667e5f86574421273cc373f3d785e4d2fc9f7d8209bceab39cffdcff5a38054459ffaea2048ce2c5a646da9a97ccdddc509b685608fdf8a78b86ea531665dbae48e6e0316132309b5c9b0cd49682db1902621ea424c176c68ad216e27c3c7de7ddfe75fa0eb110f3559e9e8935b2b9d586e3814110774b32a0a0c9077ede25630188e64992e731e2b7828a1eb07e7acf9c30e6837ab697a52a5cbabc4d90768e98552720d8fc81e27bb37dbf74151b168fb1a2ccaab9168c73beaccc7df522c5c5ac4613dbd7b1f97b365d0c7ae2411b9e08b3d530c73cada4beadd49235f60ad507053dc06fb0c5da555bf9ebecfa0be04743025be35ce7b163a3b1746ae7bfdb71bd7d189f1207adcf5a2bcecee9ba69e3c620a5569cfb0579eab715c4074dd962016a70fff000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000022337a08ca8230fb6d208cd0787fd73dc33849312d3bce8ddbba16375d008ff212608331581d4f9e68298b65e036f7f5c887077d120923d1b6bf4d46995004b840a6cf34cdad2f5caa7da4c8015f241c84f8f77aaf353ebaa1ca38dc9b571bfae0629f5fe705a1c6102d5785b5657b9fb4193e3ab33dad515dacea6b7c79086442efe008173adbf6b568b874ab343f479409d8fca758390f19d5d2b85c582bed22a8d27fff64f56d62ec309dfb4c5dbd715fef73e4b9023b92d633ff9173983bb21430071e16e58bfe73b8e2597fab81f8d097a2566e7a6d2b9e856968f4ed6440ee21e5680948ad1a646d76db1717591ac7cd9d92ea210a1b7476c6ec68051e81d8d361277eefed37cb6c48cb746f5e35e4158f57515acb70081c61bb9732f8c0c109f614fa897ec69917e597ab1271bd8c3bb8fe4807d0a245bc908c3b028f42152c664abfb52b3e83f14b44c24f3b10038935052dd17542a6d0d604ea2cf131b9864b650aebda7cc84ccf78c19525fcb8ab4a291887a820fb9587ab88174c600505dc617b1182c454ffa5f4017f792269a10855dfc4a5e81367a1ff8622f992ad18d986fda5ef03d2e30ec1c971e517342ce9e753429d216ec6c6d1101c05e186754873a681ec360f72c3d04e5ed28620ef5e12a36464fb99b3c4c005e2620206e32c87d732fbd650892bad8eac8b6ac589f49ac4051eeb95aa7500116964d2fd879b6fd41f71fbb92afe2cd27a795fe09371f27215f4844b7bf11844c29fb07297afa1adc7c9b8b3a0452c6a6f591f89aaf5a4260ef178a81763e2d86537c0b6588ca78ed27ef059d4e2e2bea78b2ec48a2cae48ff5b53e142543dbc4f4cf097352ba84a49099d89aaad9529ca22f226d69152a28117b7860c85cd0377f8f2c6029586682aeffc11ac03619bbd0434a1c8b80b47a15a362d6a36f42683f45263bd191b1a8ed3e36acf51c7d28934152b9ee0232a839690a92a452d6369cd41bccf1846fb6d6dceea0db2cc6b04dff3362f7cf7bd1d06ab3a4f51ff248fc1c09ca056f583eda6ca16b5bb651d82c511b0b59d5542c35065a6f2ba536a9637a1f03bf0ecc2152d41db6eb8948d0314711fcb8d0b3487e12300ff5793e36833214484162dea4f8606d4064908619321287b5eb85e80cf4b538d7f2bbd0f2ae7c1bfc7d4a48559f9937b3812ac571ae29b7bf41e3c50a175164c25f0fbeca5112223f6be761cb2b84f4439fa49e2e5f83357835ac85100cd53def96e61997979827491836a2016be722ec8d37de1e709e4a0196089ff994d20e2ee49c88e9c7ea00000000000000000000000000000000000000000000000000000000000000011be98151d843de69ac38069b161df5b1d193f7e9e04b21f165b07ac179dd7570050aaaa2d0259d46968011a7b18d8485634e56b8a2fd15fd743633233f0fe01e12c0c8c91a806009d9be3d48dd643de19780033d2445752348931bfc3a9fe2de1d59c690c71698a69e3d2fef6857621b1474276d4157affdfa9d4940872335fb0c1c6f243d481de7900efd90fecfed2ab4374d718bfac08b8c9f9fc40a2b1bf8147e435205243d0aa2f35d100c535b9c23032b00c8a0dab42db2a0605ea51467161cb0feef927765bf9b3b76299139742a731fde85a1b2359970ee72e8e2034c056cfd10da0723e47db5ad864bb7af54216088c5ec90903b69a4db19b984bdef0f9cceddab0e46f738e02d56f52f61ffdf0931cb48ea974fbd732af8538d89281e80e52ea29504da17d46dafa773be8a171c80fadf186ee17b38d076368ab3dd24a929329432f56d76a45256f3651c6534c9a7be9dc465fe2e180fa93c5ced96208ffa75b9192c40b39897bc88c47c163866bc0bf5d7188cc1fc27f1711c98630324c8ca70ca7fd52e2704fb89ace33ef6e0893e58de59aee0275dd15159e2ba12a31efe1f00356f8893fe5c1f1229067e35541df01b2096bf1c05ef4635251e1641cc5b8b780baa97861160085b1ccadf5ba29075a1c0caad6c008405fa0be1223019b17384d9c833074d79a9b95e19bbd6ca160219b687ce9d962da07bb3120b12f390985fa989804224eb9bed86a85fd9b4fe37fa8ebb43776971c9b2b27f22f8290eb0abbb347d180d9a27ec1b8c56da9914cd37f84c45190d88ef095d1804bd209c8204667dfbcecbd419202af32f5d1a4460981ccdc577c27d23620e6a104f29cdcb14216071e6d795b92ac9a8b43f7489afb4c61d0c6ad56bc9a8c9f019b0b71d2778522581f31abae34347c4e7e89a4096e2ea8fec9e86cf610d513015cd3af88ad92817ebf9b0ca6ced0e10cdde9757e4cb8583c9f8d3d0b1c42c210a57d794a84dfb9a25e7682bb5e8c1c6eea628ba96851c7a497965d80a0eb3e41c44b86f7c6d66513642e3ab8cd86a971db69d13622e6bb530511d6b2013a827005ae76af5c301b16fdc652e00c4174378a8890bf6292f627661b602c89e7b200c99d7aa07a250fd9f43b53c591cd665b73a47ffb3eb7455fdd94865fef6c84c2fdd1cae3d31f81404a57458519800c51346993715bd47c9b217e9d106a27e02 \ No newline at end of file diff --git a/axiom-eth/data/transaction/task.t.json b/axiom-eth/data/transaction/task.t.json new file mode 100644 index 00000000..3f680c91 --- /dev/null +++ b/axiom-eth/data/transaction/task.t.json @@ -0,0 +1,91 @@ +{ + "block_numbers": [ + 12985438, + 12985438 + ], + "queries": [ + [ + 0, + 4 + ], + [ + 0, + 4 + ] + ], + "mmr": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xe81cc62bb288e100856ea7d40af72b844e9dcb9ff8ebed659a475e2635cd4e18", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xb169c87af2d231bc71f910481d6d8315a6fc4edfab212ee003d206b9643339c0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x43062e083f5f40510f30723d9cebb51c4ae67c35d86c5b791a043bae317350e3", + "0x6cddc980f4c3b403d99080c32b3f0a6205e39560b9021d5f233c04d96c23381e", + "0x6a42052cabd8d66a584b8823c6aadf64dd2755321210c66a2b7acd1da5bdeacf", + "0xebf08ca711cbab09109bb51277c545ee43073d8fa8b46c0cbbedd46ce85e73be", + "0x477c055e69de14e3bbfe2af0389e6c3ac28ffb5b0cc8fa26b543ac47857fd646", + "0xf47e6584af17667881494720b50dd063d0900b288438df7a37e2e91440aedd23" + ], + "mmr_list_len": 16525312, + "mmr_proofs": [ + [ + "0xebd5fc0be32c2298e2ee18dac2db5b1508a64741ba7186dd59229ec0620d9d64", + "0x9139f12e0f47241172143fc5d0b0245b5ffbcdf9130da9efb14c155f6036697e", + "0x97f5d40bc9a10e06b5eef7609a3e364f30ab10675d22fbc3304179a381b39b18", + "0xc8c07e6c877f0cd60903d376c1aa911f47c96d3967d989101ed8b873cf6e38de", + "0x96cf53edbe3be783378433c518939a7e0b4e657edb6558b1f6e14edc0a125a18", + "0xfa3448a664e9406ffdc3b53e24f06fcf6b576621f854e421385bd1603ea257ee", + "0x9dffc8cb737d72da73df5e333bb7716cfec51e4b761281c6c7ff4db55689911c", + "0xef3fb7b7274810ec5bc63e7c248ea7dfe26d95abcd8bcb8d97b1f5fb617b8dc8", + "0x6a4d92e38592f280afc95efe5dd178a37c155bfad49759db7a066d597bc804d3", + "0x7db79de6d79e2ff264f4d171243f5038b575b380d31b052dda979e28fae7fc08", + "0x3106ece6d5a3c317f17c9313e7d0a3cd73649662301f50fdcedc67254b3fe153", + "0x902c8cf11e8d5cf14137e632061a52574515a2254fbd3b70cfc85a45f9dbcb4a", + "0xc48c7fe69133ac6f0c2200e600a3c15fe1832577156bc8851a7538403eafadfa", + "0x4434e3730dbe222cb8b98703748da1f07f05564c64ea66fe4765484ea982f5d6", + "0x69d2bc461de5dba21f741bf757d60ec8a92c3f29e417cb99fa76459bc3e86278", + "0xe18396e487f6c0bcd73a2d4c4c8c3583be7edefe59f20b2ce67c7f363b8a856a", + "0xa10b0dd9e041c793d0dbdf615bee9e18c3f6e3b191469bbb8cc9912d5d228050", + "0xa51d50eb9feaaf85b7ddacb99f71886135f1c4f59de3e788a5e29a485d5fdce5", + "0xa46b70512bfe0b85498e28ae8187cfadff9e58680b84ddcde450cd880ea489b1", + "0x33552dfc75e340bca3c698e4fb486ae540d07cf2a845465575cff24d866a161a", + "0x0fec590ac8394abe8477b828bf31b470d95772b3f331ff5be34ba0a899975a17" + ], + [ + "0xebd5fc0be32c2298e2ee18dac2db5b1508a64741ba7186dd59229ec0620d9d64", + "0x9139f12e0f47241172143fc5d0b0245b5ffbcdf9130da9efb14c155f6036697e", + "0x97f5d40bc9a10e06b5eef7609a3e364f30ab10675d22fbc3304179a381b39b18", + "0xc8c07e6c877f0cd60903d376c1aa911f47c96d3967d989101ed8b873cf6e38de", + "0x96cf53edbe3be783378433c518939a7e0b4e657edb6558b1f6e14edc0a125a18", + "0xfa3448a664e9406ffdc3b53e24f06fcf6b576621f854e421385bd1603ea257ee", + "0x9dffc8cb737d72da73df5e333bb7716cfec51e4b761281c6c7ff4db55689911c", + "0xef3fb7b7274810ec5bc63e7c248ea7dfe26d95abcd8bcb8d97b1f5fb617b8dc8", + "0x6a4d92e38592f280afc95efe5dd178a37c155bfad49759db7a066d597bc804d3", + "0x7db79de6d79e2ff264f4d171243f5038b575b380d31b052dda979e28fae7fc08", + "0x3106ece6d5a3c317f17c9313e7d0a3cd73649662301f50fdcedc67254b3fe153", + "0x902c8cf11e8d5cf14137e632061a52574515a2254fbd3b70cfc85a45f9dbcb4a", + "0xc48c7fe69133ac6f0c2200e600a3c15fe1832577156bc8851a7538403eafadfa", + "0x4434e3730dbe222cb8b98703748da1f07f05564c64ea66fe4765484ea982f5d6", + "0x69d2bc461de5dba21f741bf757d60ec8a92c3f29e417cb99fa76459bc3e86278", + "0xe18396e487f6c0bcd73a2d4c4c8c3583be7edefe59f20b2ce67c7f363b8a856a", + "0xa10b0dd9e041c793d0dbdf615bee9e18c3f6e3b191469bbb8cc9912d5d228050", + "0xa51d50eb9feaaf85b7ddacb99f71886135f1c4f59de3e788a5e29a485d5fdce5", + "0xa46b70512bfe0b85498e28ae8187cfadff9e58680b84ddcde450cd880ea489b1", + "0x33552dfc75e340bca3c698e4fb486ae540d07cf2a845465575cff24d866a161a", + "0x0fec590ac8394abe8477b828bf31b470d95772b3f331ff5be34ba0a899975a17" + ] + ] +} \ No newline at end of file diff --git a/axiom-eth/data/transaction/test.calldata b/axiom-eth/data/transaction/test.calldata new file mode 100644 index 00000000..52076d60 --- /dev/null +++ b/axiom-eth/data/transaction/test.calldata @@ -0,0 +1 @@ +0000000000000000000000000000000000000000009d5234faa89b20e42d1f79000000000000000000000000000000000000000000bcb37f8f18d637aebf54520000000000000000000000000000000000000000000005602d08e5a76809da5f00000000000000000000000000000000000000000045ac04dcd79d05c1af4b4e0000000000000000000000000000000000000000000711e31df1da81f0f2197f0000000000000000000000000000000000000000000025d3b755157d6bdaaf6a000000000000000000000000000000000000000000ea8b002f12a954f8c080160000000000000000000000000000000000000000008d071ea1319fc5cd45085e0000000000000000000000000000000000000000000027d125c37b20a0aec4d3000000000000000000000000000000000000000000b18bed65f22252af0ec2e0000000000000000000000000000000000000000000b36386fa863f15e186f84e000000000000000000000000000000000000000000000beb0f0846e511200ad300000000000000000000000000000000eab72f21616dd49262dbf7b9bd110b0e0000000000000000000000000000000072c91fbc6ca206fce206d3dc6158c5f100000000000000000000000000000000000000000000000000000000004c4b720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000012b9d6c09782c7d8da32ea1bddcca65257afec13cf392483d251db9fac5b25b8a21127c3537e4154c09fcd5ca4479ce7bdb2a58a619ba8c2bb69088416a2784ba041eb4569381a820cac1cdad800a7abe62660730a8b951dd6c7922f97562c87e01485103ba7ff9529f6e8b1acd7aa56522e97e0b1a6e0350b0cdb007ea4b7a241db1ce03f4f2a0adba66d8d779fe581867ec097787e70a260ac6816d8850d9cb139896f1a482870f92be1dbd96318830a00ddb9389d1187ddebcff4d061a4ce2011a17ec168f805ff9b5f4d0ce36dad7f246f06d7cfdaf7372b7407dbef3e9c1206d0deae76a22e140e017d98dbf78b4b9c6b7e404812bee22feb262a9fc77c72be50914282e17fd0c38c5c3bd9335f759eb1628bcadbdf5c4cfe8792bb028b108c524291c636a25d67d752f04031e34e915e1d0f9e604ae9a9bf8f2fe6799441ee75c2e84788aed824d6d34ed99e7da92778ebdbcfa8095fa263b0ca51c56f2173196dff6ecd677f8477e2d21a61f0e7f3eec4a7ce6bc32a1fa035980c6433f2a0953670d486d653b511516101823fdf950691e87db43b3beb6b3b49454b2b813a8e278017fe76e4c3b8e87fa002c764e392687d0e3d289b3a2b69b4c032c2f09a504e3142372525498ef94788f9efaad66bfecf54d4c59ae29a034b870f04f0e04f1c6cc86bb95712ffe6442fdb5f80f3c9687103055ac94edbc8f203a59d90e8d34c5160e6e0ac180ef4acf1aa880c66f685c0964354ccdd8b8172fa7ab25218208605fe206588d31875b12ce1e5ed224699dcafa342c4c61297ec5b9af7829ac4b95fb77dc2d49305a94c1da38b42915f6f2b1163240f5ad450dddcca894265c219fa29f98c847e36aecaf143d523e05525e9c1ed66b60c57ac1b8d837b710624706448d490b9cd18f48964c898f926517266dad4abce9ffd34c3852d9ce07ab0df7fcd773dfc0662e0250d52bb6ab57d4d21d6aee326a995009567e57030fd925660e3177520588418514adb685ae17a5b1bfe483d115a5c2f4e46ef29e0ea13d94a7c70f7ad83dd0b4dc85bb12dc9fc456b2bd36d149ab99dac6118a9f1b55f22b5770b2aac92ac4833471567fe1960409a716d24b7b1aafe93dcf1a1512a26e017e823b88b8f3e2db288d4c41d163cd11e78e66b5ecdb86ca27b14c110383cfb341295c098595e37431a470a586fa95b7bc719813d09811f7d6bbea7c05ea7450873f801c08e76ca9bbf9bd610791d7571c63beb7ded2e81236765bc6289d4222054fb9dc0ada90c249fe0b9a04cea5fa2bca0a3c43552be8683ea7ef22de3e11734e981c9ebf640047df3badde897877e1d169be5a8fd43fe8e78cc62ee071ad4ccd1501f823f9144bbdac50d44e00a28c054ef1447ca8882edbc73d1df45295777271362a9ae2a387a68407e48babebdce01d2c203bb93c41d7bea51928a1df7102b715c34684cc7eee4a76a434e0f45305aa540baaa02dce9097700619a344183251a120e6e5100362458b2984c95eb9ad55735512f905cc81f5c418f045731ac454bca784d5fc0958b81443ea884d05d0ac2d2e04281ef17c91be25f958fa89cd51570f007d22a447f49382dd089dca836ebb0ab4f06531ad9214229b36bc7ae779f89b260601ae7081d024bd40822da50332cf64e2cc93744990189930029bd3209ef193ca2a984a72e1df5298154f76db994a3d0696efbb3873298902141f64b37014eeb8fc1ca4e3f5ae21e0ae561b2513bd8db7fa8c02cafc1bc45630910562077a7cef0e9a7d38850854ab95b99594b38899a702492e27090cabfc1ea303e364adac6c94f80d05b6b4246ab09de0f2edb6a0558888a434b9102f7de6ae26acba8655245d733c63623691a5e5d989638199758a7c91fdd642118a450d00077ea6f5e17265c5881f052175012fa64388c4f709acfe9806e9d9107477e5c1512dd0a329829046c3739f6a41a519c0477270e1bfc64cba0425f210ec0482f8820cfaa1d4cb4463238e5609b5bfab8bf42b2e114b25a54712726f11bed90c00447a6ff4cafa0e344fea1ddcbb4faa9b0b1c1b5bd22bb68a31eea12f6af54e77e0d661cf149751ca0cc39711394231f95edc857521bec1581d1a951e14ee20a7a07f19ec254561bcb5abaf72e623c980d727f1dc7381215889462909c9fab77a708a9133b256e5da33a685d02265d6c9c4d4e7565bdd519f56a679073465de3f229701b001f673621e95e4d2e89f55a6eb921e7cf94d32b50756231e1f1198b1d428798eb9417536e15ac7a4e84f10263e0c566de22544fe63ff9b222ef7a3641d5df8178e3ad46b37ac5f2f83bd0abe247558d191bd72b8cf347510e7a736f0074d15695a6f49154ae1ff44423c61cac67a6c3dece44b284ab3e32607ac1865605678a9e5c51691e92dded1165afad14b17367505ce12aba25401059436a5c267141ceb9f1165c14cccdd7feeb69307dee966fbd968214df6498b17cc6596831523c377734ae848962a98719afccaa8a5329d13a32e65a977216905126f7195a1c0434d6b9bb745abbfbc71b1ac81fa98fdab67fc95e9c4c451682afed7e451c97749c7f4d1a830e671261069d8dde464d8ca6ac34841c8cf2690167721825f233501656e732eaafe16d30d0fcfc302d33d760f0f534ba705aa4f0d7b7667f2f4645777cfadbecfa8ce01675704449fc05bd0e390d96563311f11272d3c532192d77ae8ab56ce1e4016a0695e77b9238a7f7a5a9fae12cac66630150f122504eef9f8c6e8d803b3b9947e2954cebe1697b81b6675141ec766a3d60d98fcbfd674965bbe4d8180392c5ea4c815a36dcba7ede7d8755f4a63d5b2d6107ce20b660ca40e6c2ce5bb8448f136858a33ccc571ed97922c31eef9fa7c170573ebb2e499039ff88bea54dbf0a54da8d523ea9d50db83da31b76983e1ac211a39c182a920910a9e056e637341441a88d1657096ea0a6ca986c831d6b83c8912b7f091433cb4f3d69e37aa7cc063d2cf166a475c7d2a4b3daa693581eeee6c0237199009ee172d65cff63fef0e9ccc8fa1dd62e7db9738b231375ea30bc67b28580b933a49583443fe1d28544fcada5f86819d1aeb280886aae5a8bd5e42922dba421435b8ca0d7c092626771d4bf8df4631a34daf6102a19a02920298a3d8192d10f7f42ad2626322b9bd760fdc308cb1c508a0a484bd77c1afe1935ddc742685385ed232e4bba13688ad6e2628ed4b11e4114a66691b47f5c69c7b0088cf2c9e55729cfc3427e98395c13484e97e0af46172b701492778fd8fb6d651751d1c3ab10f348756e84786a45e9ea2f9b2c936249e1c52a884bbb93e0b27d6c0b4222211b2d783054c3e4f13882d4787b83de3ec0889e463b5776beebeb008b025299c525c575bc363bfb90e7894dc803a66697328d1494c8abbfa9ac9598ddea103dad60fefaee6bba77735d9a0f0b3b42284559345871089eef85dcad7d0fad10aafdbdca1f830760f50187e3f0a17b68587afd822045ad57b0e65f95665f99e2c92a83163469451c09f6f0db55a3930efb654a3f4785bb06c7aafd02025c08d17d549fca53609b6dd5f285bc69a927b255f77449d0d3ced871972e9be2732162e8d42e52f42b6e1ae9f162ac43a644de569ed8572b6596653d032c35a139dc0078e09494556f58da64905f614e694f2c9418c61c7350c8bfa044dd6b1f12ced09bddd899cb676b7aaa7458e8db349d136cb6a8b281e9f184116f5ac3a1940ec03bcafb9a4dd060da4115927c09f14cb6719ef75eba0eab5b35e71dfe55bfcc6105fcb5978014311aac78602a7ebd8af131362366c4554956a114a3e54632b4e12a1c1bba3ec49d845aa29b203c1c7d4fb11c2d2b3ddfcb3e6fa89016e14f34820c743b6bb413f987a4bea2dcbfeaf3b39015caa8bafda5323165b28824b8a000ae40fa2090f7c843719eafbde3e30059035f2a013f17fb05d91daea58945eba0e2b73fc9ac1a840e700f3ce06d2100d9cbd1ea3069c62335c890210f70639fb255b12f5e10794900fac273924e224f314def0b43b9eb0201e4072d3ce8bb2fa2a159d169896012ca661b7ec79b21740afe469d69b093570748ade71c8a846ab \ No newline at end of file diff --git a/axiom-eth/data/transaction/test.yul b/axiom-eth/data/transaction/test.yul new file mode 100644 index 00000000..71c402aa --- /dev/null +++ b/axiom-eth/data/transaction/test.yul @@ -0,0 +1,1794 @@ + + object "plonk_verifier" { + code { + function allocate(size) -> ptr { + ptr := mload(0x40) + if eq(ptr, 0) { ptr := 0x60 } + mstore(0x40, add(ptr, size)) + } + let size := datasize("Runtime") + let offset := allocate(size) + datacopy(offset, dataoffset("Runtime"), size) + return(offset, size) + } + object "Runtime" { + code { + let success:bool := true + let f_p := 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47 + let f_q := 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001 + function validate_ec_point(x, y) -> valid:bool { + { + let x_lt_p:bool := lt(x, 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47) + let y_lt_p:bool := lt(y, 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47) + valid := and(x_lt_p, y_lt_p) + } + { + let y_square := mulmod(y, y, 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47) + let x_square := mulmod(x, x, 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47) + let x_cube := mulmod(x_square, x, 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47) + let x_cube_plus_3 := addmod(x_cube, 3, 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47) + let is_affine:bool := eq(x_cube_plus_3, y_square) + valid := and(valid, is_affine) + } + } + mstore(0x20, mod(calldataload(0x0), f_q)) +mstore(0x40, mod(calldataload(0x20), f_q)) +mstore(0x60, mod(calldataload(0x40), f_q)) +mstore(0x80, mod(calldataload(0x60), f_q)) +mstore(0xa0, mod(calldataload(0x80), f_q)) +mstore(0xc0, mod(calldataload(0xa0), f_q)) +mstore(0xe0, mod(calldataload(0xc0), f_q)) +mstore(0x100, mod(calldataload(0xe0), f_q)) +mstore(0x120, mod(calldataload(0x100), f_q)) +mstore(0x140, mod(calldataload(0x120), f_q)) +mstore(0x160, mod(calldataload(0x140), f_q)) +mstore(0x180, mod(calldataload(0x160), f_q)) +mstore(0x1a0, mod(calldataload(0x180), f_q)) +mstore(0x1c0, mod(calldataload(0x1a0), f_q)) +mstore(0x1e0, mod(calldataload(0x1c0), f_q)) +mstore(0x200, mod(calldataload(0x1e0), f_q)) +mstore(0x220, mod(calldataload(0x200), f_q)) +mstore(0x240, mod(calldataload(0x220), f_q)) +mstore(0x0, 636471555827657750166888787375234867401705836697401370361760792659418825816) + + { + let x := calldataload(0x240) + mstore(0x260, x) + let y := calldataload(0x260) + mstore(0x280, y) + success := and(validate_ec_point(x, y), success) + } + + { + let x := calldataload(0x280) + mstore(0x2a0, x) + let y := calldataload(0x2a0) + mstore(0x2c0, y) + success := and(validate_ec_point(x, y), success) + } + + { + let x := calldataload(0x2c0) + mstore(0x2e0, x) + let y := calldataload(0x2e0) + mstore(0x300, y) + success := and(validate_ec_point(x, y), success) + } + + { + let x := calldataload(0x300) + mstore(0x320, x) + let y := calldataload(0x320) + mstore(0x340, y) + success := and(validate_ec_point(x, y), success) + } + + { + let x := calldataload(0x340) + mstore(0x360, x) + let y := calldataload(0x360) + mstore(0x380, y) + success := and(validate_ec_point(x, y), success) + } + + { + let x := calldataload(0x380) + mstore(0x3a0, x) + let y := calldataload(0x3a0) + mstore(0x3c0, y) + success := and(validate_ec_point(x, y), success) + } +mstore(0x3e0, keccak256(0x0, 992)) +{ + let hash := mload(0x3e0) + mstore(0x400, mod(hash, f_q)) + mstore(0x420, hash) + } + + { + let x := calldataload(0x3c0) + mstore(0x440, x) + let y := calldataload(0x3e0) + mstore(0x460, y) + success := and(validate_ec_point(x, y), success) + } + + { + let x := calldataload(0x400) + mstore(0x480, x) + let y := calldataload(0x420) + mstore(0x4a0, y) + success := and(validate_ec_point(x, y), success) + } +mstore(0x4c0, keccak256(0x420, 160)) +{ + let hash := mload(0x4c0) + mstore(0x4e0, mod(hash, f_q)) + mstore(0x500, hash) + } +mstore8(1312, 1) +mstore(0x520, keccak256(0x500, 33)) +{ + let hash := mload(0x520) + mstore(0x540, mod(hash, f_q)) + mstore(0x560, hash) + } + + { + let x := calldataload(0x440) + mstore(0x580, x) + let y := calldataload(0x460) + mstore(0x5a0, y) + success := and(validate_ec_point(x, y), success) + } + + { + let x := calldataload(0x480) + mstore(0x5c0, x) + let y := calldataload(0x4a0) + mstore(0x5e0, y) + success := and(validate_ec_point(x, y), success) + } + + { + let x := calldataload(0x4c0) + mstore(0x600, x) + let y := calldataload(0x4e0) + mstore(0x620, y) + success := and(validate_ec_point(x, y), success) + } + + { + let x := calldataload(0x500) + mstore(0x640, x) + let y := calldataload(0x520) + mstore(0x660, y) + success := and(validate_ec_point(x, y), success) + } + + { + let x := calldataload(0x540) + mstore(0x680, x) + let y := calldataload(0x560) + mstore(0x6a0, y) + success := and(validate_ec_point(x, y), success) + } + + { + let x := calldataload(0x580) + mstore(0x6c0, x) + let y := calldataload(0x5a0) + mstore(0x6e0, y) + success := and(validate_ec_point(x, y), success) + } +mstore(0x700, keccak256(0x560, 416)) +{ + let hash := mload(0x700) + mstore(0x720, mod(hash, f_q)) + mstore(0x740, hash) + } + + { + let x := calldataload(0x5c0) + mstore(0x760, x) + let y := calldataload(0x5e0) + mstore(0x780, y) + success := and(validate_ec_point(x, y), success) + } + + { + let x := calldataload(0x600) + mstore(0x7a0, x) + let y := calldataload(0x620) + mstore(0x7c0, y) + success := and(validate_ec_point(x, y), success) + } + + { + let x := calldataload(0x640) + mstore(0x7e0, x) + let y := calldataload(0x660) + mstore(0x800, y) + success := and(validate_ec_point(x, y), success) + } +mstore(0x820, keccak256(0x740, 224)) +{ + let hash := mload(0x820) + mstore(0x840, mod(hash, f_q)) + mstore(0x860, hash) + } +mstore(0x880, mod(calldataload(0x680), f_q)) +mstore(0x8a0, mod(calldataload(0x6a0), f_q)) +mstore(0x8c0, mod(calldataload(0x6c0), f_q)) +mstore(0x8e0, mod(calldataload(0x6e0), f_q)) +mstore(0x900, mod(calldataload(0x700), f_q)) +mstore(0x920, mod(calldataload(0x720), f_q)) +mstore(0x940, mod(calldataload(0x740), f_q)) +mstore(0x960, mod(calldataload(0x760), f_q)) +mstore(0x980, mod(calldataload(0x780), f_q)) +mstore(0x9a0, mod(calldataload(0x7a0), f_q)) +mstore(0x9c0, mod(calldataload(0x7c0), f_q)) +mstore(0x9e0, mod(calldataload(0x7e0), f_q)) +mstore(0xa00, mod(calldataload(0x800), f_q)) +mstore(0xa20, mod(calldataload(0x820), f_q)) +mstore(0xa40, mod(calldataload(0x840), f_q)) +mstore(0xa60, mod(calldataload(0x860), f_q)) +mstore(0xa80, mod(calldataload(0x880), f_q)) +mstore(0xaa0, mod(calldataload(0x8a0), f_q)) +mstore(0xac0, mod(calldataload(0x8c0), f_q)) +mstore(0xae0, mod(calldataload(0x8e0), f_q)) +mstore(0xb00, mod(calldataload(0x900), f_q)) +mstore(0xb20, mod(calldataload(0x920), f_q)) +mstore(0xb40, mod(calldataload(0x940), f_q)) +mstore(0xb60, mod(calldataload(0x960), f_q)) +mstore(0xb80, mod(calldataload(0x980), f_q)) +mstore(0xba0, mod(calldataload(0x9a0), f_q)) +mstore(0xbc0, mod(calldataload(0x9c0), f_q)) +mstore(0xbe0, mod(calldataload(0x9e0), f_q)) +mstore(0xc00, mod(calldataload(0xa00), f_q)) +mstore(0xc20, mod(calldataload(0xa20), f_q)) +mstore(0xc40, mod(calldataload(0xa40), f_q)) +mstore(0xc60, mod(calldataload(0xa60), f_q)) +mstore(0xc80, mod(calldataload(0xa80), f_q)) +mstore(0xca0, mod(calldataload(0xaa0), f_q)) +mstore(0xcc0, mod(calldataload(0xac0), f_q)) +mstore(0xce0, mod(calldataload(0xae0), f_q)) +mstore(0xd00, mod(calldataload(0xb00), f_q)) +mstore(0xd20, mod(calldataload(0xb20), f_q)) +mstore(0xd40, mod(calldataload(0xb40), f_q)) +mstore(0xd60, mod(calldataload(0xb60), f_q)) +mstore(0xd80, mod(calldataload(0xb80), f_q)) +mstore(0xda0, mod(calldataload(0xba0), f_q)) +mstore(0xdc0, mod(calldataload(0xbc0), f_q)) +mstore(0xde0, mod(calldataload(0xbe0), f_q)) +mstore(0xe00, mod(calldataload(0xc00), f_q)) +mstore(0xe20, mod(calldataload(0xc20), f_q)) +mstore(0xe40, mod(calldataload(0xc40), f_q)) +mstore(0xe60, mod(calldataload(0xc60), f_q)) +mstore(0xe80, mod(calldataload(0xc80), f_q)) +mstore(0xea0, mod(calldataload(0xca0), f_q)) +mstore(0xec0, mod(calldataload(0xcc0), f_q)) +mstore(0xee0, mod(calldataload(0xce0), f_q)) +mstore(0xf00, mod(calldataload(0xd00), f_q)) +mstore(0xf20, keccak256(0x860, 1728)) +{ + let hash := mload(0xf20) + mstore(0xf40, mod(hash, f_q)) + mstore(0xf60, hash) + } +mstore8(3968, 1) +mstore(0xf80, keccak256(0xf60, 33)) +{ + let hash := mload(0xf80) + mstore(0xfa0, mod(hash, f_q)) + mstore(0xfc0, hash) + } + + { + let x := calldataload(0xd20) + mstore(0xfe0, x) + let y := calldataload(0xd40) + mstore(0x1000, y) + success := and(validate_ec_point(x, y), success) + } +mstore(0x1020, keccak256(0xfc0, 96)) +{ + let hash := mload(0x1020) + mstore(0x1040, mod(hash, f_q)) + mstore(0x1060, hash) + } + + { + let x := calldataload(0xd60) + mstore(0x1080, x) + let y := calldataload(0xd80) + mstore(0x10a0, y) + success := and(validate_ec_point(x, y), success) + } +{ + let x := mload(0x20) +x := add(x, shl(88, mload(0x40))) +x := add(x, shl(176, mload(0x60))) +mstore(4288, x) +let y := mload(0x80) +y := add(y, shl(88, mload(0xa0))) +y := add(y, shl(176, mload(0xc0))) +mstore(4320, y) + + success := and(validate_ec_point(x, y), success) + } +{ + let x := mload(0xe0) +x := add(x, shl(88, mload(0x100))) +x := add(x, shl(176, mload(0x120))) +mstore(4352, x) +let y := mload(0x140) +y := add(y, shl(88, mload(0x160))) +y := add(y, shl(176, mload(0x180))) +mstore(4384, y) + + success := and(validate_ec_point(x, y), success) + } +mstore(0x1140, mulmod(mload(0x840), mload(0x840), f_q)) +mstore(0x1160, mulmod(mload(0x1140), mload(0x1140), f_q)) +mstore(0x1180, mulmod(mload(0x1160), mload(0x1160), f_q)) +mstore(0x11a0, mulmod(mload(0x1180), mload(0x1180), f_q)) +mstore(0x11c0, mulmod(mload(0x11a0), mload(0x11a0), f_q)) +mstore(0x11e0, mulmod(mload(0x11c0), mload(0x11c0), f_q)) +mstore(0x1200, mulmod(mload(0x11e0), mload(0x11e0), f_q)) +mstore(0x1220, mulmod(mload(0x1200), mload(0x1200), f_q)) +mstore(0x1240, mulmod(mload(0x1220), mload(0x1220), f_q)) +mstore(0x1260, mulmod(mload(0x1240), mload(0x1240), f_q)) +mstore(0x1280, mulmod(mload(0x1260), mload(0x1260), f_q)) +mstore(0x12a0, mulmod(mload(0x1280), mload(0x1280), f_q)) +mstore(0x12c0, mulmod(mload(0x12a0), mload(0x12a0), f_q)) +mstore(0x12e0, mulmod(mload(0x12c0), mload(0x12c0), f_q)) +mstore(0x1300, mulmod(mload(0x12e0), mload(0x12e0), f_q)) +mstore(0x1320, mulmod(mload(0x1300), mload(0x1300), f_q)) +mstore(0x1340, mulmod(mload(0x1320), mload(0x1320), f_q)) +mstore(0x1360, mulmod(mload(0x1340), mload(0x1340), f_q)) +mstore(0x1380, mulmod(mload(0x1360), mload(0x1360), f_q)) +mstore(0x13a0, mulmod(mload(0x1380), mload(0x1380), f_q)) +mstore(0x13c0, mulmod(mload(0x13a0), mload(0x13a0), f_q)) +mstore(0x13e0, mulmod(mload(0x13c0), mload(0x13c0), f_q)) +mstore(0x1400, addmod(mload(0x13e0), 21888242871839275222246405745257275088548364400416034343698204186575808495616, f_q)) +mstore(0x1420, mulmod(mload(0x1400), 21888237653275510688422624196183639687472264873923820041627027729598873448513, f_q)) +mstore(0x1440, mulmod(mload(0x1420), 13225785879531581993054172815365636627224369411478295502904397545373139154045, f_q)) +mstore(0x1460, addmod(mload(0x840), 8662456992307693229192232929891638461323994988937738840793806641202669341572, f_q)) +mstore(0x1480, mulmod(mload(0x1420), 10939663269433627367777756708678102241564365262857670666700619874077960926249, f_q)) +mstore(0x14a0, addmod(mload(0x840), 10948579602405647854468649036579172846983999137558363676997584312497847569368, f_q)) +mstore(0x14c0, mulmod(mload(0x1420), 11016257578652593686382655500910603527869149377564754001549454008164059876499, f_q)) +mstore(0x14e0, addmod(mload(0x840), 10871985293186681535863750244346671560679215022851280342148750178411748619118, f_q)) +mstore(0x1500, mulmod(mload(0x1420), 15402826414547299628414612080036060696555554914079673875872749760617770134879, f_q)) +mstore(0x1520, addmod(mload(0x840), 6485416457291975593831793665221214391992809486336360467825454425958038360738, f_q)) +mstore(0x1540, mulmod(mload(0x1420), 21710372849001950800533397158415938114909991150039389063546734567764856596059, f_q)) +mstore(0x1560, addmod(mload(0x840), 177870022837324421713008586841336973638373250376645280151469618810951899558, f_q)) +mstore(0x1580, mulmod(mload(0x1420), 2785514556381676080176937710880804108647911392478702105860685610379369825016, f_q)) +mstore(0x15a0, addmod(mload(0x840), 19102728315457599142069468034376470979900453007937332237837518576196438670601, f_q)) +mstore(0x15c0, mulmod(mload(0x1420), 8734126352828345679573237859165904705806588461301144420590422589042130041188, f_q)) +mstore(0x15e0, addmod(mload(0x840), 13154116519010929542673167886091370382741775939114889923107781597533678454429, f_q)) +mstore(0x1600, mulmod(mload(0x1420), 1, f_q)) +mstore(0x1620, addmod(mload(0x840), 21888242871839275222246405745257275088548364400416034343698204186575808495616, f_q)) +mstore(0x1640, mulmod(mload(0x1420), 11211301017135681023579411905410872569206244553457844956874280139879520583390, f_q)) +mstore(0x1660, addmod(mload(0x840), 10676941854703594198666993839846402519342119846958189386823924046696287912227, f_q)) +mstore(0x1680, mulmod(mload(0x1420), 1426404432721484388505361748317961535523355871255605456897797744433766488507, f_q)) +mstore(0x16a0, addmod(mload(0x840), 20461838439117790833741043996939313553025008529160428886800406442142042007110, f_q)) +mstore(0x16c0, mulmod(mload(0x1420), 12619617507853212586156872920672483948819476989779550311307282715684870266992, f_q)) +mstore(0x16e0, addmod(mload(0x840), 9268625363986062636089532824584791139728887410636484032390921470890938228625, f_q)) +mstore(0x1700, mulmod(mload(0x1420), 19032961837237948602743626455740240236231119053033140765040043513661803148152, f_q)) +mstore(0x1720, addmod(mload(0x840), 2855281034601326619502779289517034852317245347382893578658160672914005347465, f_q)) +mstore(0x1740, mulmod(mload(0x1420), 915149353520972163646494413843788069594022902357002628455555785223409501882, f_q)) +mstore(0x1760, addmod(mload(0x840), 20973093518318303058599911331413487018954341498059031715242648401352398993735, f_q)) +mstore(0x1780, mulmod(mload(0x1420), 3766081621734395783232337525162072736827576297943013392955872170138036189193, f_q)) +mstore(0x17a0, addmod(mload(0x840), 18122161250104879439014068220095202351720788102473020950742332016437772306424, f_q)) +mstore(0x17c0, mulmod(mload(0x1420), 4245441013247250116003069945606352967193023389718465410501109428393342802981, f_q)) +mstore(0x17e0, addmod(mload(0x840), 17642801858592025106243335799650922121355341010697568933197094758182465692636, f_q)) +mstore(0x1800, mulmod(mload(0x1420), 5854133144571823792863860130267644613802765696134002830362054821530146160770, f_q)) +mstore(0x1820, addmod(mload(0x840), 16034109727267451429382545614989630474745598704282031513336149365045662334847, f_q)) +mstore(0x1840, mulmod(mload(0x1420), 5980488956150442207659150513163747165544364597008566989111579977672498964212, f_q)) +mstore(0x1860, addmod(mload(0x840), 15907753915688833014587255232093527923003999803407467354586624208903309531405, f_q)) +mstore(0x1880, mulmod(mload(0x1420), 14557038802599140430182096396825290815503940951075961210638273254419942783582, f_q)) +mstore(0x18a0, addmod(mload(0x840), 7331204069240134792064309348431984273044423449340073133059930932155865712035, f_q)) +mstore(0x18c0, mulmod(mload(0x1420), 13553911191894110065493137367144919847521088405945523452288398666974237857208, f_q)) +mstore(0x18e0, addmod(mload(0x840), 8334331679945165156753268378112355241027275994470510891409805519601570638409, f_q)) +mstore(0x1900, mulmod(mload(0x1420), 9697063347556872083384215826199993067635178715531258559890418744774301211662, f_q)) +mstore(0x1920, addmod(mload(0x840), 12191179524282403138862189919057282020913185684884775783807785441801507283955, f_q)) +mstore(0x1940, mulmod(mload(0x1420), 10807735674816066981985242612061336605021639643453679977988966079770672437131, f_q)) +mstore(0x1960, addmod(mload(0x840), 11080507197023208240261163133195938483526724756962354365709238106805136058486, f_q)) +mstore(0x1980, mulmod(mload(0x1420), 12459868075641381822485233712013080087763946065665469821362892189399541605692, f_q)) +mstore(0x19a0, addmod(mload(0x840), 9428374796197893399761172033244195000784418334750564522335311997176266889925, f_q)) +mstore(0x19c0, mulmod(mload(0x1420), 16038300751658239075779628684257016433412502747804121525056508685985277092575, f_q)) +mstore(0x19e0, addmod(mload(0x840), 5849942120181036146466777061000258655135861652611912818641695500590531403042, f_q)) +mstore(0x1a00, mulmod(mload(0x1420), 6955697244493336113861667751840378876927906302623587437721024018233754910398, f_q)) +mstore(0x1a20, addmod(mload(0x840), 14932545627345939108384737993416896211620458097792446905977180168342053585219, f_q)) +mstore(0x1a40, mulmod(mload(0x1420), 13498745591877810872211159461644682954739332524336278910448604883789771736885, f_q)) +mstore(0x1a60, addmod(mload(0x840), 8389497279961464350035246283612592133809031876079755433249599302786036758732, f_q)) +{ + let prod := mload(0x1460) + + prod := mulmod(mload(0x14a0), prod, f_q) + mstore(0x1a80, prod) + + prod := mulmod(mload(0x14e0), prod, f_q) + mstore(0x1aa0, prod) + + prod := mulmod(mload(0x1520), prod, f_q) + mstore(0x1ac0, prod) + + prod := mulmod(mload(0x1560), prod, f_q) + mstore(0x1ae0, prod) + + prod := mulmod(mload(0x15a0), prod, f_q) + mstore(0x1b00, prod) + + prod := mulmod(mload(0x15e0), prod, f_q) + mstore(0x1b20, prod) + + prod := mulmod(mload(0x1620), prod, f_q) + mstore(0x1b40, prod) + + prod := mulmod(mload(0x1660), prod, f_q) + mstore(0x1b60, prod) + + prod := mulmod(mload(0x16a0), prod, f_q) + mstore(0x1b80, prod) + + prod := mulmod(mload(0x16e0), prod, f_q) + mstore(0x1ba0, prod) + + prod := mulmod(mload(0x1720), prod, f_q) + mstore(0x1bc0, prod) + + prod := mulmod(mload(0x1760), prod, f_q) + mstore(0x1be0, prod) + + prod := mulmod(mload(0x17a0), prod, f_q) + mstore(0x1c00, prod) + + prod := mulmod(mload(0x17e0), prod, f_q) + mstore(0x1c20, prod) + + prod := mulmod(mload(0x1820), prod, f_q) + mstore(0x1c40, prod) + + prod := mulmod(mload(0x1860), prod, f_q) + mstore(0x1c60, prod) + + prod := mulmod(mload(0x18a0), prod, f_q) + mstore(0x1c80, prod) + + prod := mulmod(mload(0x18e0), prod, f_q) + mstore(0x1ca0, prod) + + prod := mulmod(mload(0x1920), prod, f_q) + mstore(0x1cc0, prod) + + prod := mulmod(mload(0x1960), prod, f_q) + mstore(0x1ce0, prod) + + prod := mulmod(mload(0x19a0), prod, f_q) + mstore(0x1d00, prod) + + prod := mulmod(mload(0x19e0), prod, f_q) + mstore(0x1d20, prod) + + prod := mulmod(mload(0x1a20), prod, f_q) + mstore(0x1d40, prod) + + prod := mulmod(mload(0x1a60), prod, f_q) + mstore(0x1d60, prod) + + prod := mulmod(mload(0x1400), prod, f_q) + mstore(0x1d80, prod) + + } +mstore(0x1dc0, 32) +mstore(0x1de0, 32) +mstore(0x1e00, 32) +mstore(0x1e20, mload(0x1d80)) +mstore(0x1e40, 21888242871839275222246405745257275088548364400416034343698204186575808495615) +mstore(0x1e60, 21888242871839275222246405745257275088548364400416034343698204186575808495617) +success := and(eq(staticcall(gas(), 0x5, 0x1dc0, 0xc0, 0x1da0, 0x20), 1), success) +{ + + let inv := mload(0x1da0) + let v + + v := mload(0x1400) + mstore(5120, mulmod(mload(0x1d60), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x1a60) + mstore(6752, mulmod(mload(0x1d40), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x1a20) + mstore(6688, mulmod(mload(0x1d20), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x19e0) + mstore(6624, mulmod(mload(0x1d00), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x19a0) + mstore(6560, mulmod(mload(0x1ce0), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x1960) + mstore(6496, mulmod(mload(0x1cc0), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x1920) + mstore(6432, mulmod(mload(0x1ca0), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x18e0) + mstore(6368, mulmod(mload(0x1c80), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x18a0) + mstore(6304, mulmod(mload(0x1c60), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x1860) + mstore(6240, mulmod(mload(0x1c40), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x1820) + mstore(6176, mulmod(mload(0x1c20), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x17e0) + mstore(6112, mulmod(mload(0x1c00), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x17a0) + mstore(6048, mulmod(mload(0x1be0), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x1760) + mstore(5984, mulmod(mload(0x1bc0), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x1720) + mstore(5920, mulmod(mload(0x1ba0), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x16e0) + mstore(5856, mulmod(mload(0x1b80), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x16a0) + mstore(5792, mulmod(mload(0x1b60), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x1660) + mstore(5728, mulmod(mload(0x1b40), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x1620) + mstore(5664, mulmod(mload(0x1b20), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x15e0) + mstore(5600, mulmod(mload(0x1b00), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x15a0) + mstore(5536, mulmod(mload(0x1ae0), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x1560) + mstore(5472, mulmod(mload(0x1ac0), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x1520) + mstore(5408, mulmod(mload(0x1aa0), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x14e0) + mstore(5344, mulmod(mload(0x1a80), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x14a0) + mstore(5280, mulmod(mload(0x1460), inv, f_q)) + inv := mulmod(v, inv, f_q) + mstore(0x1460, inv) + + } +mstore(0x1e80, mulmod(mload(0x1440), mload(0x1460), f_q)) +mstore(0x1ea0, mulmod(mload(0x1480), mload(0x14a0), f_q)) +mstore(0x1ec0, mulmod(mload(0x14c0), mload(0x14e0), f_q)) +mstore(0x1ee0, mulmod(mload(0x1500), mload(0x1520), f_q)) +mstore(0x1f00, mulmod(mload(0x1540), mload(0x1560), f_q)) +mstore(0x1f20, mulmod(mload(0x1580), mload(0x15a0), f_q)) +mstore(0x1f40, mulmod(mload(0x15c0), mload(0x15e0), f_q)) +mstore(0x1f60, mulmod(mload(0x1600), mload(0x1620), f_q)) +mstore(0x1f80, mulmod(mload(0x1640), mload(0x1660), f_q)) +mstore(0x1fa0, mulmod(mload(0x1680), mload(0x16a0), f_q)) +mstore(0x1fc0, mulmod(mload(0x16c0), mload(0x16e0), f_q)) +mstore(0x1fe0, mulmod(mload(0x1700), mload(0x1720), f_q)) +mstore(0x2000, mulmod(mload(0x1740), mload(0x1760), f_q)) +mstore(0x2020, mulmod(mload(0x1780), mload(0x17a0), f_q)) +mstore(0x2040, mulmod(mload(0x17c0), mload(0x17e0), f_q)) +mstore(0x2060, mulmod(mload(0x1800), mload(0x1820), f_q)) +mstore(0x2080, mulmod(mload(0x1840), mload(0x1860), f_q)) +mstore(0x20a0, mulmod(mload(0x1880), mload(0x18a0), f_q)) +mstore(0x20c0, mulmod(mload(0x18c0), mload(0x18e0), f_q)) +mstore(0x20e0, mulmod(mload(0x1900), mload(0x1920), f_q)) +mstore(0x2100, mulmod(mload(0x1940), mload(0x1960), f_q)) +mstore(0x2120, mulmod(mload(0x1980), mload(0x19a0), f_q)) +mstore(0x2140, mulmod(mload(0x19c0), mload(0x19e0), f_q)) +mstore(0x2160, mulmod(mload(0x1a00), mload(0x1a20), f_q)) +mstore(0x2180, mulmod(mload(0x1a40), mload(0x1a60), f_q)) +{ + let result := mulmod(mload(0x1f60), mload(0x20), f_q) +result := addmod(mulmod(mload(0x1f80), mload(0x40), f_q), result, f_q) +result := addmod(mulmod(mload(0x1fa0), mload(0x60), f_q), result, f_q) +result := addmod(mulmod(mload(0x1fc0), mload(0x80), f_q), result, f_q) +result := addmod(mulmod(mload(0x1fe0), mload(0xa0), f_q), result, f_q) +result := addmod(mulmod(mload(0x2000), mload(0xc0), f_q), result, f_q) +result := addmod(mulmod(mload(0x2020), mload(0xe0), f_q), result, f_q) +result := addmod(mulmod(mload(0x2040), mload(0x100), f_q), result, f_q) +result := addmod(mulmod(mload(0x2060), mload(0x120), f_q), result, f_q) +result := addmod(mulmod(mload(0x2080), mload(0x140), f_q), result, f_q) +result := addmod(mulmod(mload(0x20a0), mload(0x160), f_q), result, f_q) +result := addmod(mulmod(mload(0x20c0), mload(0x180), f_q), result, f_q) +result := addmod(mulmod(mload(0x20e0), mload(0x1a0), f_q), result, f_q) +result := addmod(mulmod(mload(0x2100), mload(0x1c0), f_q), result, f_q) +result := addmod(mulmod(mload(0x2120), mload(0x1e0), f_q), result, f_q) +result := addmod(mulmod(mload(0x2140), mload(0x200), f_q), result, f_q) +result := addmod(mulmod(mload(0x2160), mload(0x220), f_q), result, f_q) +result := addmod(mulmod(mload(0x2180), mload(0x240), f_q), result, f_q) +mstore(8608, result) + } +mstore(0x21c0, mulmod(mload(0x8c0), mload(0x8a0), f_q)) +mstore(0x21e0, addmod(mload(0x880), mload(0x21c0), f_q)) +mstore(0x2200, addmod(mload(0x21e0), sub(f_q, mload(0x8e0)), f_q)) +mstore(0x2220, mulmod(mload(0x2200), mload(0xb60), f_q)) +mstore(0x2240, mulmod(mload(0x720), mload(0x2220), f_q)) +mstore(0x2260, mulmod(mload(0x940), mload(0x920), f_q)) +mstore(0x2280, addmod(mload(0x900), mload(0x2260), f_q)) +mstore(0x22a0, addmod(mload(0x2280), sub(f_q, mload(0x960)), f_q)) +mstore(0x22c0, mulmod(mload(0x22a0), mload(0xb80), f_q)) +mstore(0x22e0, addmod(mload(0x2240), mload(0x22c0), f_q)) +mstore(0x2300, mulmod(mload(0x720), mload(0x22e0), f_q)) +mstore(0x2320, mulmod(mload(0x9c0), mload(0x9a0), f_q)) +mstore(0x2340, addmod(mload(0x980), mload(0x2320), f_q)) +mstore(0x2360, addmod(mload(0x2340), sub(f_q, mload(0x9e0)), f_q)) +mstore(0x2380, mulmod(mload(0x2360), mload(0xba0), f_q)) +mstore(0x23a0, addmod(mload(0x2300), mload(0x2380), f_q)) +mstore(0x23c0, mulmod(mload(0x720), mload(0x23a0), f_q)) +mstore(0x23e0, mulmod(mload(0xa40), mload(0xa20), f_q)) +mstore(0x2400, addmod(mload(0xa00), mload(0x23e0), f_q)) +mstore(0x2420, addmod(mload(0x2400), sub(f_q, mload(0xa60)), f_q)) +mstore(0x2440, mulmod(mload(0x2420), mload(0xbc0), f_q)) +mstore(0x2460, addmod(mload(0x23c0), mload(0x2440), f_q)) +mstore(0x2480, mulmod(mload(0x720), mload(0x2460), f_q)) +mstore(0x24a0, mulmod(mload(0xac0), mload(0xaa0), f_q)) +mstore(0x24c0, addmod(mload(0xa80), mload(0x24a0), f_q)) +mstore(0x24e0, addmod(mload(0x24c0), sub(f_q, mload(0xae0)), f_q)) +mstore(0x2500, mulmod(mload(0x24e0), mload(0xbe0), f_q)) +mstore(0x2520, addmod(mload(0x2480), mload(0x2500), f_q)) +mstore(0x2540, mulmod(mload(0x720), mload(0x2520), f_q)) +mstore(0x2560, addmod(1, sub(f_q, mload(0xd20)), f_q)) +mstore(0x2580, mulmod(mload(0x2560), mload(0x1f60), f_q)) +mstore(0x25a0, addmod(mload(0x2540), mload(0x2580), f_q)) +mstore(0x25c0, mulmod(mload(0x720), mload(0x25a0), f_q)) +mstore(0x25e0, mulmod(mload(0xe40), mload(0xe40), f_q)) +mstore(0x2600, addmod(mload(0x25e0), sub(f_q, mload(0xe40)), f_q)) +mstore(0x2620, mulmod(mload(0x2600), mload(0x1e80), f_q)) +mstore(0x2640, addmod(mload(0x25c0), mload(0x2620), f_q)) +mstore(0x2660, mulmod(mload(0x720), mload(0x2640), f_q)) +mstore(0x2680, addmod(mload(0xd80), sub(f_q, mload(0xd60)), f_q)) +mstore(0x26a0, mulmod(mload(0x2680), mload(0x1f60), f_q)) +mstore(0x26c0, addmod(mload(0x2660), mload(0x26a0), f_q)) +mstore(0x26e0, mulmod(mload(0x720), mload(0x26c0), f_q)) +mstore(0x2700, addmod(mload(0xde0), sub(f_q, mload(0xdc0)), f_q)) +mstore(0x2720, mulmod(mload(0x2700), mload(0x1f60), f_q)) +mstore(0x2740, addmod(mload(0x26e0), mload(0x2720), f_q)) +mstore(0x2760, mulmod(mload(0x720), mload(0x2740), f_q)) +mstore(0x2780, addmod(mload(0xe40), sub(f_q, mload(0xe20)), f_q)) +mstore(0x27a0, mulmod(mload(0x2780), mload(0x1f60), f_q)) +mstore(0x27c0, addmod(mload(0x2760), mload(0x27a0), f_q)) +mstore(0x27e0, mulmod(mload(0x720), mload(0x27c0), f_q)) +mstore(0x2800, addmod(1, sub(f_q, mload(0x1e80)), f_q)) +mstore(0x2820, addmod(mload(0x1ea0), mload(0x1ec0), f_q)) +mstore(0x2840, addmod(mload(0x2820), mload(0x1ee0), f_q)) +mstore(0x2860, addmod(mload(0x2840), mload(0x1f00), f_q)) +mstore(0x2880, addmod(mload(0x2860), mload(0x1f20), f_q)) +mstore(0x28a0, addmod(mload(0x2880), mload(0x1f40), f_q)) +mstore(0x28c0, addmod(mload(0x2800), sub(f_q, mload(0x28a0)), f_q)) +mstore(0x28e0, mulmod(mload(0xc20), mload(0x4e0), f_q)) +mstore(0x2900, addmod(mload(0xb20), mload(0x28e0), f_q)) +mstore(0x2920, addmod(mload(0x2900), mload(0x540), f_q)) +mstore(0x2940, mulmod(mload(0xc40), mload(0x4e0), f_q)) +mstore(0x2960, addmod(mload(0x880), mload(0x2940), f_q)) +mstore(0x2980, addmod(mload(0x2960), mload(0x540), f_q)) +mstore(0x29a0, mulmod(mload(0x2980), mload(0x2920), f_q)) +mstore(0x29c0, mulmod(mload(0x29a0), mload(0xd40), f_q)) +mstore(0x29e0, mulmod(1, mload(0x4e0), f_q)) +mstore(0x2a00, mulmod(mload(0x840), mload(0x29e0), f_q)) +mstore(0x2a20, addmod(mload(0xb20), mload(0x2a00), f_q)) +mstore(0x2a40, addmod(mload(0x2a20), mload(0x540), f_q)) +mstore(0x2a60, mulmod(4131629893567559867359510883348571134090853742863529169391034518566172092834, mload(0x4e0), f_q)) +mstore(0x2a80, mulmod(mload(0x840), mload(0x2a60), f_q)) +mstore(0x2aa0, addmod(mload(0x880), mload(0x2a80), f_q)) +mstore(0x2ac0, addmod(mload(0x2aa0), mload(0x540), f_q)) +mstore(0x2ae0, mulmod(mload(0x2ac0), mload(0x2a40), f_q)) +mstore(0x2b00, mulmod(mload(0x2ae0), mload(0xd20), f_q)) +mstore(0x2b20, addmod(mload(0x29c0), sub(f_q, mload(0x2b00)), f_q)) +mstore(0x2b40, mulmod(mload(0x2b20), mload(0x28c0), f_q)) +mstore(0x2b60, addmod(mload(0x27e0), mload(0x2b40), f_q)) +mstore(0x2b80, mulmod(mload(0x720), mload(0x2b60), f_q)) +mstore(0x2ba0, mulmod(mload(0xc60), mload(0x4e0), f_q)) +mstore(0x2bc0, addmod(mload(0x900), mload(0x2ba0), f_q)) +mstore(0x2be0, addmod(mload(0x2bc0), mload(0x540), f_q)) +mstore(0x2c00, mulmod(mload(0xc80), mload(0x4e0), f_q)) +mstore(0x2c20, addmod(mload(0x980), mload(0x2c00), f_q)) +mstore(0x2c40, addmod(mload(0x2c20), mload(0x540), f_q)) +mstore(0x2c60, mulmod(mload(0x2c40), mload(0x2be0), f_q)) +mstore(0x2c80, mulmod(mload(0x2c60), mload(0xda0), f_q)) +mstore(0x2ca0, mulmod(8910878055287538404433155982483128285667088683464058436815641868457422632747, mload(0x4e0), f_q)) +mstore(0x2cc0, mulmod(mload(0x840), mload(0x2ca0), f_q)) +mstore(0x2ce0, addmod(mload(0x900), mload(0x2cc0), f_q)) +mstore(0x2d00, addmod(mload(0x2ce0), mload(0x540), f_q)) +mstore(0x2d20, mulmod(11166246659983828508719468090013646171463329086121580628794302409516816350802, mload(0x4e0), f_q)) +mstore(0x2d40, mulmod(mload(0x840), mload(0x2d20), f_q)) +mstore(0x2d60, addmod(mload(0x980), mload(0x2d40), f_q)) +mstore(0x2d80, addmod(mload(0x2d60), mload(0x540), f_q)) +mstore(0x2da0, mulmod(mload(0x2d80), mload(0x2d00), f_q)) +mstore(0x2dc0, mulmod(mload(0x2da0), mload(0xd80), f_q)) +mstore(0x2de0, addmod(mload(0x2c80), sub(f_q, mload(0x2dc0)), f_q)) +mstore(0x2e00, mulmod(mload(0x2de0), mload(0x28c0), f_q)) +mstore(0x2e20, addmod(mload(0x2b80), mload(0x2e00), f_q)) +mstore(0x2e40, mulmod(mload(0x720), mload(0x2e20), f_q)) +mstore(0x2e60, mulmod(mload(0xca0), mload(0x4e0), f_q)) +mstore(0x2e80, addmod(mload(0xa00), mload(0x2e60), f_q)) +mstore(0x2ea0, addmod(mload(0x2e80), mload(0x540), f_q)) +mstore(0x2ec0, mulmod(mload(0xcc0), mload(0x4e0), f_q)) +mstore(0x2ee0, addmod(mload(0xa80), mload(0x2ec0), f_q)) +mstore(0x2f00, addmod(mload(0x2ee0), mload(0x540), f_q)) +mstore(0x2f20, mulmod(mload(0x2f00), mload(0x2ea0), f_q)) +mstore(0x2f40, mulmod(mload(0x2f20), mload(0xe00), f_q)) +mstore(0x2f60, mulmod(284840088355319032285349970403338060113257071685626700086398481893096618818, mload(0x4e0), f_q)) +mstore(0x2f80, mulmod(mload(0x840), mload(0x2f60), f_q)) +mstore(0x2fa0, addmod(mload(0xa00), mload(0x2f80), f_q)) +mstore(0x2fc0, addmod(mload(0x2fa0), mload(0x540), f_q)) +mstore(0x2fe0, mulmod(21134065618345176623193549882539580312263652408302468683943992798037078993309, mload(0x4e0), f_q)) +mstore(0x3000, mulmod(mload(0x840), mload(0x2fe0), f_q)) +mstore(0x3020, addmod(mload(0xa80), mload(0x3000), f_q)) +mstore(0x3040, addmod(mload(0x3020), mload(0x540), f_q)) +mstore(0x3060, mulmod(mload(0x3040), mload(0x2fc0), f_q)) +mstore(0x3080, mulmod(mload(0x3060), mload(0xde0), f_q)) +mstore(0x30a0, addmod(mload(0x2f40), sub(f_q, mload(0x3080)), f_q)) +mstore(0x30c0, mulmod(mload(0x30a0), mload(0x28c0), f_q)) +mstore(0x30e0, addmod(mload(0x2e40), mload(0x30c0), f_q)) +mstore(0x3100, mulmod(mload(0x720), mload(0x30e0), f_q)) +mstore(0x3120, mulmod(mload(0xce0), mload(0x4e0), f_q)) +mstore(0x3140, addmod(mload(0xb00), mload(0x3120), f_q)) +mstore(0x3160, addmod(mload(0x3140), mload(0x540), f_q)) +mstore(0x3180, mulmod(mload(0xd00), mload(0x4e0), f_q)) +mstore(0x31a0, addmod(mload(0x21a0), mload(0x3180), f_q)) +mstore(0x31c0, addmod(mload(0x31a0), mload(0x540), f_q)) +mstore(0x31e0, mulmod(mload(0x31c0), mload(0x3160), f_q)) +mstore(0x3200, mulmod(mload(0x31e0), mload(0xe60), f_q)) +mstore(0x3220, mulmod(5625741653535312224677218588085279924365897425605943700675464992185016992283, mload(0x4e0), f_q)) +mstore(0x3240, mulmod(mload(0x840), mload(0x3220), f_q)) +mstore(0x3260, addmod(mload(0xb00), mload(0x3240), f_q)) +mstore(0x3280, addmod(mload(0x3260), mload(0x540), f_q)) +mstore(0x32a0, mulmod(14704729814417906439424896605881467874595262020190401576785074330126828718155, mload(0x4e0), f_q)) +mstore(0x32c0, mulmod(mload(0x840), mload(0x32a0), f_q)) +mstore(0x32e0, addmod(mload(0x21a0), mload(0x32c0), f_q)) +mstore(0x3300, addmod(mload(0x32e0), mload(0x540), f_q)) +mstore(0x3320, mulmod(mload(0x3300), mload(0x3280), f_q)) +mstore(0x3340, mulmod(mload(0x3320), mload(0xe40), f_q)) +mstore(0x3360, addmod(mload(0x3200), sub(f_q, mload(0x3340)), f_q)) +mstore(0x3380, mulmod(mload(0x3360), mload(0x28c0), f_q)) +mstore(0x33a0, addmod(mload(0x3100), mload(0x3380), f_q)) +mstore(0x33c0, mulmod(mload(0x720), mload(0x33a0), f_q)) +mstore(0x33e0, addmod(1, sub(f_q, mload(0xe80)), f_q)) +mstore(0x3400, mulmod(mload(0x33e0), mload(0x1f60), f_q)) +mstore(0x3420, addmod(mload(0x33c0), mload(0x3400), f_q)) +mstore(0x3440, mulmod(mload(0x720), mload(0x3420), f_q)) +mstore(0x3460, mulmod(mload(0xe80), mload(0xe80), f_q)) +mstore(0x3480, addmod(mload(0x3460), sub(f_q, mload(0xe80)), f_q)) +mstore(0x34a0, mulmod(mload(0x3480), mload(0x1e80), f_q)) +mstore(0x34c0, addmod(mload(0x3440), mload(0x34a0), f_q)) +mstore(0x34e0, mulmod(mload(0x720), mload(0x34c0), f_q)) +mstore(0x3500, addmod(mload(0xec0), mload(0x4e0), f_q)) +mstore(0x3520, mulmod(mload(0x3500), mload(0xea0), f_q)) +mstore(0x3540, addmod(mload(0xf00), mload(0x540), f_q)) +mstore(0x3560, mulmod(mload(0x3540), mload(0x3520), f_q)) +mstore(0x3580, addmod(mload(0xb00), mload(0x4e0), f_q)) +mstore(0x35a0, mulmod(mload(0x3580), mload(0xe80), f_q)) +mstore(0x35c0, addmod(mload(0xb40), mload(0x540), f_q)) +mstore(0x35e0, mulmod(mload(0x35c0), mload(0x35a0), f_q)) +mstore(0x3600, addmod(mload(0x3560), sub(f_q, mload(0x35e0)), f_q)) +mstore(0x3620, mulmod(mload(0x3600), mload(0x28c0), f_q)) +mstore(0x3640, addmod(mload(0x34e0), mload(0x3620), f_q)) +mstore(0x3660, mulmod(mload(0x720), mload(0x3640), f_q)) +mstore(0x3680, addmod(mload(0xec0), sub(f_q, mload(0xf00)), f_q)) +mstore(0x36a0, mulmod(mload(0x3680), mload(0x1f60), f_q)) +mstore(0x36c0, addmod(mload(0x3660), mload(0x36a0), f_q)) +mstore(0x36e0, mulmod(mload(0x720), mload(0x36c0), f_q)) +mstore(0x3700, mulmod(mload(0x3680), mload(0x28c0), f_q)) +mstore(0x3720, addmod(mload(0xec0), sub(f_q, mload(0xee0)), f_q)) +mstore(0x3740, mulmod(mload(0x3720), mload(0x3700), f_q)) +mstore(0x3760, addmod(mload(0x36e0), mload(0x3740), f_q)) +mstore(0x3780, mulmod(mload(0x13e0), mload(0x13e0), f_q)) +mstore(0x37a0, mulmod(mload(0x3780), mload(0x13e0), f_q)) +mstore(0x37c0, mulmod(1, mload(0x13e0), f_q)) +mstore(0x37e0, mulmod(1, mload(0x3780), f_q)) +mstore(0x3800, mulmod(mload(0x3760), mload(0x1400), f_q)) +mstore(0x3820, mulmod(mload(0x1140), mload(0x840), f_q)) +mstore(0x3840, mulmod(mload(0x3820), mload(0x840), f_q)) +mstore(0x3860, mulmod(mload(0x840), 1, f_q)) +mstore(0x3880, addmod(mload(0x1040), sub(f_q, mload(0x3860)), f_q)) +mstore(0x38a0, mulmod(mload(0x840), 1426404432721484388505361748317961535523355871255605456897797744433766488507, f_q)) +mstore(0x38c0, addmod(mload(0x1040), sub(f_q, mload(0x38a0)), f_q)) +mstore(0x38e0, mulmod(mload(0x840), 8734126352828345679573237859165904705806588461301144420590422589042130041188, f_q)) +mstore(0x3900, addmod(mload(0x1040), sub(f_q, mload(0x38e0)), f_q)) +mstore(0x3920, mulmod(mload(0x840), 11211301017135681023579411905410872569206244553457844956874280139879520583390, f_q)) +mstore(0x3940, addmod(mload(0x1040), sub(f_q, mload(0x3920)), f_q)) +mstore(0x3960, mulmod(mload(0x840), 12619617507853212586156872920672483948819476989779550311307282715684870266992, f_q)) +mstore(0x3980, addmod(mload(0x1040), sub(f_q, mload(0x3960)), f_q)) +mstore(0x39a0, mulmod(mload(0x840), 13225785879531581993054172815365636627224369411478295502904397545373139154045, f_q)) +mstore(0x39c0, addmod(mload(0x1040), sub(f_q, mload(0x39a0)), f_q)) +{ + let result := mulmod(mload(0x1040), mulmod(mload(0x3820), 3544324119167359571073009690693121464267965232733679586767649244433889388945, f_q), f_q) +result := addmod(mulmod(mload(0x840), mulmod(mload(0x3820), 18343918752671915651173396054564153624280399167682354756930554942141919106672, f_q), f_q), result, f_q) +mstore(14816, result) + } +{ + let result := mulmod(mload(0x1040), mulmod(mload(0x3820), 3860370625838117017501327045244227871206764201116468958063324100051382735289, f_q), f_q) +result := addmod(mulmod(mload(0x840), mulmod(mload(0x3820), 21616901807277407275624036604424346159916096890712898844034238973395610537327, f_q), f_q), result, f_q) +mstore(14848, result) + } +{ + let result := mulmod(mload(0x1040), mulmod(mload(0x3820), 21616901807277407275624036604424346159916096890712898844034238973395610537327, f_q), f_q) +result := addmod(mulmod(mload(0x840), mulmod(mload(0x3820), 889236556954614024749610889108815341999962898269585485843658889664869519176, f_q), f_q), result, f_q) +mstore(14880, result) + } +{ + let result := mulmod(mload(0x1040), mulmod(mload(0x3820), 3209408481237076479025468386201293941554240476766691830436732310949352383503, f_q), f_q) +result := addmod(mulmod(mload(0x840), mulmod(mload(0x3820), 12080394110851700286656425387058292751221637853580771255128961096834426654570, f_q), f_q), result, f_q) +mstore(14912, result) + } +mstore(0x3a60, mulmod(1, mload(0x3880), f_q)) +mstore(0x3a80, mulmod(mload(0x3a60), mload(0x3940), f_q)) +mstore(0x3aa0, mulmod(mload(0x3a80), mload(0x38c0), f_q)) +mstore(0x3ac0, mulmod(mload(0x3aa0), mload(0x3980), f_q)) +{ + let result := mulmod(mload(0x1040), 1, f_q) +result := addmod(mulmod(mload(0x840), 21888242871839275222246405745257275088548364400416034343698204186575808495616, f_q), result, f_q) +mstore(15072, result) + } +{ + let result := mulmod(mload(0x1040), mulmod(mload(0x1140), 8390819244605639573390577733158868133682115698337564550620146375401109684432, f_q), f_q) +result := addmod(mulmod(mload(0x840), mulmod(mload(0x1140), 13497423627233635648855828012098406954866248702078469793078057811174698811185, f_q), f_q), result, f_q) +mstore(15104, result) + } +{ + let result := mulmod(mload(0x1040), mulmod(mload(0x1140), 14389468897523033212448771694851898440525479866834419679925499462425232628530, f_q), f_q) +result := addmod(mulmod(mload(0x840), mulmod(mload(0x1140), 10771624105926513343199793365135253961557027396599172824137553349410803667382, f_q), f_q), result, f_q) +mstore(15136, result) + } +{ + let result := mulmod(mload(0x1040), mulmod(mload(0x1140), 8021781111580269725587432039983408559403601261632071736490564397134126857583, f_q), f_q) +result := addmod(mulmod(mload(0x840), mulmod(mload(0x1140), 13263758384809315129424392494083758423780924407584659157289746760747196496964, f_q), f_q), result, f_q) +mstore(15168, result) + } +mstore(0x3b60, mulmod(mload(0x3a80), mload(0x39c0), f_q)) +{ + let result := mulmod(mload(0x1040), mulmod(mload(0x840), 10676941854703594198666993839846402519342119846958189386823924046696287912228, f_q), f_q) +result := addmod(mulmod(mload(0x840), mulmod(mload(0x840), 11211301017135681023579411905410872569206244553457844956874280139879520583389, f_q), f_q), result, f_q) +mstore(15232, result) + } +{ + let result := mulmod(mload(0x1040), mulmod(mload(0x840), 11211301017135681023579411905410872569206244553457844956874280139879520583389, f_q), f_q) +result := addmod(mulmod(mload(0x840), mulmod(mload(0x840), 9784896584414196635074050157092911033682888682202239499976482395445754094883, f_q), f_q), result, f_q) +mstore(15264, result) + } +{ + let result := mulmod(mload(0x1040), mulmod(mload(0x840), 13154116519010929542673167886091370382741775939114889923107781597533678454430, f_q), f_q) +result := addmod(mulmod(mload(0x840), mulmod(mload(0x840), 8734126352828345679573237859165904705806588461301144420590422589042130041187, f_q), f_q), result, f_q) +mstore(15296, result) + } +{ + let result := mulmod(mload(0x1040), mulmod(mload(0x840), 8734126352828345679573237859165904705806588461301144420590422589042130041187, f_q), f_q) +result := addmod(mulmod(mload(0x840), mulmod(mload(0x840), 5948611796446669599396300148285100597158677068822442314729736978662760216172, f_q), f_q), result, f_q) +mstore(15328, result) + } +mstore(0x3c00, mulmod(mload(0x3a60), mload(0x3900), f_q)) +{ + let prod := mload(0x39e0) + + prod := mulmod(mload(0x3a00), prod, f_q) + mstore(0x3c20, prod) + + prod := mulmod(mload(0x3a20), prod, f_q) + mstore(0x3c40, prod) + + prod := mulmod(mload(0x3a40), prod, f_q) + mstore(0x3c60, prod) + + prod := mulmod(mload(0x3ae0), prod, f_q) + mstore(0x3c80, prod) + + prod := mulmod(mload(0x3a60), prod, f_q) + mstore(0x3ca0, prod) + + prod := mulmod(mload(0x3b00), prod, f_q) + mstore(0x3cc0, prod) + + prod := mulmod(mload(0x3b20), prod, f_q) + mstore(0x3ce0, prod) + + prod := mulmod(mload(0x3b40), prod, f_q) + mstore(0x3d00, prod) + + prod := mulmod(mload(0x3b60), prod, f_q) + mstore(0x3d20, prod) + + prod := mulmod(mload(0x3b80), prod, f_q) + mstore(0x3d40, prod) + + prod := mulmod(mload(0x3ba0), prod, f_q) + mstore(0x3d60, prod) + + prod := mulmod(mload(0x3a80), prod, f_q) + mstore(0x3d80, prod) + + prod := mulmod(mload(0x3bc0), prod, f_q) + mstore(0x3da0, prod) + + prod := mulmod(mload(0x3be0), prod, f_q) + mstore(0x3dc0, prod) + + prod := mulmod(mload(0x3c00), prod, f_q) + mstore(0x3de0, prod) + + } +mstore(0x3e20, 32) +mstore(0x3e40, 32) +mstore(0x3e60, 32) +mstore(0x3e80, mload(0x3de0)) +mstore(0x3ea0, 21888242871839275222246405745257275088548364400416034343698204186575808495615) +mstore(0x3ec0, 21888242871839275222246405745257275088548364400416034343698204186575808495617) +success := and(eq(staticcall(gas(), 0x5, 0x3e20, 0xc0, 0x3e00, 0x20), 1), success) +{ + + let inv := mload(0x3e00) + let v + + v := mload(0x3c00) + mstore(15360, mulmod(mload(0x3dc0), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3be0) + mstore(15328, mulmod(mload(0x3da0), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3bc0) + mstore(15296, mulmod(mload(0x3d80), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3a80) + mstore(14976, mulmod(mload(0x3d60), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3ba0) + mstore(15264, mulmod(mload(0x3d40), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3b80) + mstore(15232, mulmod(mload(0x3d20), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3b60) + mstore(15200, mulmod(mload(0x3d00), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3b40) + mstore(15168, mulmod(mload(0x3ce0), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3b20) + mstore(15136, mulmod(mload(0x3cc0), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3b00) + mstore(15104, mulmod(mload(0x3ca0), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3a60) + mstore(14944, mulmod(mload(0x3c80), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3ae0) + mstore(15072, mulmod(mload(0x3c60), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3a40) + mstore(14912, mulmod(mload(0x3c40), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3a20) + mstore(14880, mulmod(mload(0x3c20), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3a00) + mstore(14848, mulmod(mload(0x39e0), inv, f_q)) + inv := mulmod(v, inv, f_q) + mstore(0x39e0, inv) + + } +{ + let result := mload(0x39e0) +result := addmod(mload(0x3a00), result, f_q) +result := addmod(mload(0x3a20), result, f_q) +result := addmod(mload(0x3a40), result, f_q) +mstore(16096, result) + } +mstore(0x3f00, mulmod(mload(0x3ac0), mload(0x3a60), f_q)) +{ + let result := mload(0x3ae0) +mstore(16160, result) + } +mstore(0x3f40, mulmod(mload(0x3ac0), mload(0x3b60), f_q)) +{ + let result := mload(0x3b00) +result := addmod(mload(0x3b20), result, f_q) +result := addmod(mload(0x3b40), result, f_q) +mstore(16224, result) + } +mstore(0x3f80, mulmod(mload(0x3ac0), mload(0x3a80), f_q)) +{ + let result := mload(0x3b80) +result := addmod(mload(0x3ba0), result, f_q) +mstore(16288, result) + } +mstore(0x3fc0, mulmod(mload(0x3ac0), mload(0x3c00), f_q)) +{ + let result := mload(0x3bc0) +result := addmod(mload(0x3be0), result, f_q) +mstore(16352, result) + } +{ + let prod := mload(0x3ee0) + + prod := mulmod(mload(0x3f20), prod, f_q) + mstore(0x4000, prod) + + prod := mulmod(mload(0x3f60), prod, f_q) + mstore(0x4020, prod) + + prod := mulmod(mload(0x3fa0), prod, f_q) + mstore(0x4040, prod) + + prod := mulmod(mload(0x3fe0), prod, f_q) + mstore(0x4060, prod) + + } +mstore(0x40a0, 32) +mstore(0x40c0, 32) +mstore(0x40e0, 32) +mstore(0x4100, mload(0x4060)) +mstore(0x4120, 21888242871839275222246405745257275088548364400416034343698204186575808495615) +mstore(0x4140, 21888242871839275222246405745257275088548364400416034343698204186575808495617) +success := and(eq(staticcall(gas(), 0x5, 0x40a0, 0xc0, 0x4080, 0x20), 1), success) +{ + + let inv := mload(0x4080) + let v + + v := mload(0x3fe0) + mstore(16352, mulmod(mload(0x4040), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3fa0) + mstore(16288, mulmod(mload(0x4020), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3f60) + mstore(16224, mulmod(mload(0x4000), inv, f_q)) + inv := mulmod(v, inv, f_q) + + v := mload(0x3f20) + mstore(16160, mulmod(mload(0x3ee0), inv, f_q)) + inv := mulmod(v, inv, f_q) + mstore(0x3ee0, inv) + + } +mstore(0x4160, mulmod(mload(0x3f00), mload(0x3f20), f_q)) +mstore(0x4180, mulmod(mload(0x3f40), mload(0x3f60), f_q)) +mstore(0x41a0, mulmod(mload(0x3f80), mload(0x3fa0), f_q)) +mstore(0x41c0, mulmod(mload(0x3fc0), mload(0x3fe0), f_q)) +mstore(0x41e0, mulmod(mload(0xf40), mload(0xf40), f_q)) +mstore(0x4200, mulmod(mload(0x41e0), mload(0xf40), f_q)) +mstore(0x4220, mulmod(mload(0x4200), mload(0xf40), f_q)) +mstore(0x4240, mulmod(mload(0x4220), mload(0xf40), f_q)) +mstore(0x4260, mulmod(mload(0x4240), mload(0xf40), f_q)) +mstore(0x4280, mulmod(mload(0x4260), mload(0xf40), f_q)) +mstore(0x42a0, mulmod(mload(0x4280), mload(0xf40), f_q)) +mstore(0x42c0, mulmod(mload(0x42a0), mload(0xf40), f_q)) +mstore(0x42e0, mulmod(mload(0x42c0), mload(0xf40), f_q)) +mstore(0x4300, mulmod(mload(0x42e0), mload(0xf40), f_q)) +mstore(0x4320, mulmod(mload(0x4300), mload(0xf40), f_q)) +mstore(0x4340, mulmod(mload(0x4320), mload(0xf40), f_q)) +mstore(0x4360, mulmod(mload(0x4340), mload(0xf40), f_q)) +mstore(0x4380, mulmod(mload(0x4360), mload(0xf40), f_q)) +mstore(0x43a0, mulmod(mload(0x4380), mload(0xf40), f_q)) +mstore(0x43c0, mulmod(mload(0x43a0), mload(0xf40), f_q)) +mstore(0x43e0, mulmod(mload(0x43c0), mload(0xf40), f_q)) +mstore(0x4400, mulmod(mload(0x43e0), mload(0xf40), f_q)) +mstore(0x4420, mulmod(mload(0xfa0), mload(0xfa0), f_q)) +mstore(0x4440, mulmod(mload(0x4420), mload(0xfa0), f_q)) +mstore(0x4460, mulmod(mload(0x4440), mload(0xfa0), f_q)) +mstore(0x4480, mulmod(mload(0x4460), mload(0xfa0), f_q)) +{ + let result := mulmod(mload(0x880), mload(0x39e0), f_q) +result := addmod(mulmod(mload(0x8a0), mload(0x3a00), f_q), result, f_q) +result := addmod(mulmod(mload(0x8c0), mload(0x3a20), f_q), result, f_q) +result := addmod(mulmod(mload(0x8e0), mload(0x3a40), f_q), result, f_q) +mstore(17568, result) + } +mstore(0x44c0, mulmod(mload(0x44a0), mload(0x3ee0), f_q)) +mstore(0x44e0, mulmod(sub(f_q, mload(0x44c0)), 1, f_q)) +{ + let result := mulmod(mload(0x900), mload(0x39e0), f_q) +result := addmod(mulmod(mload(0x920), mload(0x3a00), f_q), result, f_q) +result := addmod(mulmod(mload(0x940), mload(0x3a20), f_q), result, f_q) +result := addmod(mulmod(mload(0x960), mload(0x3a40), f_q), result, f_q) +mstore(17664, result) + } +mstore(0x4520, mulmod(mload(0x4500), mload(0x3ee0), f_q)) +mstore(0x4540, mulmod(sub(f_q, mload(0x4520)), mload(0xf40), f_q)) +mstore(0x4560, mulmod(1, mload(0xf40), f_q)) +mstore(0x4580, addmod(mload(0x44e0), mload(0x4540), f_q)) +{ + let result := mulmod(mload(0x980), mload(0x39e0), f_q) +result := addmod(mulmod(mload(0x9a0), mload(0x3a00), f_q), result, f_q) +result := addmod(mulmod(mload(0x9c0), mload(0x3a20), f_q), result, f_q) +result := addmod(mulmod(mload(0x9e0), mload(0x3a40), f_q), result, f_q) +mstore(17824, result) + } +mstore(0x45c0, mulmod(mload(0x45a0), mload(0x3ee0), f_q)) +mstore(0x45e0, mulmod(sub(f_q, mload(0x45c0)), mload(0x41e0), f_q)) +mstore(0x4600, mulmod(1, mload(0x41e0), f_q)) +mstore(0x4620, addmod(mload(0x4580), mload(0x45e0), f_q)) +{ + let result := mulmod(mload(0xa00), mload(0x39e0), f_q) +result := addmod(mulmod(mload(0xa20), mload(0x3a00), f_q), result, f_q) +result := addmod(mulmod(mload(0xa40), mload(0x3a20), f_q), result, f_q) +result := addmod(mulmod(mload(0xa60), mload(0x3a40), f_q), result, f_q) +mstore(17984, result) + } +mstore(0x4660, mulmod(mload(0x4640), mload(0x3ee0), f_q)) +mstore(0x4680, mulmod(sub(f_q, mload(0x4660)), mload(0x4200), f_q)) +mstore(0x46a0, mulmod(1, mload(0x4200), f_q)) +mstore(0x46c0, addmod(mload(0x4620), mload(0x4680), f_q)) +{ + let result := mulmod(mload(0xa80), mload(0x39e0), f_q) +result := addmod(mulmod(mload(0xaa0), mload(0x3a00), f_q), result, f_q) +result := addmod(mulmod(mload(0xac0), mload(0x3a20), f_q), result, f_q) +result := addmod(mulmod(mload(0xae0), mload(0x3a40), f_q), result, f_q) +mstore(18144, result) + } +mstore(0x4700, mulmod(mload(0x46e0), mload(0x3ee0), f_q)) +mstore(0x4720, mulmod(sub(f_q, mload(0x4700)), mload(0x4220), f_q)) +mstore(0x4740, mulmod(1, mload(0x4220), f_q)) +mstore(0x4760, addmod(mload(0x46c0), mload(0x4720), f_q)) +mstore(0x4780, mulmod(mload(0x4760), 1, f_q)) +mstore(0x47a0, mulmod(mload(0x4560), 1, f_q)) +mstore(0x47c0, mulmod(mload(0x4600), 1, f_q)) +mstore(0x47e0, mulmod(mload(0x46a0), 1, f_q)) +mstore(0x4800, mulmod(mload(0x4740), 1, f_q)) +mstore(0x4820, mulmod(1, mload(0x3f00), f_q)) +{ + let result := mulmod(mload(0xb00), mload(0x3ae0), f_q) +mstore(18496, result) + } +mstore(0x4860, mulmod(mload(0x4840), mload(0x4160), f_q)) +mstore(0x4880, mulmod(sub(f_q, mload(0x4860)), 1, f_q)) +mstore(0x48a0, mulmod(mload(0x4820), 1, f_q)) +{ + let result := mulmod(mload(0xf00), mload(0x3ae0), f_q) +mstore(18624, result) + } +mstore(0x48e0, mulmod(mload(0x48c0), mload(0x4160), f_q)) +mstore(0x4900, mulmod(sub(f_q, mload(0x48e0)), mload(0xf40), f_q)) +mstore(0x4920, mulmod(mload(0x4820), mload(0xf40), f_q)) +mstore(0x4940, addmod(mload(0x4880), mload(0x4900), f_q)) +{ + let result := mulmod(mload(0xb20), mload(0x3ae0), f_q) +mstore(18784, result) + } +mstore(0x4980, mulmod(mload(0x4960), mload(0x4160), f_q)) +mstore(0x49a0, mulmod(sub(f_q, mload(0x4980)), mload(0x41e0), f_q)) +mstore(0x49c0, mulmod(mload(0x4820), mload(0x41e0), f_q)) +mstore(0x49e0, addmod(mload(0x4940), mload(0x49a0), f_q)) +{ + let result := mulmod(mload(0xb40), mload(0x3ae0), f_q) +mstore(18944, result) + } +mstore(0x4a20, mulmod(mload(0x4a00), mload(0x4160), f_q)) +mstore(0x4a40, mulmod(sub(f_q, mload(0x4a20)), mload(0x4200), f_q)) +mstore(0x4a60, mulmod(mload(0x4820), mload(0x4200), f_q)) +mstore(0x4a80, addmod(mload(0x49e0), mload(0x4a40), f_q)) +{ + let result := mulmod(mload(0xb60), mload(0x3ae0), f_q) +mstore(19104, result) + } +mstore(0x4ac0, mulmod(mload(0x4aa0), mload(0x4160), f_q)) +mstore(0x4ae0, mulmod(sub(f_q, mload(0x4ac0)), mload(0x4220), f_q)) +mstore(0x4b00, mulmod(mload(0x4820), mload(0x4220), f_q)) +mstore(0x4b20, addmod(mload(0x4a80), mload(0x4ae0), f_q)) +{ + let result := mulmod(mload(0xb80), mload(0x3ae0), f_q) +mstore(19264, result) + } +mstore(0x4b60, mulmod(mload(0x4b40), mload(0x4160), f_q)) +mstore(0x4b80, mulmod(sub(f_q, mload(0x4b60)), mload(0x4240), f_q)) +mstore(0x4ba0, mulmod(mload(0x4820), mload(0x4240), f_q)) +mstore(0x4bc0, addmod(mload(0x4b20), mload(0x4b80), f_q)) +{ + let result := mulmod(mload(0xba0), mload(0x3ae0), f_q) +mstore(19424, result) + } +mstore(0x4c00, mulmod(mload(0x4be0), mload(0x4160), f_q)) +mstore(0x4c20, mulmod(sub(f_q, mload(0x4c00)), mload(0x4260), f_q)) +mstore(0x4c40, mulmod(mload(0x4820), mload(0x4260), f_q)) +mstore(0x4c60, addmod(mload(0x4bc0), mload(0x4c20), f_q)) +{ + let result := mulmod(mload(0xbc0), mload(0x3ae0), f_q) +mstore(19584, result) + } +mstore(0x4ca0, mulmod(mload(0x4c80), mload(0x4160), f_q)) +mstore(0x4cc0, mulmod(sub(f_q, mload(0x4ca0)), mload(0x4280), f_q)) +mstore(0x4ce0, mulmod(mload(0x4820), mload(0x4280), f_q)) +mstore(0x4d00, addmod(mload(0x4c60), mload(0x4cc0), f_q)) +{ + let result := mulmod(mload(0xbe0), mload(0x3ae0), f_q) +mstore(19744, result) + } +mstore(0x4d40, mulmod(mload(0x4d20), mload(0x4160), f_q)) +mstore(0x4d60, mulmod(sub(f_q, mload(0x4d40)), mload(0x42a0), f_q)) +mstore(0x4d80, mulmod(mload(0x4820), mload(0x42a0), f_q)) +mstore(0x4da0, addmod(mload(0x4d00), mload(0x4d60), f_q)) +{ + let result := mulmod(mload(0xc20), mload(0x3ae0), f_q) +mstore(19904, result) + } +mstore(0x4de0, mulmod(mload(0x4dc0), mload(0x4160), f_q)) +mstore(0x4e00, mulmod(sub(f_q, mload(0x4de0)), mload(0x42c0), f_q)) +mstore(0x4e20, mulmod(mload(0x4820), mload(0x42c0), f_q)) +mstore(0x4e40, addmod(mload(0x4da0), mload(0x4e00), f_q)) +{ + let result := mulmod(mload(0xc40), mload(0x3ae0), f_q) +mstore(20064, result) + } +mstore(0x4e80, mulmod(mload(0x4e60), mload(0x4160), f_q)) +mstore(0x4ea0, mulmod(sub(f_q, mload(0x4e80)), mload(0x42e0), f_q)) +mstore(0x4ec0, mulmod(mload(0x4820), mload(0x42e0), f_q)) +mstore(0x4ee0, addmod(mload(0x4e40), mload(0x4ea0), f_q)) +{ + let result := mulmod(mload(0xc60), mload(0x3ae0), f_q) +mstore(20224, result) + } +mstore(0x4f20, mulmod(mload(0x4f00), mload(0x4160), f_q)) +mstore(0x4f40, mulmod(sub(f_q, mload(0x4f20)), mload(0x4300), f_q)) +mstore(0x4f60, mulmod(mload(0x4820), mload(0x4300), f_q)) +mstore(0x4f80, addmod(mload(0x4ee0), mload(0x4f40), f_q)) +{ + let result := mulmod(mload(0xc80), mload(0x3ae0), f_q) +mstore(20384, result) + } +mstore(0x4fc0, mulmod(mload(0x4fa0), mload(0x4160), f_q)) +mstore(0x4fe0, mulmod(sub(f_q, mload(0x4fc0)), mload(0x4320), f_q)) +mstore(0x5000, mulmod(mload(0x4820), mload(0x4320), f_q)) +mstore(0x5020, addmod(mload(0x4f80), mload(0x4fe0), f_q)) +{ + let result := mulmod(mload(0xca0), mload(0x3ae0), f_q) +mstore(20544, result) + } +mstore(0x5060, mulmod(mload(0x5040), mload(0x4160), f_q)) +mstore(0x5080, mulmod(sub(f_q, mload(0x5060)), mload(0x4340), f_q)) +mstore(0x50a0, mulmod(mload(0x4820), mload(0x4340), f_q)) +mstore(0x50c0, addmod(mload(0x5020), mload(0x5080), f_q)) +{ + let result := mulmod(mload(0xcc0), mload(0x3ae0), f_q) +mstore(20704, result) + } +mstore(0x5100, mulmod(mload(0x50e0), mload(0x4160), f_q)) +mstore(0x5120, mulmod(sub(f_q, mload(0x5100)), mload(0x4360), f_q)) +mstore(0x5140, mulmod(mload(0x4820), mload(0x4360), f_q)) +mstore(0x5160, addmod(mload(0x50c0), mload(0x5120), f_q)) +{ + let result := mulmod(mload(0xce0), mload(0x3ae0), f_q) +mstore(20864, result) + } +mstore(0x51a0, mulmod(mload(0x5180), mload(0x4160), f_q)) +mstore(0x51c0, mulmod(sub(f_q, mload(0x51a0)), mload(0x4380), f_q)) +mstore(0x51e0, mulmod(mload(0x4820), mload(0x4380), f_q)) +mstore(0x5200, addmod(mload(0x5160), mload(0x51c0), f_q)) +{ + let result := mulmod(mload(0xd00), mload(0x3ae0), f_q) +mstore(21024, result) + } +mstore(0x5240, mulmod(mload(0x5220), mload(0x4160), f_q)) +mstore(0x5260, mulmod(sub(f_q, mload(0x5240)), mload(0x43a0), f_q)) +mstore(0x5280, mulmod(mload(0x4820), mload(0x43a0), f_q)) +mstore(0x52a0, addmod(mload(0x5200), mload(0x5260), f_q)) +mstore(0x52c0, mulmod(mload(0x37c0), mload(0x3f00), f_q)) +mstore(0x52e0, mulmod(mload(0x37e0), mload(0x3f00), f_q)) +{ + let result := mulmod(mload(0x3800), mload(0x3ae0), f_q) +mstore(21248, result) + } +mstore(0x5320, mulmod(mload(0x5300), mload(0x4160), f_q)) +mstore(0x5340, mulmod(sub(f_q, mload(0x5320)), mload(0x43c0), f_q)) +mstore(0x5360, mulmod(mload(0x4820), mload(0x43c0), f_q)) +mstore(0x5380, mulmod(mload(0x52c0), mload(0x43c0), f_q)) +mstore(0x53a0, mulmod(mload(0x52e0), mload(0x43c0), f_q)) +mstore(0x53c0, addmod(mload(0x52a0), mload(0x5340), f_q)) +{ + let result := mulmod(mload(0xc00), mload(0x3ae0), f_q) +mstore(21472, result) + } +mstore(0x5400, mulmod(mload(0x53e0), mload(0x4160), f_q)) +mstore(0x5420, mulmod(sub(f_q, mload(0x5400)), mload(0x43e0), f_q)) +mstore(0x5440, mulmod(mload(0x4820), mload(0x43e0), f_q)) +mstore(0x5460, addmod(mload(0x53c0), mload(0x5420), f_q)) +mstore(0x5480, mulmod(mload(0x5460), mload(0xfa0), f_q)) +mstore(0x54a0, mulmod(mload(0x48a0), mload(0xfa0), f_q)) +mstore(0x54c0, mulmod(mload(0x4920), mload(0xfa0), f_q)) +mstore(0x54e0, mulmod(mload(0x49c0), mload(0xfa0), f_q)) +mstore(0x5500, mulmod(mload(0x4a60), mload(0xfa0), f_q)) +mstore(0x5520, mulmod(mload(0x4b00), mload(0xfa0), f_q)) +mstore(0x5540, mulmod(mload(0x4ba0), mload(0xfa0), f_q)) +mstore(0x5560, mulmod(mload(0x4c40), mload(0xfa0), f_q)) +mstore(0x5580, mulmod(mload(0x4ce0), mload(0xfa0), f_q)) +mstore(0x55a0, mulmod(mload(0x4d80), mload(0xfa0), f_q)) +mstore(0x55c0, mulmod(mload(0x4e20), mload(0xfa0), f_q)) +mstore(0x55e0, mulmod(mload(0x4ec0), mload(0xfa0), f_q)) +mstore(0x5600, mulmod(mload(0x4f60), mload(0xfa0), f_q)) +mstore(0x5620, mulmod(mload(0x5000), mload(0xfa0), f_q)) +mstore(0x5640, mulmod(mload(0x50a0), mload(0xfa0), f_q)) +mstore(0x5660, mulmod(mload(0x5140), mload(0xfa0), f_q)) +mstore(0x5680, mulmod(mload(0x51e0), mload(0xfa0), f_q)) +mstore(0x56a0, mulmod(mload(0x5280), mload(0xfa0), f_q)) +mstore(0x56c0, mulmod(mload(0x5360), mload(0xfa0), f_q)) +mstore(0x56e0, mulmod(mload(0x5380), mload(0xfa0), f_q)) +mstore(0x5700, mulmod(mload(0x53a0), mload(0xfa0), f_q)) +mstore(0x5720, mulmod(mload(0x5440), mload(0xfa0), f_q)) +mstore(0x5740, addmod(mload(0x4780), mload(0x5480), f_q)) +mstore(0x5760, mulmod(1, mload(0x3f40), f_q)) +{ + let result := mulmod(mload(0xd20), mload(0x3b00), f_q) +result := addmod(mulmod(mload(0xd40), mload(0x3b20), f_q), result, f_q) +result := addmod(mulmod(mload(0xd60), mload(0x3b40), f_q), result, f_q) +mstore(22400, result) + } +mstore(0x57a0, mulmod(mload(0x5780), mload(0x4180), f_q)) +mstore(0x57c0, mulmod(sub(f_q, mload(0x57a0)), 1, f_q)) +mstore(0x57e0, mulmod(mload(0x5760), 1, f_q)) +{ + let result := mulmod(mload(0xd80), mload(0x3b00), f_q) +result := addmod(mulmod(mload(0xda0), mload(0x3b20), f_q), result, f_q) +result := addmod(mulmod(mload(0xdc0), mload(0x3b40), f_q), result, f_q) +mstore(22528, result) + } +mstore(0x5820, mulmod(mload(0x5800), mload(0x4180), f_q)) +mstore(0x5840, mulmod(sub(f_q, mload(0x5820)), mload(0xf40), f_q)) +mstore(0x5860, mulmod(mload(0x5760), mload(0xf40), f_q)) +mstore(0x5880, addmod(mload(0x57c0), mload(0x5840), f_q)) +{ + let result := mulmod(mload(0xde0), mload(0x3b00), f_q) +result := addmod(mulmod(mload(0xe00), mload(0x3b20), f_q), result, f_q) +result := addmod(mulmod(mload(0xe20), mload(0x3b40), f_q), result, f_q) +mstore(22688, result) + } +mstore(0x58c0, mulmod(mload(0x58a0), mload(0x4180), f_q)) +mstore(0x58e0, mulmod(sub(f_q, mload(0x58c0)), mload(0x41e0), f_q)) +mstore(0x5900, mulmod(mload(0x5760), mload(0x41e0), f_q)) +mstore(0x5920, addmod(mload(0x5880), mload(0x58e0), f_q)) +mstore(0x5940, mulmod(mload(0x5920), mload(0x4420), f_q)) +mstore(0x5960, mulmod(mload(0x57e0), mload(0x4420), f_q)) +mstore(0x5980, mulmod(mload(0x5860), mload(0x4420), f_q)) +mstore(0x59a0, mulmod(mload(0x5900), mload(0x4420), f_q)) +mstore(0x59c0, addmod(mload(0x5740), mload(0x5940), f_q)) +mstore(0x59e0, mulmod(1, mload(0x3f80), f_q)) +{ + let result := mulmod(mload(0xe40), mload(0x3b80), f_q) +result := addmod(mulmod(mload(0xe60), mload(0x3ba0), f_q), result, f_q) +mstore(23040, result) + } +mstore(0x5a20, mulmod(mload(0x5a00), mload(0x41a0), f_q)) +mstore(0x5a40, mulmod(sub(f_q, mload(0x5a20)), 1, f_q)) +mstore(0x5a60, mulmod(mload(0x59e0), 1, f_q)) +{ + let result := mulmod(mload(0xe80), mload(0x3b80), f_q) +result := addmod(mulmod(mload(0xea0), mload(0x3ba0), f_q), result, f_q) +mstore(23168, result) + } +mstore(0x5aa0, mulmod(mload(0x5a80), mload(0x41a0), f_q)) +mstore(0x5ac0, mulmod(sub(f_q, mload(0x5aa0)), mload(0xf40), f_q)) +mstore(0x5ae0, mulmod(mload(0x59e0), mload(0xf40), f_q)) +mstore(0x5b00, addmod(mload(0x5a40), mload(0x5ac0), f_q)) +mstore(0x5b20, mulmod(mload(0x5b00), mload(0x4440), f_q)) +mstore(0x5b40, mulmod(mload(0x5a60), mload(0x4440), f_q)) +mstore(0x5b60, mulmod(mload(0x5ae0), mload(0x4440), f_q)) +mstore(0x5b80, addmod(mload(0x59c0), mload(0x5b20), f_q)) +mstore(0x5ba0, mulmod(1, mload(0x3fc0), f_q)) +{ + let result := mulmod(mload(0xec0), mload(0x3bc0), f_q) +result := addmod(mulmod(mload(0xee0), mload(0x3be0), f_q), result, f_q) +mstore(23488, result) + } +mstore(0x5be0, mulmod(mload(0x5bc0), mload(0x41c0), f_q)) +mstore(0x5c00, mulmod(sub(f_q, mload(0x5be0)), 1, f_q)) +mstore(0x5c20, mulmod(mload(0x5ba0), 1, f_q)) +mstore(0x5c40, mulmod(mload(0x5c00), mload(0x4460), f_q)) +mstore(0x5c60, mulmod(mload(0x5c20), mload(0x4460), f_q)) +mstore(0x5c80, addmod(mload(0x5b80), mload(0x5c40), f_q)) +mstore(0x5ca0, mulmod(1, mload(0x3ac0), f_q)) +mstore(0x5cc0, mulmod(1, mload(0x1040), f_q)) +mstore(0x5ce0, 0x0000000000000000000000000000000000000000000000000000000000000001) + mstore(0x5d00, 0x0000000000000000000000000000000000000000000000000000000000000002) +mstore(0x5d20, mload(0x5c80)) +success := and(eq(staticcall(gas(), 0x7, 0x5ce0, 0x60, 0x5ce0, 0x40), 1), success) +mstore(0x5d40, mload(0x5ce0)) + mstore(0x5d60, mload(0x5d00)) +mstore(0x5d80, mload(0x260)) + mstore(0x5da0, mload(0x280)) +success := and(eq(staticcall(gas(), 0x6, 0x5d40, 0x80, 0x5d40, 0x40), 1), success) +mstore(0x5dc0, mload(0x2a0)) + mstore(0x5de0, mload(0x2c0)) +mstore(0x5e00, mload(0x47a0)) +success := and(eq(staticcall(gas(), 0x7, 0x5dc0, 0x60, 0x5dc0, 0x40), 1), success) +mstore(0x5e20, mload(0x5d40)) + mstore(0x5e40, mload(0x5d60)) +mstore(0x5e60, mload(0x5dc0)) + mstore(0x5e80, mload(0x5de0)) +success := and(eq(staticcall(gas(), 0x6, 0x5e20, 0x80, 0x5e20, 0x40), 1), success) +mstore(0x5ea0, mload(0x2e0)) + mstore(0x5ec0, mload(0x300)) +mstore(0x5ee0, mload(0x47c0)) +success := and(eq(staticcall(gas(), 0x7, 0x5ea0, 0x60, 0x5ea0, 0x40), 1), success) +mstore(0x5f00, mload(0x5e20)) + mstore(0x5f20, mload(0x5e40)) +mstore(0x5f40, mload(0x5ea0)) + mstore(0x5f60, mload(0x5ec0)) +success := and(eq(staticcall(gas(), 0x6, 0x5f00, 0x80, 0x5f00, 0x40), 1), success) +mstore(0x5f80, mload(0x320)) + mstore(0x5fa0, mload(0x340)) +mstore(0x5fc0, mload(0x47e0)) +success := and(eq(staticcall(gas(), 0x7, 0x5f80, 0x60, 0x5f80, 0x40), 1), success) +mstore(0x5fe0, mload(0x5f00)) + mstore(0x6000, mload(0x5f20)) +mstore(0x6020, mload(0x5f80)) + mstore(0x6040, mload(0x5fa0)) +success := and(eq(staticcall(gas(), 0x6, 0x5fe0, 0x80, 0x5fe0, 0x40), 1), success) +mstore(0x6060, mload(0x360)) + mstore(0x6080, mload(0x380)) +mstore(0x60a0, mload(0x4800)) +success := and(eq(staticcall(gas(), 0x7, 0x6060, 0x60, 0x6060, 0x40), 1), success) +mstore(0x60c0, mload(0x5fe0)) + mstore(0x60e0, mload(0x6000)) +mstore(0x6100, mload(0x6060)) + mstore(0x6120, mload(0x6080)) +success := and(eq(staticcall(gas(), 0x6, 0x60c0, 0x80, 0x60c0, 0x40), 1), success) +mstore(0x6140, mload(0x3a0)) + mstore(0x6160, mload(0x3c0)) +mstore(0x6180, mload(0x54a0)) +success := and(eq(staticcall(gas(), 0x7, 0x6140, 0x60, 0x6140, 0x40), 1), success) +mstore(0x61a0, mload(0x60c0)) + mstore(0x61c0, mload(0x60e0)) +mstore(0x61e0, mload(0x6140)) + mstore(0x6200, mload(0x6160)) +success := and(eq(staticcall(gas(), 0x6, 0x61a0, 0x80, 0x61a0, 0x40), 1), success) +mstore(0x6220, mload(0x480)) + mstore(0x6240, mload(0x4a0)) +mstore(0x6260, mload(0x54c0)) +success := and(eq(staticcall(gas(), 0x7, 0x6220, 0x60, 0x6220, 0x40), 1), success) +mstore(0x6280, mload(0x61a0)) + mstore(0x62a0, mload(0x61c0)) +mstore(0x62c0, mload(0x6220)) + mstore(0x62e0, mload(0x6240)) +success := and(eq(staticcall(gas(), 0x6, 0x6280, 0x80, 0x6280, 0x40), 1), success) +mstore(0x6300, 0x1f224f5998f14bc19a1fc8ab9271b179aa55c5c2188c466029c96038ef96993e) + mstore(0x6320, 0x051ac90c8a88cbae8df335e747936e9567931d8b470dec40c0fc3b4d510e4973) +mstore(0x6340, mload(0x54e0)) +success := and(eq(staticcall(gas(), 0x7, 0x6300, 0x60, 0x6300, 0x40), 1), success) +mstore(0x6360, mload(0x6280)) + mstore(0x6380, mload(0x62a0)) +mstore(0x63a0, mload(0x6300)) + mstore(0x63c0, mload(0x6320)) +success := and(eq(staticcall(gas(), 0x6, 0x6360, 0x80, 0x6360, 0x40), 1), success) +mstore(0x63e0, 0x04528ec7365a2881b7d3c8925570e06bb3b17f04f6a95384ac8ed19a30c12097) + mstore(0x6400, 0x28d1ef470a8a5278ad6d2eb9047ad7e93024113f543b06870f1bbea7177db404) +mstore(0x6420, mload(0x5500)) +success := and(eq(staticcall(gas(), 0x7, 0x63e0, 0x60, 0x63e0, 0x40), 1), success) +mstore(0x6440, mload(0x6360)) + mstore(0x6460, mload(0x6380)) +mstore(0x6480, mload(0x63e0)) + mstore(0x64a0, mload(0x6400)) +success := and(eq(staticcall(gas(), 0x6, 0x6440, 0x80, 0x6440, 0x40), 1), success) +mstore(0x64c0, 0x148da7a45ae1351d24cb90fac1678751b258a5aff7b437f9183860716b066d1e) + mstore(0x64e0, 0x083fe9b9175e0b464d258a01327e37688eea7a94859457f95cfc50edf15e7d37) +mstore(0x6500, mload(0x5520)) +success := and(eq(staticcall(gas(), 0x7, 0x64c0, 0x60, 0x64c0, 0x40), 1), success) +mstore(0x6520, mload(0x6440)) + mstore(0x6540, mload(0x6460)) +mstore(0x6560, mload(0x64c0)) + mstore(0x6580, mload(0x64e0)) +success := and(eq(staticcall(gas(), 0x6, 0x6520, 0x80, 0x6520, 0x40), 1), success) +mstore(0x65a0, 0x2c89ef76f1ff53371da882bd1f56419409140a67f6e43f9cb6b8e3dc290f3d32) + mstore(0x65c0, 0x092b7306ff29f079a954b0e67cd377246de2fda9e67852ac4ac9b239380844d7) +mstore(0x65e0, mload(0x5540)) +success := and(eq(staticcall(gas(), 0x7, 0x65a0, 0x60, 0x65a0, 0x40), 1), success) +mstore(0x6600, mload(0x6520)) + mstore(0x6620, mload(0x6540)) +mstore(0x6640, mload(0x65a0)) + mstore(0x6660, mload(0x65c0)) +success := and(eq(staticcall(gas(), 0x6, 0x6600, 0x80, 0x6600, 0x40), 1), success) +mstore(0x6680, 0x264ba2649108dc04cf91bfe5ce7f9e39718ef3489c3a63af4434ef95d78947d3) + mstore(0x66a0, 0x1f7b8d6de9ff44a2b5ce79cf4d428ae53d24106c5a8b496018d4527c988ccaa8) +mstore(0x66c0, mload(0x5560)) +success := and(eq(staticcall(gas(), 0x7, 0x6680, 0x60, 0x6680, 0x40), 1), success) +mstore(0x66e0, mload(0x6600)) + mstore(0x6700, mload(0x6620)) +mstore(0x6720, mload(0x6680)) + mstore(0x6740, mload(0x66a0)) +success := and(eq(staticcall(gas(), 0x6, 0x66e0, 0x80, 0x66e0, 0x40), 1), success) +mstore(0x6760, 0x01c33b5a6e9a4bd6d333d8cebc27b12269a07577f863423cb21af26f8882fb1b) + mstore(0x6780, 0x13ec8460a262474074812c8d0013ae3348c30cd29d9f2bf0b7544af011c7e86c) +mstore(0x67a0, mload(0x5580)) +success := and(eq(staticcall(gas(), 0x7, 0x6760, 0x60, 0x6760, 0x40), 1), success) +mstore(0x67c0, mload(0x66e0)) + mstore(0x67e0, mload(0x6700)) +mstore(0x6800, mload(0x6760)) + mstore(0x6820, mload(0x6780)) +success := and(eq(staticcall(gas(), 0x6, 0x67c0, 0x80, 0x67c0, 0x40), 1), success) +mstore(0x6840, 0x138ca2ca05c4c86af016dce6765689ad133c465f3ae958265217d6f3bf956096) + mstore(0x6860, 0x27df80ac3bfaa603862f1baee6eb9f3da5e7995e5def2f3f836802ff4abbcfc7) +mstore(0x6880, mload(0x55a0)) +success := and(eq(staticcall(gas(), 0x7, 0x6840, 0x60, 0x6840, 0x40), 1), success) +mstore(0x68a0, mload(0x67c0)) + mstore(0x68c0, mload(0x67e0)) +mstore(0x68e0, mload(0x6840)) + mstore(0x6900, mload(0x6860)) +success := and(eq(staticcall(gas(), 0x6, 0x68a0, 0x80, 0x68a0, 0x40), 1), success) +mstore(0x6920, 0x13e0e546075988f7a28874395078b73892d877975ff57cf00aa6316aa5abd52d) + mstore(0x6940, 0x10c6b7871774e2609ecc62b7cf0a80fa3594d43705cc239619b3ea3bcfd30829) +mstore(0x6960, mload(0x55c0)) +success := and(eq(staticcall(gas(), 0x7, 0x6920, 0x60, 0x6920, 0x40), 1), success) +mstore(0x6980, mload(0x68a0)) + mstore(0x69a0, mload(0x68c0)) +mstore(0x69c0, mload(0x6920)) + mstore(0x69e0, mload(0x6940)) +success := and(eq(staticcall(gas(), 0x6, 0x6980, 0x80, 0x6980, 0x40), 1), success) +mstore(0x6a00, 0x246e7ca9b83c0f5457b0733911aef2997251e5af5f1144016162f307f8a69596) + mstore(0x6a20, 0x02000973e02faa177b17b846e7b1a2ba349c22c20badb7da24299a175d5b43ec) +mstore(0x6a40, mload(0x55e0)) +success := and(eq(staticcall(gas(), 0x7, 0x6a00, 0x60, 0x6a00, 0x40), 1), success) +mstore(0x6a60, mload(0x6980)) + mstore(0x6a80, mload(0x69a0)) +mstore(0x6aa0, mload(0x6a00)) + mstore(0x6ac0, mload(0x6a20)) +success := and(eq(staticcall(gas(), 0x6, 0x6a60, 0x80, 0x6a60, 0x40), 1), success) +mstore(0x6ae0, 0x290ffddf7e57e7249144c7e2a528da0ae6bc012eadcfcf530f101416a002b3f1) + mstore(0x6b00, 0x121773be2cc80e3dbfb20e8d8eb60617a5d2a4bbd5cbef21593ff1da7e7695fd) +mstore(0x6b20, mload(0x5600)) +success := and(eq(staticcall(gas(), 0x7, 0x6ae0, 0x60, 0x6ae0, 0x40), 1), success) +mstore(0x6b40, mload(0x6a60)) + mstore(0x6b60, mload(0x6a80)) +mstore(0x6b80, mload(0x6ae0)) + mstore(0x6ba0, mload(0x6b00)) +success := and(eq(staticcall(gas(), 0x6, 0x6b40, 0x80, 0x6b40, 0x40), 1), success) +mstore(0x6bc0, 0x2ece498dabef6ec6074c536e36548f87384a81b09b17745e1848e3fe41046ed8) + mstore(0x6be0, 0x25742093fb504498c37f80b511cde8e51398711b35ffc1a56b1bf2f155ea1f54) +mstore(0x6c00, mload(0x5620)) +success := and(eq(staticcall(gas(), 0x7, 0x6bc0, 0x60, 0x6bc0, 0x40), 1), success) +mstore(0x6c20, mload(0x6b40)) + mstore(0x6c40, mload(0x6b60)) +mstore(0x6c60, mload(0x6bc0)) + mstore(0x6c80, mload(0x6be0)) +success := and(eq(staticcall(gas(), 0x6, 0x6c20, 0x80, 0x6c20, 0x40), 1), success) +mstore(0x6ca0, 0x1eaf3b78ae89964c57cbb6ffd74ae258c84106fcfacdf0b34cfb7f0517c83d34) + mstore(0x6cc0, 0x03fc9b6fb73e4108bb2725c51e3243cb49d65e30692b1104511477c67a64ff3e) +mstore(0x6ce0, mload(0x5640)) +success := and(eq(staticcall(gas(), 0x7, 0x6ca0, 0x60, 0x6ca0, 0x40), 1), success) +mstore(0x6d00, mload(0x6c20)) + mstore(0x6d20, mload(0x6c40)) +mstore(0x6d40, mload(0x6ca0)) + mstore(0x6d60, mload(0x6cc0)) +success := and(eq(staticcall(gas(), 0x6, 0x6d00, 0x80, 0x6d00, 0x40), 1), success) +mstore(0x6d80, 0x211c7ebb676fb55bf5bf70d25c8099113588fa46f9487b31c80fdd19c99c56b6) + mstore(0x6da0, 0x28c5339caf705e65ce8437725dafa12804573290575791b19eceea6aef10c8a8) +mstore(0x6dc0, mload(0x5660)) +success := and(eq(staticcall(gas(), 0x7, 0x6d80, 0x60, 0x6d80, 0x40), 1), success) +mstore(0x6de0, mload(0x6d00)) + mstore(0x6e00, mload(0x6d20)) +mstore(0x6e20, mload(0x6d80)) + mstore(0x6e40, mload(0x6da0)) +success := and(eq(staticcall(gas(), 0x6, 0x6de0, 0x80, 0x6de0, 0x40), 1), success) +mstore(0x6e60, 0x0ece099bd367af0715b4d47e7bc614bfe9a9f45c51f9969d4b104f4dcb0c279f) + mstore(0x6e80, 0x0c7702bd2b7cafe83d79c152ef77a1661a50c4331f3458377a383a8ac68bb8ae) +mstore(0x6ea0, mload(0x5680)) +success := and(eq(staticcall(gas(), 0x7, 0x6e60, 0x60, 0x6e60, 0x40), 1), success) +mstore(0x6ec0, mload(0x6de0)) + mstore(0x6ee0, mload(0x6e00)) +mstore(0x6f00, mload(0x6e60)) + mstore(0x6f20, mload(0x6e80)) +success := and(eq(staticcall(gas(), 0x6, 0x6ec0, 0x80, 0x6ec0, 0x40), 1), success) +mstore(0x6f40, 0x131587b948c9547bfb0ece7b4031b60b72c7a08bd30b600d57b831f772955941) + mstore(0x6f60, 0x0967910d6f98d32bfa6d7debd908f5b9a3e084269b7116d76d4a0b7bf4fa8758) +mstore(0x6f80, mload(0x56a0)) +success := and(eq(staticcall(gas(), 0x7, 0x6f40, 0x60, 0x6f40, 0x40), 1), success) +mstore(0x6fa0, mload(0x6ec0)) + mstore(0x6fc0, mload(0x6ee0)) +mstore(0x6fe0, mload(0x6f40)) + mstore(0x7000, mload(0x6f60)) +success := and(eq(staticcall(gas(), 0x6, 0x6fa0, 0x80, 0x6fa0, 0x40), 1), success) +mstore(0x7020, mload(0x760)) + mstore(0x7040, mload(0x780)) +mstore(0x7060, mload(0x56c0)) +success := and(eq(staticcall(gas(), 0x7, 0x7020, 0x60, 0x7020, 0x40), 1), success) +mstore(0x7080, mload(0x6fa0)) + mstore(0x70a0, mload(0x6fc0)) +mstore(0x70c0, mload(0x7020)) + mstore(0x70e0, mload(0x7040)) +success := and(eq(staticcall(gas(), 0x6, 0x7080, 0x80, 0x7080, 0x40), 1), success) +mstore(0x7100, mload(0x7a0)) + mstore(0x7120, mload(0x7c0)) +mstore(0x7140, mload(0x56e0)) +success := and(eq(staticcall(gas(), 0x7, 0x7100, 0x60, 0x7100, 0x40), 1), success) +mstore(0x7160, mload(0x7080)) + mstore(0x7180, mload(0x70a0)) +mstore(0x71a0, mload(0x7100)) + mstore(0x71c0, mload(0x7120)) +success := and(eq(staticcall(gas(), 0x6, 0x7160, 0x80, 0x7160, 0x40), 1), success) +mstore(0x71e0, mload(0x7e0)) + mstore(0x7200, mload(0x800)) +mstore(0x7220, mload(0x5700)) +success := and(eq(staticcall(gas(), 0x7, 0x71e0, 0x60, 0x71e0, 0x40), 1), success) +mstore(0x7240, mload(0x7160)) + mstore(0x7260, mload(0x7180)) +mstore(0x7280, mload(0x71e0)) + mstore(0x72a0, mload(0x7200)) +success := and(eq(staticcall(gas(), 0x6, 0x7240, 0x80, 0x7240, 0x40), 1), success) +mstore(0x72c0, mload(0x6c0)) + mstore(0x72e0, mload(0x6e0)) +mstore(0x7300, mload(0x5720)) +success := and(eq(staticcall(gas(), 0x7, 0x72c0, 0x60, 0x72c0, 0x40), 1), success) +mstore(0x7320, mload(0x7240)) + mstore(0x7340, mload(0x7260)) +mstore(0x7360, mload(0x72c0)) + mstore(0x7380, mload(0x72e0)) +success := and(eq(staticcall(gas(), 0x6, 0x7320, 0x80, 0x7320, 0x40), 1), success) +mstore(0x73a0, mload(0x580)) + mstore(0x73c0, mload(0x5a0)) +mstore(0x73e0, mload(0x5960)) +success := and(eq(staticcall(gas(), 0x7, 0x73a0, 0x60, 0x73a0, 0x40), 1), success) +mstore(0x7400, mload(0x7320)) + mstore(0x7420, mload(0x7340)) +mstore(0x7440, mload(0x73a0)) + mstore(0x7460, mload(0x73c0)) +success := and(eq(staticcall(gas(), 0x6, 0x7400, 0x80, 0x7400, 0x40), 1), success) +mstore(0x7480, mload(0x5c0)) + mstore(0x74a0, mload(0x5e0)) +mstore(0x74c0, mload(0x5980)) +success := and(eq(staticcall(gas(), 0x7, 0x7480, 0x60, 0x7480, 0x40), 1), success) +mstore(0x74e0, mload(0x7400)) + mstore(0x7500, mload(0x7420)) +mstore(0x7520, mload(0x7480)) + mstore(0x7540, mload(0x74a0)) +success := and(eq(staticcall(gas(), 0x6, 0x74e0, 0x80, 0x74e0, 0x40), 1), success) +mstore(0x7560, mload(0x600)) + mstore(0x7580, mload(0x620)) +mstore(0x75a0, mload(0x59a0)) +success := and(eq(staticcall(gas(), 0x7, 0x7560, 0x60, 0x7560, 0x40), 1), success) +mstore(0x75c0, mload(0x74e0)) + mstore(0x75e0, mload(0x7500)) +mstore(0x7600, mload(0x7560)) + mstore(0x7620, mload(0x7580)) +success := and(eq(staticcall(gas(), 0x6, 0x75c0, 0x80, 0x75c0, 0x40), 1), success) +mstore(0x7640, mload(0x640)) + mstore(0x7660, mload(0x660)) +mstore(0x7680, mload(0x5b40)) +success := and(eq(staticcall(gas(), 0x7, 0x7640, 0x60, 0x7640, 0x40), 1), success) +mstore(0x76a0, mload(0x75c0)) + mstore(0x76c0, mload(0x75e0)) +mstore(0x76e0, mload(0x7640)) + mstore(0x7700, mload(0x7660)) +success := and(eq(staticcall(gas(), 0x6, 0x76a0, 0x80, 0x76a0, 0x40), 1), success) +mstore(0x7720, mload(0x680)) + mstore(0x7740, mload(0x6a0)) +mstore(0x7760, mload(0x5b60)) +success := and(eq(staticcall(gas(), 0x7, 0x7720, 0x60, 0x7720, 0x40), 1), success) +mstore(0x7780, mload(0x76a0)) + mstore(0x77a0, mload(0x76c0)) +mstore(0x77c0, mload(0x7720)) + mstore(0x77e0, mload(0x7740)) +success := and(eq(staticcall(gas(), 0x6, 0x7780, 0x80, 0x7780, 0x40), 1), success) +mstore(0x7800, mload(0x440)) + mstore(0x7820, mload(0x460)) +mstore(0x7840, mload(0x5c60)) +success := and(eq(staticcall(gas(), 0x7, 0x7800, 0x60, 0x7800, 0x40), 1), success) +mstore(0x7860, mload(0x7780)) + mstore(0x7880, mload(0x77a0)) +mstore(0x78a0, mload(0x7800)) + mstore(0x78c0, mload(0x7820)) +success := and(eq(staticcall(gas(), 0x6, 0x7860, 0x80, 0x7860, 0x40), 1), success) +mstore(0x78e0, mload(0xfe0)) + mstore(0x7900, mload(0x1000)) +mstore(0x7920, sub(f_q, mload(0x5ca0))) +success := and(eq(staticcall(gas(), 0x7, 0x78e0, 0x60, 0x78e0, 0x40), 1), success) +mstore(0x7940, mload(0x7860)) + mstore(0x7960, mload(0x7880)) +mstore(0x7980, mload(0x78e0)) + mstore(0x79a0, mload(0x7900)) +success := and(eq(staticcall(gas(), 0x6, 0x7940, 0x80, 0x7940, 0x40), 1), success) +mstore(0x79c0, mload(0x1080)) + mstore(0x79e0, mload(0x10a0)) +mstore(0x7a00, mload(0x5cc0)) +success := and(eq(staticcall(gas(), 0x7, 0x79c0, 0x60, 0x79c0, 0x40), 1), success) +mstore(0x7a20, mload(0x7940)) + mstore(0x7a40, mload(0x7960)) +mstore(0x7a60, mload(0x79c0)) + mstore(0x7a80, mload(0x79e0)) +success := and(eq(staticcall(gas(), 0x6, 0x7a20, 0x80, 0x7a20, 0x40), 1), success) +mstore(0x7aa0, mload(0x7a20)) + mstore(0x7ac0, mload(0x7a40)) +mstore(0x7ae0, mload(0x1080)) + mstore(0x7b00, mload(0x10a0)) +mstore(0x7b20, mload(0x10c0)) + mstore(0x7b40, mload(0x10e0)) +mstore(0x7b60, mload(0x1100)) + mstore(0x7b80, mload(0x1120)) +mstore(0x7ba0, keccak256(0x7aa0, 256)) +mstore(31680, mod(mload(31648), f_q)) +mstore(0x7be0, mulmod(mload(0x7bc0), mload(0x7bc0), f_q)) +mstore(0x7c00, mulmod(1, mload(0x7bc0), f_q)) +mstore(0x7c20, mload(0x7b20)) + mstore(0x7c40, mload(0x7b40)) +mstore(0x7c60, mload(0x7c00)) +success := and(eq(staticcall(gas(), 0x7, 0x7c20, 0x60, 0x7c20, 0x40), 1), success) +mstore(0x7c80, mload(0x7aa0)) + mstore(0x7ca0, mload(0x7ac0)) +mstore(0x7cc0, mload(0x7c20)) + mstore(0x7ce0, mload(0x7c40)) +success := and(eq(staticcall(gas(), 0x6, 0x7c80, 0x80, 0x7c80, 0x40), 1), success) +mstore(0x7d00, mload(0x7b60)) + mstore(0x7d20, mload(0x7b80)) +mstore(0x7d40, mload(0x7c00)) +success := and(eq(staticcall(gas(), 0x7, 0x7d00, 0x60, 0x7d00, 0x40), 1), success) +mstore(0x7d60, mload(0x7ae0)) + mstore(0x7d80, mload(0x7b00)) +mstore(0x7da0, mload(0x7d00)) + mstore(0x7dc0, mload(0x7d20)) +success := and(eq(staticcall(gas(), 0x6, 0x7d60, 0x80, 0x7d60, 0x40), 1), success) +mstore(0x7de0, mload(0x7c80)) + mstore(0x7e00, mload(0x7ca0)) +mstore(0x7e20, 0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2) + mstore(0x7e40, 0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed) + mstore(0x7e60, 0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b) + mstore(0x7e80, 0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa) +mstore(0x7ea0, mload(0x7d60)) + mstore(0x7ec0, mload(0x7d80)) +mstore(0x7ee0, 0x0181624e80f3d6ae28df7e01eaeab1c0e919877a3b8a6b7fbc69a6817d596ea2) + mstore(0x7f00, 0x1783d30dcb12d259bb89098addf6280fa4b653be7a152542a28f7b926e27e648) + mstore(0x7f20, 0x00ae44489d41a0d179e2dfdc03bddd883b7109f8b6ae316a59e815c1a6b35304) + mstore(0x7f40, 0x0b2147ab62a386bd63e6de1522109b8c9588ab466f5aadfde8c41ca3749423ee) +success := and(eq(staticcall(gas(), 0x8, 0x7de0, 0x180, 0x7de0, 0x20), 1), success) +success := and(eq(mload(0x7de0), 1), success) + + if not(success) { revert(0, 0) } + return(0, 0) + + } + } + } \ No newline at end of file diff --git a/axiom-eth/data/tx_receipts/task.goerli.json b/axiom-eth/data/tx_receipts/task.goerli.json new file mode 100644 index 00000000..5afac6fa --- /dev/null +++ b/axiom-eth/data/tx_receipts/task.goerli.json @@ -0,0 +1,128 @@ +{ + "txQueries": [ + { + "txHash": "0x9ba6df3200fe8d62103ede64a32a2475e4c7f992fe5e8ea7f08a490edf32d48d", + "fieldIdx": 3 + }, + { + "txHash": "0x684dcb11fdc89ee4568b03dc9c165e585c552a3361f6c5813418bc625cb76932", + "fieldIdx": 7 + } + ], + "receiptQueries": [ + { + "txHash": "0x1320047d683efdf132b7939418a9f0061f9dfd7348a2f347f32745fb46cb6ec6", + "fieldIdx": 0, + "logIdx": 0 + }, + { + "txHash": "0x1320047d683efdf132b7939418a9f0061f9dfd7348a2f347f32745fb46cb6ec6", + "fieldIdx": 3, + "logIdx": 0 + } + ], + "mmr": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x9001a1b1b89def70442c874a9056d7bdfd7f241547aa6fe0d11d001770654894", + "0x2caa414cfbca54154d3a8815f9e1a6d410f4e1d0f76df14f9eecebb0ea05b59d", + "0x33d65445a72205b92572a182e3867c9c66fca3a086d02dda622122df063b38c8", + "0xdde7f249a13c0fef92a9aa7abd559586732c0398241813253fcf07d956859bce", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xf6bcd969602fef0c36ec84ee377e592ec3b5311534ab3c1efcf0646acd96eead" + ], + "txMmrProofs": [ + [ + "0xe3b171a6a94d9a3eb8705be2027dd1b7f36155af525d9f1c42e4fc3aeb6415a5", + "0xfd9d4ed5748eb546fcec653502f773e2571ba7aa34ee7e62ff8c475d69bff6ec", + "0x24f4542a34fe0deef6bfe0242023d54436de7e608ba96fdf3dcc8fda20b91f6b", + "0xa9e0c8a16b1a4bff904c80cbb51997564d88fda9cd22da5794a6eec2d16e9bae", + "0x125a3020dedca37a6c42c96c8163390e7320a6dfbcea705b6a8e049f7d64f4ef", + "0x19f311181477e59a986732cc488ee0f52e11bb239831f767172c69badf0f4b0e", + "0x2ca98b1cdde7a16482691c899177f93f908caf04950aef620155b5ce5e26d543", + "0xaad44d06475f108d50b42ff6295acc614063171ad482c005e773367870cc3710", + "0x98ea01437632be717454c461d9510e868a00801d32ca236b1c96fba0b10c58a9", + "0x34626e1dcd0630fc5d9e62df2be2c29c6174e2931d96745718237efdbbb153bf", + "0x6ed4b0b5107fc60f8f45d887a82762585ca552ab893720a4e0e1c47c2d199b54", + "0x88faddf76a438510c102c9b2da1d5a3e0e9a69b5f1b1a9d23c6c94f26471d0a1", + "0xe900295c9ec37bdbeb3abda8bd371a434299bc3edf044020eccb5ef761ff5bc1", + "0xd523c754baccd7d9f7d9a35d43157b2bb1f93ca76815a517e8a91cc888b2701c", + "0x67ed5a40bef84013461f6d1bba4a5327d9e3fdc3a8bb037b620d6bde2a14c013", + "0x664cc20d84db67636e15b015613aa033984035060bb132d1d27bf30eb0354f68", + "0x6cc5504083b2c00e4d8fa153cfc095ab1ea72156678c61fa19658becbd2b622e", + "0xb1749f505305caf01d0096ea232f9d1f9a4fbaa9bdf10b8b50c31ff802ef757d" + ], + [ + "0xbd12800a021e3f70cefe7ac301cbb0c61a7076f13fcd735e3acebabba877b9f2", + "0x91f271ed7f8994119bbe33ad0faf89d9f51e082a4b4202da6f5c3b94ee641874", + "0x0009a588763e0c0fad4f5cfa630c860dec550f71e60e6d95103697b0b4af3713", + "0x6a925ce5509b4f31367775e8cfa2fcd678bcd6f2f6c17228758c38f6f930b894", + "0x4e6fc1a7b0a16583b601eac12869b5cf04095bec54af60b57a8539ba7c997e6d", + "0x0be1bdfd1519c802082e1f432ebcff9de6c6d019981ca53907eb66a7fabbd893", + "0x8a7e2dbe3865a5d51cf0b9b01f55233df9887f8769c7093e48b638912d14099a", + "0xa87b80fda7e258142e56541b3ac39155a7b2b8ebc50b37bb4fb6af0bbbe99b1e", + "0x6f46cbb147939955a6902d02026a2c87b51bb5a5b428060ebed2285a65a1ffde", + "0x9c07be5f69fd6a38195fe1c95c53b90e7ac844b3a08e86cea98181253b2a7566", + "0x0c05fe92c25adbb248c3315c9607dc4c0a5effa547fe8c5b25b59a34892ff53e", + "0x6bdf9c5b572dea2a8b2754adefe9e00d42b59aa970156ec0997eaf91b17f5a7c", + "0xb424aa1d27b48cee75c37d2cd13749f6095c7c86435080e8cfdeca97e13383ce", + "0xec3c6c5338ca0bddd0c7925aef2835b9ffb2ac4ac2b0d5f5ce38f59f06d387c7", + "0xa01781a978c22545f26063dd0a8f0a7c052b19e1f3706169c1b71d627a7ea13c", + "0xf8d8f6319c2dd17fb421463eb8d0c86881d0afaf8e08e95c49b5a9068727cae7" + ] + ], + "receiptMmrProofs": [ + [ + "0xf7c9fdba1d27d4d66b2085ef041e97c5b9bddf0262f03cb75fff4ae5342da7fc", + "0xf01d543b87c11402616226836455ed3b03a00b9c3feebb478eb9e5a97be07392", + "0x4bd8b09a6fd10f09f920c3aa6aab942d63d55fdd1f093301b5fcb8f7fd59ce8a", + "0x79423f92140f3e6698b8d38394170fcd19c452282674c30835edf7c06dff49ef", + "0x4c39bed02253ac79744c1ff0134c82eecda62341048e9edc7f48caa0b06d2d1e", + "0x28805385c643dfc4f15251f848d32fe536cccc605b6f36312f8f1210c88bc4e5", + "0x3b14201f7ea94e76ec305487b1c5e3ec56715eafbe57a5ea3253b75f3625a772", + "0x22ac80faa27cba53fc2ca042902eb8c55d0b3599294ee004186a962ca8d9cc6f", + "0x73bc39a9b31be50ecdf05fa2b8e1826a72c59abffdbe010f135e0a4089440603", + "0xbb5e3fb8c83f0237baa615239a5372c98c9efd2c3a41d289b6ce134ee7a3bc50", + "0x4be9154bca6cb87ee928b0cc41bf29b1043ba1265fde3b2dad297d4d2b198f29", + "0x464ec85779dcb2a11b64e28a6aada9563b9a24803cf0250bf073768ed75e1c5f", + "0xe76931d3a2a46f459cf169a0e0e6d44af220c98835a9fd406e82ef9b167fffa1", + "0x7faeb24db369709c70ae2c41259d2216bc320e9dc29ee0d7e358b18a375ca113", + "0xdf96d84651c4ed2a01bad7d2ff2a465bf334587adcfdeec26ebc5104bff2e4d5", + "0xf8d8f6319c2dd17fb421463eb8d0c86881d0afaf8e08e95c49b5a9068727cae7" + ], + [ + "0xf7c9fdba1d27d4d66b2085ef041e97c5b9bddf0262f03cb75fff4ae5342da7fc", + "0xf01d543b87c11402616226836455ed3b03a00b9c3feebb478eb9e5a97be07392", + "0x4bd8b09a6fd10f09f920c3aa6aab942d63d55fdd1f093301b5fcb8f7fd59ce8a", + "0x79423f92140f3e6698b8d38394170fcd19c452282674c30835edf7c06dff49ef", + "0x4c39bed02253ac79744c1ff0134c82eecda62341048e9edc7f48caa0b06d2d1e", + "0x28805385c643dfc4f15251f848d32fe536cccc605b6f36312f8f1210c88bc4e5", + "0x3b14201f7ea94e76ec305487b1c5e3ec56715eafbe57a5ea3253b75f3625a772", + "0x22ac80faa27cba53fc2ca042902eb8c55d0b3599294ee004186a962ca8d9cc6f", + "0x73bc39a9b31be50ecdf05fa2b8e1826a72c59abffdbe010f135e0a4089440603", + "0xbb5e3fb8c83f0237baa615239a5372c98c9efd2c3a41d289b6ce134ee7a3bc50", + "0x4be9154bca6cb87ee928b0cc41bf29b1043ba1265fde3b2dad297d4d2b198f29", + "0x464ec85779dcb2a11b64e28a6aada9563b9a24803cf0250bf073768ed75e1c5f", + "0xe76931d3a2a46f459cf169a0e0e6d44af220c98835a9fd406e82ef9b167fffa1", + "0x7faeb24db369709c70ae2c41259d2216bc320e9dc29ee0d7e358b18a375ca113", + "0xdf96d84651c4ed2a01bad7d2ff2a465bf334587adcfdeec26ebc5104bff2e4d5", + "0xf8d8f6319c2dd17fb421463eb8d0c86881d0afaf8e08e95c49b5a9068727cae7" + ] + ] +} diff --git a/axiom-eth/data/tx_receipts/task.mainnet.json b/axiom-eth/data/tx_receipts/task.mainnet.json new file mode 100644 index 00000000..3c64456c --- /dev/null +++ b/axiom-eth/data/tx_receipts/task.mainnet.json @@ -0,0 +1,61 @@ +{ + "txQueries": [ + { + "txHash": "0x66c7b4d2bb7cc086f3eeb3b46bca9c0bb825826bbfc43ebc45f1c9eb7e7344d9", + "fieldIdx": 4 + } + ], + "receiptQueries": [], + "mmr": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xe81cc62bb288e100856ea7d40af72b844e9dcb9ff8ebed659a475e2635cd4e18", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xb169c87af2d231bc71f910481d6d8315a6fc4edfab212ee003d206b9643339c0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x43062e083f5f40510f30723d9cebb51c4ae67c35d86c5b791a043bae317350e3", + "0x6cddc980f4c3b403d99080c32b3f0a6205e39560b9021d5f233c04d96c23381e", + "0x6a42052cabd8d66a584b8823c6aadf64dd2755321210c66a2b7acd1da5bdeacf", + "0xebf08ca711cbab09109bb51277c545ee43073d8fa8b46c0cbbedd46ce85e73be", + "0x477c055e69de14e3bbfe2af0389e6c3ac28ffb5b0cc8fa26b543ac47857fd646", + "0xf47e6584af17667881494720b50dd063d0900b288438df7a37e2e91440aedd23" + ], + "txMmrProofs": [ + [ + "0xebd5fc0be32c2298e2ee18dac2db5b1508a64741ba7186dd59229ec0620d9d64", + "0x9139f12e0f47241172143fc5d0b0245b5ffbcdf9130da9efb14c155f6036697e", + "0x97f5d40bc9a10e06b5eef7609a3e364f30ab10675d22fbc3304179a381b39b18", + "0xc8c07e6c877f0cd60903d376c1aa911f47c96d3967d989101ed8b873cf6e38de", + "0x96cf53edbe3be783378433c518939a7e0b4e657edb6558b1f6e14edc0a125a18", + "0xfa3448a664e9406ffdc3b53e24f06fcf6b576621f854e421385bd1603ea257ee", + "0x9dffc8cb737d72da73df5e333bb7716cfec51e4b761281c6c7ff4db55689911c", + "0xef3fb7b7274810ec5bc63e7c248ea7dfe26d95abcd8bcb8d97b1f5fb617b8dc8", + "0x6a4d92e38592f280afc95efe5dd178a37c155bfad49759db7a066d597bc804d3", + "0x7db79de6d79e2ff264f4d171243f5038b575b380d31b052dda979e28fae7fc08", + "0x3106ece6d5a3c317f17c9313e7d0a3cd73649662301f50fdcedc67254b3fe153", + "0x902c8cf11e8d5cf14137e632061a52574515a2254fbd3b70cfc85a45f9dbcb4a", + "0xc48c7fe69133ac6f0c2200e600a3c15fe1832577156bc8851a7538403eafadfa", + "0x4434e3730dbe222cb8b98703748da1f07f05564c64ea66fe4765484ea982f5d6", + "0x69d2bc461de5dba21f741bf757d60ec8a92c3f29e417cb99fa76459bc3e86278", + "0xe18396e487f6c0bcd73a2d4c4c8c3583be7edefe59f20b2ce67c7f363b8a856a", + "0xa10b0dd9e041c793d0dbdf615bee9e18c3f6e3b191469bbb8cc9912d5d228050", + "0xa51d50eb9feaaf85b7ddacb99f71886135f1c4f59de3e788a5e29a485d5fdce5", + "0xa46b70512bfe0b85498e28ae8187cfadff9e58680b84ddcde450cd880ea489b1", + "0x33552dfc75e340bca3c698e4fb486ae540d07cf2a845465575cff24d866a161a", + "0x0fec590ac8394abe8477b828bf31b470d95772b3f331ff5be34ba0a899975a17" + ] + ], + "receiptMmrProofs": [] +} diff --git a/axiom-eth/proptest-regressions/solidity/tests.txt b/axiom-eth/proptest-regressions/solidity/tests.txt new file mode 100644 index 00000000..db6bdc1a --- /dev/null +++ b/axiom-eth/proptest-regressions/solidity/tests.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc a5b59729ad75fa91e62214a81d6430ccdda1e75a56338267c43632989cbf7b64 # shrinks to input = MappingTest { key: [], mapping_slot: [], ground_truth_slot: [41, 13, 236, 217, 84, 139, 98, 168, 214, 3, 69, 169, 136, 56, 111, 200, 75, 166, 188, 149, 72, 64, 8, 246, 54, 47, 147, 22, 14, 243, 229, 99] } diff --git a/axiom-eth/rust-toolchain b/axiom-eth/rust-toolchain deleted file mode 100644 index 51ab4759..00000000 --- a/axiom-eth/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -nightly-2022-10-28 \ No newline at end of file diff --git a/axiom-eth/scripts/input_gen/empty_storage_pf.json b/axiom-eth/scripts/input_gen/empty_storage_pf.json index 7c60a547..9979ea73 100644 --- a/axiom-eth/scripts/input_gen/empty_storage_pf.json +++ b/axiom-eth/scripts/input_gen/empty_storage_pf.json @@ -1,18 +1,23 @@ { "accountProof": [ - "0xf90211a090dcaf88c40c7bbc95a912cbdde67c175767b31173df9ee4b0d733bfdd511c43a0babe369f6b12092f49181ae04ca173fb68d1a5456f18d20fa32cba73954052bda0473ecf8a7e36a829e75039a3b055e51b8332cbf03324ab4af2066bbd6fbf0021a0bbda34753d7aa6c38e603f360244e8f59611921d9e1f128372fec0d586d4f9e0a04e44caecff45c9891f74f6a2156735886eedf6f1a733628ebc802ec79d844648a0a5f3f2f7542148c973977c8a1e154c4300fec92f755f7846f1b734d3ab1d90e7a0e823850f50bf72baae9d1733a36a444ab65d0a6faaba404f0583ce0ca4dad92da0f7a00cbe7d4b30b11faea3ae61b7f1f2b315b61d9f6bd68bfe587ad0eeceb721a07117ef9fc932f1a88e908eaead8565c19b5645dc9e5b1b6e841c5edbdfd71681a069eb2de283f32c11f859d7bcf93da23990d3e662935ed4d6b39ce3673ec84472a0203d26456312bbc4da5cd293b75b840fc5045e493d6f904d180823ec22bfed8ea09287b5c21f2254af4e64fca76acc5cd87399c7f1ede818db4326c98ce2dc2208a06fc2d754e304c48ce6a517753c62b1a9c1d5925b89707486d7fc08919e0a94eca07b1c54f15e299bd58bdfef9741538c7828b5d7d11a489f9c20d052b3471df475a051f9dd3739a927c89e357580a4c97b40234aa01ed3d5e0390dc982a7975880a0a089d613f26159af43616fd9455bb461f4869bfede26f2130835ed067a8b967bfb80", - "0xf90211a0bab67b63350661fa25783c2f67452ac187ac840aecf87b21c018c2035d34c052a0b5e3f7b3f777e03fbf164aafadac86619f0f37e9cb5b08b9a04a3dcaf3a5f716a0e954ffd760ea43659bcc50d88b636d98567483579bbf6785e637d3829709595da0b23b16bce7e838f0bf5beb4cc74e6844ca66fb17bd8a8c83602da9a8dff15e73a08dad00ba31d27f1dddd771ee9f1e609f8807cec316d72858ed027301f14488f7a0d3644048165527229f485a307489e0dd7107cfed73ffe30d044fe86d0e606109a02920c77571f135d925cec57a2cac42bcd40e4b0b3426c4309f0aca5a7a5600d9a0f2d16f1bd1e79df737387dfb52308992102802b441441f740966d74aadf94660a0578e9dba675fd742b618e6ac4d6b5523ab3b9e30151b4d63eca66887c79bda73a09f07666b63c448f3c898fa2cfea6288e66bf9ee65682f488ae3189d9739d5c37a08cc17835bf6ae0c3c69111574eb062a8e01d539baf51539426881de11c91fed7a0b62f824f2967b09f913c6dd38729a63a494eb005475130748f8e898e01ea2bdba08b4237ff97f223ff8296845e310f6fea8bbe985db7a63306fb72773535a978e0a0df6fd21e29171ab9fb03f1ac079ae9eb9b0570608b412cf2ce22d7e2ec4d4624a07daa3895bb7ec32efd44cae6d0ac31d3e5baa80368ee5ed7af8c22afb55d4ecea06c1115b5e036fe92e035ecf3f33476c856c33a436c361df7cb91c85e92e699ee80", - "0xf901f1a0c6bb1ea0701b1d10b7e30c4e164aebfb84dc7140887f90d65c4ec18f88f46aaea00a14703faec0e8f1d3d9ec723a3dd84e4eee5865d1bd2c26d1a375083d20d2c7a04d0fcfdb9a4a34eaaeccc15c85eceb4c4646e6e50d1248740c0d580bec52bcf7a05c77f9cf9a0ecd0aeeeeb2f76bb8285ebe7d7065bfcc2e37943bf7c99758825ea0ce584dc6b6b047497242950c20893e443a5075b3e9264e123e0d3d12874b87b9a06c5c5ede4d731402cb0ef37d3044cc14d39faed5d35748adeeef6a28138b261ea077b1173929d1c3020208c8885404cacda9db98d616cb258cee45a568464c5509a00bea809a0dac35b35667cd99830728fa78984f712388b4c56c8b50182706b595a06635ee1a440b3fe8ee9cf851127cd9e0f7051931ec6b52661f4a09f40399013f80a0e277e554593db21498b19ef314223c2cdd0236948a827d0b29833e17fac54b5ea0515a59a38883e72663f00a8aa5442593aa3b489fbe77eced17be90c51a928070a0a153da2deb6dbd1e905ac5fdd115b334657e3f30478420db77d9f23f6b52ea23a0e2233ea578ab43d549c7f60b66ccf7aa6c6dcb06589475932cf7a4b9a6c4fb1ca036bd9177121efcaae4f000754e3ea047967e33e6e9ed2e4e4138d2029adebbe2a0a73dc00b3a2bf6810803011679b9e6d4449ef5f3ae022010aefb291cc439fec380", - "0xf8518080a09c8697d3f0aa50246d1685a5437a50d6d6254a20678dd6bc00bcc21443da0a2e808080808080a0dd326b9398b50288fd981618b21156989fb71ddd5a424f62bcf0c1267907825a80808080808080" + "0xf90211a09e1a6ffa921727604af012d8ddff1e61292bb8a07cf843985648b6d6745486bea039267463db8cf4788611e0e6dc8f4a15959700fd7ab7480b4cd799a65e4a5ca9a0a6ba7d2e833fa39ef74f1c515ea5f0bc7dd7333465e5f3ee10f82c1643344049a08394c3d6b2b4063c0eb77e6b048164b23bcb5356101b3375c76217250df239dda06c315c1a9e9372e446cec3c1923cfca25bc319024c3fd068077ded7c57964d09a0d4f915dc90ea3380ed2d07985e7a4e3ee665db8d7ac3a70eee8d6b4577731951a03b21ca8e7f3c50f0db54b585c200c09c89f46d716a08bd8735394c3aa3b207eda0bf4b24566ce7b81847dfff7dd8af046c175f2eb2f5cae4d865748f71302e9bf0a0a61820108cde1a33c5e993a6e15ee8292605cb5efdae0d183cbbfc54bf3bb293a0913bc1da86b9333cf074ff3fa343d763e82f3d3c8d8ce81e6def8906141b3e51a0103719aa9e6070f622c27234e2fb70525e76804faa77b936900b811149205465a0acd9199e81382b974c2c4c4afee675ee6c1dbc6ab399a183a740cc6268db8aeaa095bbd96e08c238ee4950c243f7d355e919518caf9d6ca7634ef66335e1c1be03a0f91aa5ba1cf0f645c54f7f509ee055e112984be29d3d758d11802126fcf53041a02c553f8a9b836530292923bfc27ceb0f42d767e71832aee7705bab3c747a5f80a0ca3d1604084618dcb770ff87df1d59f833d2c6adf057fa29e425258692b5e02f80", + "0xf90211a003394cfb9e1c194c8fc031f6c55da758f838d793bce4b2de0579c06e0d6df1e4a0606ba857bdea832abe43b60b1a3e24d215378e126945e0878a36d27b98da891ca099430b112456210ffbd73f21f8f76134ad0d923c1ff988e14e98e7076458070aa09225637bae7fb4f3faa847370ed0f40eda1ed67e6af7ec94388f87cf018c266ca08302446d22127db44a4c72624b8659e689c403f75980375e0ed39d9634dee97ea013e18a6c9ad2cf18fb51be5baa08bb80b9f5ad4bc9f0dd0972cee11d0874d72aa043560749e3c67dde381f49557ebf31ae7228a1e2fb28ec91532329809d77db9aa0f6fbcbe1ff4c9321ef2dd9c01ec3dbb5328a1c2bc75d018b972e2b13569a5838a042656b44401b37437b217c763d9c3efa0d00066d6975b569eb0ac5be7b178325a08e87750e6ab64af8f2291dfded32dc1cfc6af781f6a4b3585f792676b6582d23a02630b3e3e42d1f87c10c85235bfdb593dc5b5b1918e56525d051ef2b3d682e6ca07bfd50cbce109d71b40fa5cba7d251cce183051afe8b40a80539505ead3cfffda07fec64e3385d48567ab423d0f94b13496129cbdb104bf237dccafc6a328cd4c1a06f9e547e4b0902b550667f20d54cef2c9aa15491d202dcb88df06275c59e0051a00fb66d35b5f71a4503182b85df82249dcc6672471fb2cc224d39b37b9380cef9a0177ce4305cd3e1441093756e84457deaf50a143ebfceed8269185eff8b43383780", + "0xf90211a091e5862ea58db1745f55296af00831cbebd152a0c34fe67583cff1266458647da0975e9f0c07e93b5077078cdb3dd918d7b0abf1ae197e45aed54b5bcabac7a26da00c33771f0de09aaebdf8a138eaf023273f39b9bd71d4422be1852b5508e5af5ca06b57931a4f97032ccc17e73e58227a3ebb4eccc6195223ea716fe8eb016dd0eba0940cca2fe8f35424ccdd4cc54e20e0e7116d4158e735da8a8031746f7106aca8a0563638a07a3274c0ae9cb490dbee3c1967fed1e3873b17da105a27447bab58cfa06c9c1e68380e91bce6637224b43d714ae588a8bdd1bc53f6d5997ac3184114c4a0f76b54d70f433a5adfdf14028fc1f77120645d31cc8829251263a6b5f8c084aca01cc4565d1490a949e0d969577087e5f64b7f6f8740818093e42787d2132808f0a0d02c85c89f6b8804c5b05dd48743f8fe429ff1d78b184a051daba280b7535467a0f663e59322f10c6f874f9a87843ec36395188beb64beaa25ae763b811763112ca09baf2ff7670c5339fb2034f1dea434d34d4fd96369a1a90116eaabacce287e80a0b9b19b8a821b23a2544d5b59b5dcd2164cb9aedac32580241ccda07cc74048cda0a916ee6224bff693ac28a2f3b7c28a82893c6f6d9ed6ae40537ce50ad00f921da08346078a3e3c05cc8e0b5d19d306cf825526a5703ebea610fe7845845e27e4eca0b1a71853390a2f482ca843fcc39102608d62d6e02643780315dc24e5c8a37fc680", + "0xf90211a022533babedbe51e141ef2071500091d2fb2db200f8315f73bfa09cca7062bad0a0a92524dbbc5e5882e791b9fa5a6298a88955367f0eb8a8ae7960051ea528cb88a0cc40cd30364c9b7c791a1f653d312807e90b6451f559d6ed4e7845247a29e77ba05089ed245e3d1a2b51bb131cf30ecdc3bc22b05b6d415ba329e763acfd1bf7fea0839751437155cae550f2fac889d8c12124150626d92c960f648a635d7d2871bfa02ab4b61e86ceea35efb62aa4145ad0032e8de270b7374032b7b6a2c430ecf666a07e9c665062b64eba84b82cc0212cf71f6db673df3e0b4603c8976963211f4903a0a6efc3e7d9e3c77781e0a58e3dc15bcae6280e498207a428b50477fcef533526a09fe3c62fcf625b80d85cd5b2ab1a9f9873730c421e6eddecc9a8f2941cff1881a0318a7c31c6c11c893fe68ee5fe238abb1b5b8184328dd362113c2a17ef33c236a00ac64f0a7f1234cb01a44660ca25c704d2c95b8c23659ee4ff8311aac05e2d36a0ae914832630f8c1acbb746b6b1dbd5e2124362c70f48188f35b4f1c15c2b1cfaa0821b42665926905e9356b80fe1557a541167c8b66fc13ef3988201656504ddeaa0f7b757866ed33463a5726b2f5434ccbf9c27dbc4fa14b3711405b975996ac7cba059ff74c6fffed5e934fcefd98a01b6179486fdff6bf4b438b61262e3c1e04fb6a08331d4eb99dac92e6b1406d6fc1e1195372f9e3eca5cf01457762f6a59fd05a780", + "0xf90211a0e569b9913ce619ccd100bb4f4f6e0e27508c2255ee086af7f5d3fe9949a35af6a064e6a9708a47c608e2d2ea66c43101a02ceb86b1030d19da8ba08773d6b9d162a087bdf3a6b076843f2399422414177a6bd98ca43d8dfbd84bb485807f862ebc70a0432f0d4660a7079ee31cbb276de54d2b44ef7761acbcd87f1bacc2472bde6b69a025008896b6264734ae47cf1a337db5831d9dbaf4db865442e5ca86dd50ef75e2a088fca5b09967a58c67e54f996cb60f755dd50f56ef9e30f170318c418405b274a072bf772936d2869a2201d435c12b4f5126a67e6b02b1fa82170aa7b59a0fef0ea08f42fdf6cb500b44b4827ed1b9f793649730ba5e2092dea30acce4b1cda1360ba0dcc56188e766ad31dc496f120acd6eac62a0cb75d96c902ccae9c03a85ac55b0a06baf5e378e51e08d70e36147d4afa0932c6593d6f796d3cb1b770b6e45d5deeaa0d125697914c5fd3aafdb463db8c44ab4301be1dc94a8490fc79f78afbbe465b8a01b2353b73d0b53935e0c6b66f76ca510437876550a4b5a2f9b1e70881d54a804a0d539d6b28740955b2e51b0e72e9c845730a4b4af53c0c5e68ca25703e4fb516aa0f60efed6d4924d93860b04f850b1d468a10bc11250bd59920e87b2e104e58a9fa027323f5428333054913094fc99ec1ddee68011ed8486282803b54b9d33ebe882a0b717930686065acfcfa26d26ffaa7292c00c7d95544fd9e33aa3135ffdfc7a9280", + "0xf90211a0452083769a54fad677e929dd77fab33135c4afd4be6b4e0544515824c1ed5ef9a02b627a98969342594a740f368d1d2c04774f743ad4840c1e7cc2fa293670b745a0ce3230152fc6b3a9f0904c34090cbac3742e29494dd5e7a087fd9d7ed23278d8a0c109454e027e6ad62261268ca60a8393aed870a0a3f717d2c6f3cecc94adb85aa0cc916def1b999af566a0a83bd5c645365fe5563097e048c6b5c1fa1b22a2bf7da0366673465256afd697828dee6dd61c51efd1bf5dcf256f12f25a40c59bcd8ed3a05417eb53efc7f807949562a014818939b810d397a28a4b3b5418af8b18be969ea00dc079a1386f7f622973502b2a143334483d1ea8fedce6033c1856690fdda0cfa002d9dcb6ff89b82909bcb2b930842ea48ce142f96e7cbc83fc398dc0acd8f2c2a0fbdc374f6e6dbd2e1e095472153bd643047cdd1d013ac263f43fb516417eacada0e6301d02d858da65a69824787e79317cdb220c0be911884186b841e1fca14f26a0e9fa5809fa0f0ff6d198f82f0c61fc0dbca1b1f2d790eeffc9c769dbdf74365da0746ddce6d4653ab890c7f1f974cb892285c3413a6560e4d8d1c35a8241a5ed29a0ef2314361e2b81eb496104960174a8cf9da04e2a6818b258623a52a426a30131a07e4b3e3287706c3ea576e2352c15b747824ab02e79d7f356efa99cff3d164250a0c39983684fc51b19ea9543ebec03def9fd68b3099f1f6382bd58c78cca5fc48c80", + "0xf90151a08a1672ebb05aac6b892d2b57a8c23b39324073871abaad6816fdc2f1ef560fd580808080a05ba6a0e56884ee3c77e559582852cc9f7ed0424763e933a9ce16ad92a54cdd6e80a0d5fb0a3096322f5d1a8ded61f781b24249e3696e3da4d500f7fc864fb5d8e505a0f15f3ba96a351bf987fa5fa3b19060e9b8c6ab4f66253889a38f1ef8ffaae67ba030df0fc6401936032434e2322e991a5c84c04db3718d556944727d0fa6cc3e7da0f2641fc664af7f7c48df51c5af237b036e742615dbedb948f88903b3f1e9f1a280a0362fc8b7b56aa11e5dd3aa8c73ff04fb8c9dcfd2c4afe9adf835d644a5dd3be3a02d0823f444d656496f0298e1683a327c5aaf4356a72b45a4aa4af8ea9c8f4191a03e691bdb9b12a1f2cfb56b69d36df9d060e0aa1c880e691320e745e8beb83833a0972b828871ad39acb5d36be111bc1ed630f522eb5c34fca4bf33c1f2c326ea6f80", + "0xf8518080a0b9adb40116cae4b932b3a669200795bb505f27ea16b99a62ae862384da3803d980a04240542417f20dfcd2c8c95b5f95a6262b2bd2f99cffde2247ccd73322620303808080808080808080808080", + "0xf8709d20e8d9174f6325cb58d1667ea6512ebd61aacc0be4d9dc9ff4eea7603cb850f84e8210c28827bd73dc2bb0f8a5a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" ], - "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", - "balance": "0x0", + "address": "0xd5df13dc9a72319ddfbd226fece1e438aa920d55", + "balance": "0x27bd73dc2bb0f8a5", "codeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", - "nonce": "0x0", + "nonce": "0x10c2", "storageHash": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", "storageProof": [ { - "key": "0000000000000000000000000000000000000000000000000000000000000000", + "key": "0", "proof": [], "value": "0x0" } diff --git a/axiom-eth/scripts/input_gen/noninclusion_extension_pf2.json b/axiom-eth/scripts/input_gen/noninclusion_extension_pf2.json new file mode 100644 index 00000000..3dc9434c --- /dev/null +++ b/axiom-eth/scripts/input_gen/noninclusion_extension_pf2.json @@ -0,0 +1,28 @@ +{ + "address": "0xbb9bc244d798123fde783fcc1c72d3bb8c189413", + "balance": "0x96918416033373f44f12c", + "codeHash": "0x6a5d24750f78441e56fec050dc52fe8e911976485b7472faac7464a176a67caa", + "nonce": "0x3", + "storageHash": "0x8d7250d4dfa9ef030b1151e78dea9336d14bb78f2bce5a9d10e3270d605b11a6", + "accountProof": [ + "0xf90211a06875525241ed00974ff8f285d34cf893d590cb081b06ccc8a58becaafbc61586a01b8224bf9e335e42ee25ac1f580f0e675333440fab002754ff309807c6739f42a07eba4b6c8bb368c032a824c5e198f89c81840c7a7fd3e980eb6198c9733b09a5a0a94bfe5ee1cbcff11436d6bdb7a88664489329d5884c505bf244df1edec218e0a0f72a31f473419d4e8fb3f14cda0e574675d59664d9957f15f2aba9c674016f30a0b75e6cf09853e23027b38c892a913be14f28cb9eccb7ebad419cc0bac97f2c76a02fbe507c717acd450d370d3810459eb6b21b99b772b1ebc6ce2d6fc675489d00a0766bd9b7509e7050b87ffc562c64afc6c410cc2c9c7957607f86366fe2b1bc34a06f9c87d701284a345eafa31af48ad089d299d6db29bc41080454b8b4c7a7f509a0ffd2f17a5ba5b3703565bc7c6814a887f9dae4963a7aeb4097c3417667ec2474a0f4acf51081bec49ce14fe4de9f953bbe88331a82888f20975f1e3d7dc832b9aea087c3e94f2cd280568d7b02542d3a1abd4e1236b725f80b375f3d373dd4fa0052a06096434bc0d93cd92458f013f3ed83d2763936adb7f2534183d3d3d8ace601b4a01bcdc447840bb0250efb7517bd2ffd7da493b7b5859855b746533f18ffb454cba0b92b452de3be6e9dedee3beda9ce094dedb9ed176d4efa1047260031ed1b77a0a079e1cc06c10332ef983eedf3de13cb341e7b1a04f4695cd5ad59b2bc42db569980", + "0xf90211a09ba45dc6079684b768fe08a715666ac68520c62c313b2c7b157d5f0c03db6304a05d481b0002593012fd24d2164b755081287e0317385052a8762c1fb520bf0d23a0eb4e543b156fff69b7de8536bf0c9b540240374bb254db4103ca6d43d2e096eba04ce9ebd41461ab69ad714f78db5c9b83f62bc27d14c42209ad07522e6d7f3a19a021a3c256e39ca1890a9515e2c5ba926c2d8d98d3efa4ef7cd2c733518779a9b8a068c7fe4002c065d3084149cf05a61cf50fdf5737b2517a7ed29132200c8288e4a01d3d30246b8290e8983ecc61dfd4f503336f25a52254935e91367ea18bd4e136a0fd77007483e80764d390c9bdae7269e2060c68bc8f7d58a1b30dff07e83a7c3aa062cb759f5b4a51dafab908375897267c40ae4998095b808b5b4b89d4d0ade4eea0fd7b9deba9bf7a39a910e7eebfc100e96734015b6b0723d425bccef537be6a20a06aba685b3ce9c9a6a8d3886b7dbcae0a0b7e6113889915bd2fb0931cd94d7e38a0cffc3a6628b5ea012f29687a52cefb2aad9c6f17b6ac237e34892eb23e65d98da0ada0992f24ccbdfa0de3f32c4f3926cde0493538911f2ced6174120e774654a7a04d98289fa6b82300868a9ed731d0755a973c549c5ea91fa2036ffad70516c702a0e9acae2f3d411d0f6c6d5c912a77af5743d7b8e43e143b97ce57f60a8c498a0ca0c73576eab13deab8cef0a30d024cfb7362d0deac40e8a5b3ad49bb6d015eef7080", + "0xf90211a0281b05ef5553277eb4d42cf6fb4aea6c72a098fce901c6843923ff16dd32fddea0f7105b967bd8f4e4ee05d26b56a6049ad3f49188e0db01393197d9c1a4f39a4ca02478359fc762b583ea59b3b8f34663817e5feb17f334ea43f3de9de335d39d12a0a6fec39fcc228dd5502f0e19c6950535ee9666c8ceda5910392fd8f776ae05d6a0a4f627369c564ab043d337342f3774d1b2b55e52cc4725feb884e22f92734b42a07e702c82be23b07c4c2f893c6b49ff2f7ba23fb33948b8efb1954bc1cf8a68d0a0cdc4f3f16b43aedf844b281ec87822564a15808b48684e12dae0bb4de1b2c76ba09063615b516ae857873276644ead39c1bf175f33e9d7d8bf0d653e264e46173ea03b9728af6ea0b3eb408e5ea3fc09e0e1f5ab24f5e3a8b359661777875aa2efb0a069b6e0f2a4bceaf2f862cb20603cb823ab8356094121c16dca89c9bacfb80355a02a9d3d9c2fa293c6ce954015f33c16b884d02d88bafda1e8a0b323073742cd65a0c3a40b1295b99f81304424040aa2019300a5ed51cf47943578276553aba5ededa0537baa0251dd9abdb5f6b9c87b2062c5016ed54d81dd413ff2ffe5451e67dd56a01c110675f445f35562112c9e7aadf60f0456395f31dac8f9e523eba5ea65e855a0027c5d9ba14e140cac36dc0261e3e3d1c2da5eddf6d113d766b661abe7be3be5a0abb10ae31008d2d3c69baecd628749a364d931d0e42579776d1714c54daa3e8b80", + "0xf90211a0d976f65468f55c49ee686c24ff2b18f5103d3d209a6db684dd37508696f8b847a0d92ff004e27aba9aba3621bd82f7471429b1b59648a6ea18c4eca765976914d6a02558819a7abb626efcf86110212b685c810705f67307de5c0288ed37009f4925a0ab220639050926bacac78f5a40ffecb92220d74ed5ebaf342937074167c78d5ca0d0e2322b9b5117a9256db21732261ad1cd42933d2838e39209b0725f2f22de7da091c5b7b4e040dc9d82d60286148a904cbc76dcead181b9130b58376d3bc30e93a0a5ec0e3d7ff3ae87fa323fe2fda207501868bab3e4181652d71075deb67e4b42a07a9dbfac3a7774ce342e97a98379919060e837790bcddc5d1d3d897db77f80aea015ce6fd88cad4396f4ce597007be3363a95f8e317a0604e0c789cbbce11c3ac3a09b3266040079422303a42c069ceb94362cfd4fef697ee9eefc86ee20f11ed50da0e0fdbb2e74546da02f137bd6bd8220c09787b82f6e76bc505bc37da246d004a9a0981952f9c255a7d01a316732e2cc32c18d62a1f748d7fd5466ecb72f35a075b7a0ed2ad26e9cbee479798b32fef4bf1413cf485b5d2f57ee69ca94655517e78848a01f287e9ca4e72dbb04088f45fc70664b6a7677cc86f4c135edd0c0d86b4cc323a018edae04c38d66bfb0a6513b9a6d6ecf73e112a4f66890cc4982654591e14ddda01f6bf8f8a7c6ede4045460d6f9b7d55189b664380462cb9ab94bbe6d7f71f61a80", + "0xf89180a07639f948c5a444fc90516106d6cb55402e17598f7fe9aa3ef1118f1293c0775d80a0a902464a86381c46240735ca8f986f5137034514a8b528976ff25f588008e89980808080a0c1fe8e7dee6bc282c2891f54412767a17411dbd8eec5531e3d3d33440db7e1eb80808080a00251bc5b7345f1723de51c7c5f4c52433f1d2ce5ef6f49173f9b041ce6940cb2808080", + "0xf8729e330c9f91acda96a5453d2bd6e46294f79f072835244746ff001b6b20e5b2b851f84f038b096918416033373f44f12ca08d7250d4dfa9ef030b1151e78dea9336d14bb78f2bce5a9d10e3270d605b11a6a06a5d24750f78441e56fec050dc52fe8e911976485b7472faac7464a176a67caa" + ], + "storageProof": [ + { + "key": "d1e5be1e03a9eaadc3b681ff116bae5b35f636dae177083008cf9075f00cfa8d", + "proof": [ + "0xf90211a0f05b0c4d8599338b3421224540ab9c4d9626bc597d5fa7ed2a6071554b47a6dfa06fec4c7cc47051b44b91007482da61c91a2e4ee5c6aba946dc85694cafd9e12da0125f12fcd3e7c3e755e0a813e4442663f1ef4f9f86c12c95d634670041119b64a0c13fe7c559a518551b418b9e8ff778356cd41c2a1f9d35c72a3b79a468181e4da0633ab7b1b2a25503b4cb7b58388be724b544817eae189437d75fd185fdf08633a03df7b7ca249e67d8d89ed34e58ae30a72fe4c4a0d50c19fefd4d72d56401a867a0cbe06a4a38f4b05e3552e6bfcefb14e56bce58c5062530f516a64a1bbbf70a75a062e55a51133f76af32690afcfff97d4772085b6030f31364716465bf2161dd17a0c7b6e1c1bfde75f27be5d3c98271069a7c4b82e78870cf91ab5556a5df16aa59a00b7afda86207aaf250acf7692e9b624149965bf663c00c739dc3bb5ec7902c7aa052e5dfffe1d1a7cf631a34bc723e6af3e2c47ee36e7433635c67061985be8010a040ea6b2cf42b7149eb6d47e634935258f6210944b1d176e4f0510bc0a89844d3a0ffd4ff032808737c37dc710969fd4859fc1dc529cbae90e30932f52fb3df93cba05be6bbb19f6a69d6b7d8a5c73d1f6015f880faaf965be2f4606a5d0ffc78ee92a02bb96d39f5de2916e0d2cdcaed05cb1578bbaacea4361fff1716603226e5e9f1a03fbc494221fbadfc7356bf7044782af5efb668ae7e7c9ea97bec82d2cefb562f80", + "0xf90211a01d2617c9d9cc778a01eedc9763e9f47539c4c7bbdf0967ca198efda6d2537aeda0ac66dbead9bbbd89986ad411b93a9e958eff7e640cfe35dc5f60838692b4ea16a070b0e165d0c45583e2717912ba7e7b79ded65520afdb98af230c4be8397d729ea0d5ca3761921753160848ac1e9a00ce5bdc5d35b8c3caf08ebc2acd4e8ba2c3cba059dccd6fc739aefb37617051d87b397f75e407fd14a26137bb771a17ff9a0bd2a0b7a0212fb2e901ac369023d45a87d9586a6c5ba8d2287f3bb58295913e076e56a02c3aea153c85a68b319478f3e8cf5dfe779a6dd51c6fc03460f1ce61d1c62f01a098b7a22c454cf96a43b313d87bbd85517fb53b3d95983d60791bdbed5dea8225a091544c0c8238fd39b12f19d4b744f5d826fbbafd6bbb9adeb176ff21298c7873a0cb007dd98fb91c7f483660f0302179085323ea2ee78444da3fa140acb63d6c5ba0e72654923c72648e18488a814e269c09cb2db1958b0d3d1e9f22feabb4bf18d5a02e8e004b322376e2b012bbd79f554171bd6db5b44271905409d02ae052c8c475a0c15564cb8ae8d115d58963137cce2fc8169cc0e60a57d7974ac02b7fa08dbd3ea06d10ea979253f8a8f8d14eb3af6346424d537d2d923be989ab7aea562820c967a0bab8515a1a7ce668dc9827f40b126689d9c01a910627470f1090c18636c974c1a0d14fe2c4cb72557941fe1003e4a41794dd81acab9b4d2a71fa4080e54418eae780", + "0xf90211a06b83275f9cf83dd9b1dc71420d1e3c7c58d1f73b97a0fc154ac717fe40663567a098e259a9513192dd29e6b388fbfae6f2a0f95f0dbd9906a19ec479bcdd88e307a08428d1d7af6a3567a730491257d7cf0b5c85473daab46ba277f526401dda498aa087bb09ccc970e185ff75a69099159de0cb6470c11487cc7aea34732d7d39e41da0e1ed27369b2b97ecad8cba15ade8301b07634c47c42077e801ff161a95dc31a7a001907f198c00cc5e26554e2faffa8022642ea38a522bce1cabddf1df5cd1965ca075a6a35c8b056e497f1f722a9b340c4a21c3a9ee39bbb19e94155d902fff2d95a0d54f3a4bba0494b7e0e13a1ec3c8b57f86e55931c97b366a9ad0e6cbe7182c71a0687b1ab830ee6cccf63529ab0b2bd04a88611773a1e56d815ec759ad31bc0cc4a0912f21ca281e7eb18c194271de7858a451bd17833f48122a6fc212542f9d532aa0c151193cb0d1578db9a6eb283fa899f7d80d38f52d9dc25402f0471cd1c80a00a030d84df7230606b70e5086a540bf23a84137bc50abe4ea8920a53145ffcc4491a0f10a5014826e7920cb30b94ac67b312e07a8eea02a397632d01f0ba1915970f2a0400cd101d8464b1228d00d66dc09145f1ab1e372da516e12225b8f0142d70fd2a08d85d1638509f1cb3adf3d858c5bc7936c33f848bfc218350daa8ead0c7ca108a0922603ffefc60101f53b24b6c1e87fc23112e7380b9f7bc66d4f660845e1ad6280", + "0xf9011180a09d7f732a8b8b77e8521023b103c3d0bc49407e0e087b3d2a352eb5f29476c25fa0e74ff0fb8b92e95dc334b60293704e7ab7f6435daaa53c5c529e53f693bac950a02d2485c8a30c2d72a731cfb7cd0e0f9c89d6ea67bd3287e76e387cfd790257b7a01cbc6483b948a8d65dcf8142167aeab689195935ac52e96cffa2e253bc7fd0a18080808080a03b4a4656fe4deb37341501c5e89517c55cd8579132744f00c778fc638ed7f47080a0850a8b2f7cf933f8b8cc7f7caf720adafdd3d6fdf3d79743b7adbd4b47a7154780a0568e11b290ec02d80cee7242426ebe6b050db362d869d0a254cd16afe42dfc6aa0e176348f1651d1660d976f62d4eb8b893ef88749cc8ac7bb271510197529f1aa80", + "0xea9f206cdd33f710b60e97af0ef88584affcef1a8179b6d037941782931180d6e58988c2648418dbf37955" + ], + "value": "0x0" + } + ] +} diff --git a/axiom-eth/scripts/input_gen/query_test_storage.json b/axiom-eth/scripts/input_gen/query_test_storage.json index 098c4efd..891f3b50 100644 --- a/axiom-eth/scripts/input_gen/query_test_storage.json +++ b/axiom-eth/scripts/input_gen/query_test_storage.json @@ -3,8 +3,8 @@ "jsonrpc": "2.0", "method": "eth_getProof", "params": [ - "0x0000000000000000000000000000000000000000", - [], - "0xf929e6" + "0xd5Df13DC9A72319dDfbd226Fece1E438aA920D55", + ["0x0"], + "0x103ec96" ] } diff --git a/axiom-eth/scripts/input_gen/small_val_storage_pf.json b/axiom-eth/scripts/input_gen/small_val_storage_pf.json new file mode 100644 index 00000000..96a0b7f0 --- /dev/null +++ b/axiom-eth/scripts/input_gen/small_val_storage_pf.json @@ -0,0 +1,25 @@ +{ + "accountProof": [ + ], + "address": "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb", + "balance": "0x12df5f56180f1e41a90", + "codeHash": "0xe2e7a7524a98ce629ee406c15c51a683e4167f0b74ea230566ddece7ae9d6f0b", + "nonce": "0x1", + "storageHash": "0xc138c0edb743c4874f25abde4e8e22ef5a24ae96167ef179eaecdb773880588a", + "storageProof": [ + { + "key": "015130eac76c1a0c44f4cd1dcd859cd8dfad11d8b97bedfbd5b2574864aec982", + "proof": [ + "0xf90211a061acf60be385db534720c3a90cb12b926a5feb4aa8915b9967efd6cb941a6bd2a047e5cce4b1ccd6d16215eae416486c25124bed878b0de607ce325dda1f1ff8a6a08fff7aa4dd54c636d97a8d4bdb8632607d8ee37b62008230a45fbb01ee7d06b0a095387a22f5dbdfa96473ea3da9d09e9811bac8c77219415c4cdd64d1c4f13eb7a02baf58e7d6ef54088f9263339c861536ac214f4fb37f4f2fa2131897e9f873efa0d47dcdbd16d1063d8265132cccf6d855ac465abf6ba3918e491e72fd198c35dea0756c8cb86bc2190fa0445a355d3634d151c0694051f3a9c2baf441b58e3d46d1a0ddcaab2008871bfbc3e37e792e1a5f2cf3762b216b63fa5ebcde02161a124e36a0c97aea09ed7dcba2074f35c5b3f6feaf6d4b4530e5f98fcf6d49021aa7a9bbe0a0eaa4be5178b745a6b368f99c8397a7916088be619924099a48e9caad7b10bf9ea0c8182bb15a64f2d00de328fc48ba33c8d904c0abe8a724cf990c7266019411aea0bc964e6778c173341cc1ba19378e01ad6f92aa34a15e779e22b58e3424271c96a05f45cfa8177cad4fa7febe7d92bea593651b0dee03426cafcefd639989ac2689a073ebd753cfc68fc7c63b92eef438e4963a2c5d9ef26d69f3bc092954ff042ac5a063b091b66f32fdface075fd018e6a43c6a18c5e45c0f36018766b9fd359e3f07a0622edead43b56de895f373d1023d453a7f28fe9b053d3d43f38e81d8bbcc33cc80", + "0xf90211a0d3a6f796f683b9500a144cffbec831a1cd0f36fa179d495df2ca739950eced36a0aa6265cdd5fdf1f235483206cc564a7e6368402819bfb59fae1af54215501ce8a08daf46ff598319c6f8669f4fb24bf457aec4a548e67bb34464b8806145f09732a084bc9452c3c31282800fedabc79f26b4ba5ff870b1501e4e15a0445c109baa62a0e26425c50bb9cddf78b5ea820af5235b2af83b54399804153d31b56e008ba40ca06ced50532f5303138b053cda8bfa07e8be5b5a281f598e463739663cad60b7f6a0db2121deace8995ab60128cba5a5bf414ffdac28e4dc90b26690f593d93d98fea04c6fe3b62eaaf990df14fefcd15b7822c0c9b6a3bfb6c97fbae4898fd8cb8751a03e63d266e6bdcf2f6b68b09f51208dce30b8eccca11cacd8d4d817f5cd35ebdca09f20131570ebabd82ec6f025ec3161893c2860bbf66fc612eb9dd529e63d12dfa006366ecc6e9adec67eb87a740dec9de832bebd24fb35eb639660b36624136718a04ee5aa74f2b65c5b8f9addab8b4efe241dcc94cb00862e8eab3f2064de570993a028639797a44fb9006dacbd8c9516a7b4c2c0b4f5fcfdee58e6d43822d7bcfb98a003d75afe382d531d98d8ba7096b7df7c16a882fe674b5f198dbb0be78133837fa0d696f1f254fb0f631fd54455fdf6bd3ac97a1e1e7ffb56531f9b59a000543b02a04d4f0d1386215b2d7e64fcd2bcb8288c169faef6f2597570ffd01a9e348e9e9680", + "0xf90211a03ef27e62add278aab0d21cc70ad297e32faf1013d12a945ab541b54c218e332aa0780c1b88c39ffd54b8437d392e1cfe174245eae8b5f57ba1465f5c4bdc009eaba021c6cb03221a1ddb9479231b06eec98e0cc29757c6b1f66c075161d19d28d899a0dd77dfa5f6dd39de8be556a74938a32ada4164cd3d74c9cbda70654b5eea48a7a081c39c7fb4b8f4fec9492de412cba484fa94fc5af022c8ea4ef2b74a3d5eba50a0be7769cb5d2c11c9f0e27b77d9253cc1c58fa93a6bc323d41b91518db6a1b4f7a0a822a61d22a729d9b7eb8a732fc7aa7df47d8b93962c61c036624f8919bcf936a0fabbc2b3ff72ab5f33ff8e1b15b7290f7ea596cf7d8df41f8ccc672829981779a088d974885759c175d317f33bed84ad1fad92117ff49eebe4822445f6c6432ec2a0a8f0c9342ec02038d206a397eb79ae89db9d50ffde50ec5ea1eab473b1ed65aaa0d4cbc8faf94ec2d78a86b447459671136d442df37b9a0334dea626dbee21a7bca0ebb21e8a0693a521e11574dbc2b64c0b60e1ff03d275ae9015fd6f77e2ffb348a0c42ac066fe48e16c989a92cbc8abb771492ec1214773acae9b479201996d79faa00d0aa5e650dd52161db61f3e8daf3a98b5807b253adba4aa330a1b2597016ddca04dca1357f7c38d9fa3be494ba30930d17e5e2b259376cc0729cbb18e28522d13a0f47755047d48aed62cc054b9444230a02e3b4365a423a86da7d1651d30bed5b480", + "0xf90211a0eda0f1dcdef84dfdac07fda9d761613e383566a81ba5b13c160a0061bed49005a03c3d8334c3d7ce552a401fe7f608e40693452c37b0ac9c854eab22e433932088a0588d16eba3c636da30d34b0702eb7b6f5fb378ff7bd4b957bb5cab7bbda42b2aa0a23edbab1becfbb4f1f901b345fb360f65bc7f3477cdd9d593a34fc3b40aabe8a0c7816adbe5240f04a86ba81deb8d0740be0950eabb9f074f1b57c27e36e0b301a05d1058132f99bb51cd44d1b22a7145a2c8b3cafc65ebd3f3e5c5d10652d0cb50a0e0f5e3576473f62725a2a94c0be6742af15eb0cb0e620941b83bf252eb0c9284a00c9a49e594f73e3b4dcf2c8f4acc07c7ae263de5b9c1b1917f8b0981694c1a88a05918db6ccef5fc33ee95423a35d593f7463b8ddacf6ed599be4475d41aed78dfa06405ecfdea3ad15e04a21514c6d43647e88b7aafabceb43b54f121b552ef1453a0cbd382e1c28d0abd466fd6a13acf2229afe27546ac12965baecca343346ac606a06252e48391dc4c31c8aae394fbbba75381346f6dcaba952525d636fa8260e14da0943e5af9e9e84896217ce85a58f0c453f6fb3b09825d3af9e9039ac8f06ee347a07d07568a760de38e6623bad03ea0941e6abcbce99cf809b1da3de034481c7f6ca0864d94bff09139683ae4674b43a8ceedd058134a77b1e6fbcc22d7a821483304a0d5066909c2b6b5a95d927ae60fc7cb37bb5192acbfb72f070479ef84e7a8d88b80", + "0xf90211a0feb174d439e0dc86af9c6391161126cbb3041f4c3d65977e80f0a2f617df88a3a05f3b90bc51ced0a787645952db0965085988b103ef9d70c9b7a13b419f2a8497a001137732eed9526bc1233f5e7a6094a7260ca40305bf09249c5be92ce05a43b0a0949136e6c758730ac0a0d1ce5142b5090ef330e355f39203170e1d5c9a263d17a0dfa05eb61765de1d31a696008e783d08193dd044264d5ae3b90c916ba012a445a08e1c09b54baf884acb1b789503a82a50019231d5faf8e333e9a8305d70a07306a0bbb1be9c91b93b94f7a62c05bc22350c1221c858d0a7e69055acb15ab075cb6fa0ca8ac36ca096caa295ffca544c34943a899a9fe01a38777e7f51eccf3db140e3a0ea412b9806cff2f55c13b23cf1e8b496e652274656ec5ad33484030151edd2c5a04b4ecf27d14b52daed5d42449fd34b3a17cbbdc6a5d045807c9ebe36907ea6e5a05e82e7ab805f9deb9e1f161efcfbb4790b2e0d1c5ae8c4dcf8135f327baaddc8a0b33244a04adb6a5c63933311d2aea2004dd0dcb088ab1f32deb096d28f70fd22a0e62a290a9cab845ef11adc0de7c6b8fef91a362a11b65c33d8478ae6eaf6d095a0cbf4b6962195e1b78ac9d18e3182272f52cd3bcf99c757e278c345d7d99dbb50a0e90c8d491802dd1744921b54aadd18c27889349dbe26f39c022adba06d04f070a079e011dd3e741eba34865f99e613dbaae1ed633091ea728d350fba5dd826774280", + "0xf9011180a04f0bc36ba74e6466f88f99ebda9aaf5e3b0d0ccb1babefde75e1794354c3872c80a081039b3c1c6f623d87e0bfc2c167b94e9396b4531a4c96b1a530889fa1f614f8a09cc8165d6cfc1cb51c22bb8d227f0405788a4fe1882972d69792abc7012c10e2a0dfd3faa8be2e0b5a889b142c3215d5f33475a0ba82fc1b1a0b0f729145bc713680a0a12352bfaa48f2cf6836747939970bedbd57c4bb41be21e8bca51e707f914f17a044e81f28fcc6dbff10ed896e61739345198a5209f18c606a0b8acf94125a3cd6a062453197ff143d336268ceac5a85c51ad3e550703939676d34647c35acb37e4a8080a02dd35da72afddfa9855b7a5a934486dcac55fcf982be124df02066f1482834d280808080", + "0xe48200aca0011e0fd69b0d105be03f26707e488aec6ea7a3439dd73c22c95eb780cd7b766c", + "0xf84f80808080a0f02444e4dd7a61d2a14f061f39ff92ca158bfff48da37120a4c9c54155cf6a3ede9c300012866d2bb9d91c572671af93a9cfaaf8295e4838e1b8f6d739e0018080808080808080808080" + ], + "value": "0x01" + } + ] +} diff --git a/axiom-eth/src/batch_query/README.md b/axiom-eth/src/batch_query/README.md deleted file mode 100644 index a9730fb2..00000000 --- a/axiom-eth/src/batch_query/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# AxiomV1Query ZK Circuits - -Design doc: [here](https://hackmd.io/@axiom/S17K2drf2) diff --git a/axiom-eth/src/batch_query/aggregation/final_response.rs b/axiom-eth/src/batch_query/aggregation/final_response.rs deleted file mode 100644 index 81b976ab..00000000 --- a/axiom-eth/src/batch_query/aggregation/final_response.rs +++ /dev/null @@ -1,268 +0,0 @@ -use std::{cell::RefCell, env::var, iter}; - -use halo2_base::{ - gates::{builder::CircuitBuilderStage, RangeChip, RangeInstructions}, - halo2_proofs::{ - halo2curves::bn256::{Bn256, Fr, G1Affine}, - poly::kzg::commitment::ParamsKZG, - }, -}; -use itertools::Itertools; -use snark_verifier::{loader::halo2::Halo2Loader, util::hash::Poseidon}; -use snark_verifier_sdk::{ - halo2::{aggregation::AggregationCircuit, POSEIDON_SPEC}, - Snark, LIMBS, SHPLONK, -}; - -use crate::{ - batch_query::{ - aggregation::{merklelize_instances, HashStrategy}, - response::{ - account::{ - ACCOUNT_BLOCK_RESPONSE_KECCAK_INDEX, ACCOUNT_FULL_RESPONSE_POSEIDON_INDEX, - ACCOUNT_INSTANCE_SIZE, ACCOUNT_KECCAK_ROOT_INDICES, ACCOUNT_POSEIDON_ROOT_INDICES, - KECCAK_ACCOUNT_FULL_RESPONSE_INDEX, - }, - block_header::{ - BLOCK_INSTANCE_SIZE, BLOCK_KECCAK_ROOT_INDICES, BLOCK_POSEIDON_ROOT_INDICES, - BLOCK_RESPONSE_POSEIDON_INDEX, KECCAK_BLOCK_RESPONSE_INDEX, - }, - row_consistency::{ - ROW_ACCT_BLOCK_KECCAK_INDEX, ROW_ACCT_POSEIDON_INDEX, ROW_BLOCK_POSEIDON_INDEX, - ROW_STORAGE_ACCT_KECCAK_INDEX, ROW_STORAGE_BLOCK_KECCAK_INDEX, - ROW_STORAGE_POSEIDON_INDEX, - }, - storage::{ - KECCAK_STORAGE_FULL_RESPONSE_INDEX, STORAGE_ACCOUNT_RESPONSE_KECCAK_INDEX, - STORAGE_BLOCK_RESPONSE_KECCAK_INDEX, STORAGE_FULL_RESPONSE_POSEIDON_INDEX, - STORAGE_INSTANCE_SIZE, STORAGE_KECCAK_ROOT_INDICES, STORAGE_POSEIDON_ROOT_INDICES, - }, - }, - DummyEccChip, - }, - keccak::{FixedLenRLCs, FnSynthesize, KeccakChip, VarLenRLCs}, - rlp::{ - builder::{RlcThreadBreakPoints, RlcThreadBuilder}, - rlc::FIRST_PHASE, - RlpChip, - }, - util::EthConfigParams, - EthCircuitBuilder, -}; - -/// Circuit that assembles the full response table, verifying -/// block hashes in the table are included in a Merkle Mountain Range (MMR). -/// The MMR will be a commitment to a contiguous list of block hashes, for block numbers `[0, mmr_list_len)`. -/// -/// Public instances: accumulators, followed by 13 field elements: -/// * `poseidon_tree_root(block_responses.poseidon)` // as a field element -/// * `keccak_tree_root(block_responses.keccak)` // 2 field elements, in hi-lo form -/// * `poseidon_tree_root(full_account_responses.poseidon)` // as a field element -/// * `keccak_tree_root(full_account_responses.keccak)` // 2 field elements, in hi-lo form -/// * `poseidon_tree_root(full_storage_response.poseidon)` // as a field element -/// * `keccak_tree_root(full_storage_response.keccak)` // 2 field elements, in hi-lo form -/// * `keccak256(abi.encodePacked(mmr[BLOCK_BATCH_DEPTH..]))` // 2 field elements, H256 in hi-lo form. -/// * `keccak256(abi.encodePacked(mmr[..BLOCK_BATCH_DEPTH]))` as 2 field elements, H256 in hi-lo form. -/// To be clear, `abi.encodedPacked(mmr[d..]) = mmr[d] . mmr[d + 1] . ... . mmr[mmr_num_peaks - 1]` where `.` is concatenation of byte arrays. -#[derive(Clone, Debug)] -pub struct FinalResponseAssemblyCircuit { - /// Snark with merklelized block responses, verified against MMR - pub column_block_snark: Snark, - /// Snark with merklelized account responses - pub column_account_snark: Snark, - /// Snark with merklelized storage responses - pub column_storage_snark: Snark, - /// Snark for checking consistency of each row of the table (block, account, storage) - pub row_consistency_snark: Snark, - /// True if `column_block_snark` was an aggregation circuit - pub column_block_has_accumulator: bool, - /// True if `column_account_snark` was an aggregation circuit - pub column_account_has_accumulator: bool, - /// True if `column_storage_snark` was an aggregation circuit - pub column_storage_has_accumulator: bool, - /// True if `row_consistency_snark` was an aggregation circuit - pub row_consistency_has_accumulator: bool, -} - -impl FinalResponseAssemblyCircuit { - pub fn new( - column_block: (Snark, bool), - column_account: (Snark, bool), - column_storage: (Snark, bool), - row_consistency: (Snark, bool), - ) -> Self { - Self { - column_block_snark: column_block.0, - column_account_snark: column_account.0, - column_storage_snark: column_storage.0, - row_consistency_snark: row_consistency.0, - column_block_has_accumulator: column_block.1, - column_account_has_accumulator: column_account.1, - column_storage_has_accumulator: column_storage.1, - row_consistency_has_accumulator: row_consistency.1, - } - } -} - -impl FinalResponseAssemblyCircuit { - fn create( - self, - stage: CircuitBuilderStage, - break_points: Option, - lookup_bits: usize, - params: &ParamsKZG, - ) -> EthCircuitBuilder> { - log::info!("New FinalResponseAggregationCircuit",); - // aggregate the snarks - let aggregation = AggregationCircuit::new::( - stage, - Some(Vec::new()), // break points aren't actually used, since we will just take the builder from this circuit - lookup_bits, - params, - [ - self.column_block_snark, - self.column_account_snark, - self.column_storage_snark, - self.row_consistency_snark, - ], - ); - let (block_instance, account_instance, storage_instance, row_consistency_instance) = - aggregation - .previous_instances - .iter() - .zip_eq([ - self.column_block_has_accumulator, - self.column_account_has_accumulator, - self.column_storage_has_accumulator, - self.row_consistency_has_accumulator, - ]) - .map(|(instance, has_accumulator)| { - let start = (has_accumulator as usize) * 4 * LIMBS; - &instance[start..] - }) - .collect_tuple() - .unwrap(); - - // TODO: should reuse RangeChip from aggregation circuit, but can't refactor right now - let range = RangeChip::default(lookup_bits); - let gate_builder = aggregation.inner.circuit.0.builder.take(); - let _chip = DummyEccChip(range.gate()); - let loader = Halo2Loader::::new(_chip, gate_builder); - - let mut keccak = KeccakChip::default(); - let mut poseidon = Poseidon::from_spec(&loader, POSEIDON_SPEC.clone()); - - let (block_instance, verify_mmr_instance) = - block_instance.split_at(block_instance.len() - 4); - let block_instance = merklelize_instances( - HashStrategy::Tree, - block_instance, - BLOCK_INSTANCE_SIZE - 4, // exclude mmrs - BLOCK_POSEIDON_ROOT_INDICES, - BLOCK_KECCAK_ROOT_INDICES, - &loader, - &mut poseidon, - &range, - &mut keccak, - ); - let account_instance = merklelize_instances( - HashStrategy::Onion, - account_instance, - ACCOUNT_INSTANCE_SIZE, - ACCOUNT_POSEIDON_ROOT_INDICES, - ACCOUNT_KECCAK_ROOT_INDICES, - &loader, - &mut poseidon, - &range, - &mut keccak, - ); - let storage_instance = merklelize_instances( - HashStrategy::Onion, - storage_instance, - STORAGE_INSTANCE_SIZE, - STORAGE_POSEIDON_ROOT_INDICES, - STORAGE_KECCAK_ROOT_INDICES, - &loader, - &mut poseidon, - &range, - &mut keccak, - ); - - let mut gate_builder = loader.take_ctx(); - let ctx = gate_builder.main(FIRST_PHASE); - // each root in row consistency circuit must match the corresponding root in the other column circuits - ctx.constrain_equal( - &row_consistency_instance[ROW_BLOCK_POSEIDON_INDEX], - &block_instance[BLOCK_RESPONSE_POSEIDON_INDEX], - ); - ctx.constrain_equal( - &row_consistency_instance[ROW_ACCT_POSEIDON_INDEX], - &account_instance[ACCOUNT_FULL_RESPONSE_POSEIDON_INDEX], - ); - ctx.constrain_equal( - &row_consistency_instance[ROW_ACCT_BLOCK_KECCAK_INDEX], - &account_instance[ACCOUNT_BLOCK_RESPONSE_KECCAK_INDEX], - ); - ctx.constrain_equal( - &row_consistency_instance[ROW_STORAGE_POSEIDON_INDEX], - &storage_instance[STORAGE_FULL_RESPONSE_POSEIDON_INDEX], - ); - ctx.constrain_equal( - &row_consistency_instance[ROW_STORAGE_BLOCK_KECCAK_INDEX], - &storage_instance[STORAGE_BLOCK_RESPONSE_KECCAK_INDEX], - ); - ctx.constrain_equal( - &row_consistency_instance[ROW_STORAGE_ACCT_KECCAK_INDEX], - &storage_instance[STORAGE_ACCOUNT_RESPONSE_KECCAK_INDEX], - ); - - // All computations are contained in the `aggregations`'s builder, so we take that to create a new RlcThreadBuilder - let builder = RlcThreadBuilder { threads_rlc: Vec::new(), gate_builder }; - let mut assigned_instances = aggregation.inner.assigned_instances; - - // add new public instances - assigned_instances.extend( - iter::once(block_instance[BLOCK_RESPONSE_POSEIDON_INDEX]) - .chain(block_instance[KECCAK_BLOCK_RESPONSE_INDEX..].iter().take(2).copied()) - .chain(iter::once(account_instance[ACCOUNT_FULL_RESPONSE_POSEIDON_INDEX])) - .chain( - account_instance[KECCAK_ACCOUNT_FULL_RESPONSE_INDEX..].iter().take(2).copied(), - ) - .chain(iter::once(storage_instance[STORAGE_FULL_RESPONSE_POSEIDON_INDEX])) - .chain( - storage_instance[KECCAK_STORAGE_FULL_RESPONSE_INDEX..].iter().take(2).copied(), - ) - .chain(verify_mmr_instance.iter().copied()), - ); - - EthCircuitBuilder::new( - assigned_instances, - builder, - RefCell::new(keccak), - range, - break_points, - |_: &mut RlcThreadBuilder, - _: RlpChip, - _: (FixedLenRLCs, VarLenRLCs)| {}, - ) - } - - pub fn create_circuit( - self, - stage: CircuitBuilderStage, - break_points: Option, - lookup_bits: usize, - params: &ParamsKZG, - ) -> EthCircuitBuilder> { - let circuit = self.create(stage, break_points, lookup_bits, params); - #[cfg(not(feature = "production"))] - if stage != CircuitBuilderStage::Prover { - let config_params: EthConfigParams = serde_json::from_str( - var("ETH_CONFIG_PARAMS").expect("ETH_CONFIG_PARAMS is not set").as_str(), - ) - .unwrap(); - circuit.config(config_params.degree as usize, Some(config_params.unusable_rows)); - } - circuit - } -} diff --git a/axiom-eth/src/batch_query/aggregation/merkle.rs b/axiom-eth/src/batch_query/aggregation/merkle.rs deleted file mode 100644 index dbe1e7bf..00000000 --- a/axiom-eth/src/batch_query/aggregation/merkle.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! Aggregation circuit involving both Poseidon and Keccak hashes. This is used for -//! aggregation of a single column response, from initial circuits such as -//! `Multi{Block,Account,Storage}Circuit`. - -use std::rc::Rc; - -use halo2_base::{ - gates::{RangeChip, RangeInstructions}, - halo2_proofs::halo2curves::CurveAffine, - AssignedValue, -}; -use itertools::Itertools; -use snark_verifier::{ - loader::halo2::{Halo2Loader, Scalar}, - util::hash::Poseidon, -}; - -use crate::{ - batch_query::{ - hash::{keccak_packed, poseidon_onion, poseidon_tree_root}, - response::FixedByteArray, - EccInstructions, - }, - keccak::KeccakChip, - util::{bytes_be_to_u128, u128s_to_bytes_be}, - Field, -}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum HashStrategy { - Tree, - Onion, -} - -/// Aggregates snarks and computes keccak and Poseidon Merkle roots of previous public instances. -/// -/// We assume public instances in previous `snarks`, other than old accumulators, come in repeating chunks of size `chunk_size`. -/// -/// Computes the Keccak Merkle roots -/// * `keccak_strategy[H256(instance[chunk_size * j + i .. chunk_size * j + i + 2]) for all j])` for each `i` in `keccak_indices`. -/// -/// and the Poseidon Merkle roots -/// * `poseidon_strategy([instance[chunk_size * j + i] for all j])` for each `i` in `poseidon_indices` -/// -/// where `H256([hi,lo])` assumes that `hi, lo` are `u128` and forms the 256-bit integer `hi << 128 | lo`. -/// -/// Here `keccak_strategy` means `keccak_merkle_root` if `strategy == Tree` or `keccak(a_0 . keccak(a_1 . keccak( ... )))` if `strategy == Onion`. -/// Similarly for `poseidon`. -/// -/// Returns: -/// * Poseidon roots (`poseidon_indices.len()` field elements) -/// * Keccak roots (`keccak_indices.len() * 2` field elements in hi-lo u128 form) -/// -/// # Panics -/// If `strategy == Tree` and `instance.len() / chunk_size` is not a power of 2. -#[allow(clippy::too_many_arguments)] -pub fn merklelize_instances( - strategy: HashStrategy, - instances: &[AssignedValue], - chunk_size: usize, - poseidon_indices: &[usize], - keccak_indices: &[usize], - loader: &Rc>, - poseidon: &mut Poseidon, T, RATE>, - range: &RangeChip, - keccak: &mut KeccakChip, -) -> Vec> -where - F: Field, - C: CurveAffine, - EccChip: EccInstructions, -{ - for idx in poseidon_indices.iter().chain(keccak_indices.iter()) { - assert!(*idx < chunk_size, "poseidon and keccak indices must be < chunk_size"); - } - let mut tmp_builder = loader.ctx_mut(); - // keccak tree root - let ctx = tmp_builder.main(0); - // get hi-lo u128s from previous instances and convert to bytes - let mut keccak_leaves = vec![vec![]; keccak_indices.len()]; - for chunk in instances.chunks_exact(chunk_size) { - for (leaves, &idx) in keccak_leaves.iter_mut().zip(keccak_indices.iter()) { - let hi_lo = &chunk[idx..idx + 2]; - leaves.push(u128s_to_bytes_be(ctx, range, hi_lo)); - } - } - // compute the keccak merkle roots - let keccak_roots = keccak_leaves - .iter() - .flat_map(|leaves| { - let bytes = match strategy { - HashStrategy::Tree => keccak.merkle_tree_root(ctx, range.gate(), leaves), - HashStrategy::Onion => { - let mut onion = FixedByteArray(leaves[0].clone()); - for leaf in &leaves[1..] { - onion = keccak_packed( - ctx, - range.gate(), - keccak, - FixedByteArray([onion.as_ref(), &leaf[..]].concat()), - ); - } - onion.0 - } - }; - bytes_be_to_u128(ctx, range.gate(), &bytes) - }) - .collect_vec(); - debug_assert_eq!(keccak_roots.len(), keccak_indices.len() * 2); - drop(tmp_builder); - - // compute the poseidon merkle roots - // load field elements from prev instances to Scalar - let mut poseidon_leaves = vec![vec![]; poseidon_indices.len()]; - for chunk in instances.chunks_exact(chunk_size) { - for (leaves, &idx) in poseidon_leaves.iter_mut().zip(poseidon_indices.iter()) { - leaves.push(loader.scalar_from_assigned(chunk[idx])); - } - } - let poseidon_roots = poseidon_leaves - .into_iter() - .map(|leaves| match strategy { - HashStrategy::Tree => poseidon_tree_root(poseidon, leaves, &[]).into_assigned(), - HashStrategy::Onion => { - poseidon_onion(poseidon, leaves.into_iter().map(|leaf| leaf.into())).into_assigned() - } - }) - .collect_vec(); - - [keccak_roots, poseidon_roots].concat() -} diff --git a/axiom-eth/src/batch_query/aggregation/mod.rs b/axiom-eth/src/batch_query/aggregation/mod.rs deleted file mode 100644 index 4ac5502e..00000000 --- a/axiom-eth/src/batch_query/aggregation/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod final_response; -mod merkle; -mod poseidon; - -pub use final_response::*; -pub use merkle::*; -pub use poseidon::*; diff --git a/axiom-eth/src/batch_query/aggregation/poseidon.rs b/axiom-eth/src/batch_query/aggregation/poseidon.rs deleted file mode 100644 index 69ea99eb..00000000 --- a/axiom-eth/src/batch_query/aggregation/poseidon.rs +++ /dev/null @@ -1,193 +0,0 @@ -//! Aggregation circuits involving only Poseidon hashes, used for aggregation of -//! `RowConsistencyCircuit` and `VerifyVsMmrCircuit`. - -use halo2_base::{ - gates::{ - builder::{CircuitBuilderStage, MultiPhaseThreadBreakPoints}, - GateChip, - }, - halo2_proofs::{ - halo2curves::bn256::{Bn256, G1Affine}, - poly::kzg::commitment::ParamsKZG, - }, -}; -use itertools::Itertools; -use snark_verifier::{loader::halo2::Halo2Loader, util::hash::Poseidon}; -use snark_verifier_sdk::{ - halo2::{aggregation::AggregationCircuit, POSEIDON_SPEC}, - Snark, LIMBS, -}; - -use crate::{ - batch_query::{ - hash::{poseidon_onion, poseidon_tree_root}, - DummyEccChip, - }, - rlp::rlc::FIRST_PHASE, - util::circuit::PublicAggregationCircuit, - AggregationPreCircuit, -}; - -use super::HashStrategy; - -/// Aggregates snarks and computes *possibly multiple* Poseidon Merkle roots of previous public instances. -/// -/// See [`PublicAggregationCircuit`] for `snarks` format. -/// -/// Assumes public instances of previous `snarks`, excluding old accumulators, come in tuples of `num_roots` field elements. -/// -/// The circuit concatenates all previous public instances, aside from old accumulators, into `instance` and computes `num_roots` Poseidon Merkle roots. -/// * `i`th Poseidon Merkle root is computed from `instance[j * num_roots + i]` for all `j` -/// -/// Public instances of the circuit are the accumulators, followed by: -/// * `num_roots` Poseidon roots (each a single field element) -// -// This is same as `MerkleAggregationCircuit`, but with empty `keccak_indices`. This means we don't need `KeccakChip`. -#[derive(Clone, Debug)] -pub struct PoseidonAggregationCircuit { - pub strategy: HashStrategy, - pub snarks: Vec<(Snark, bool)>, - pub num_roots: usize, -} - -impl PoseidonAggregationCircuit { - pub fn new(strategy: HashStrategy, snarks: Vec<(Snark, bool)>, num_roots: usize) -> Self { - assert!(!snarks.is_empty(), "no snarks to aggregate"); - let mut total_instances = 0; - for (snark, has_acc) in &snarks { - let start = (*has_acc as usize) * 4 * LIMBS; - let n = snark.instances.iter().map(|x| x.len()).sum::() - start; - assert_eq!(n % num_roots, 0, "snark does not have correct number of instances"); - total_instances += n; - } - let num_leaves = total_instances / num_roots; - assert!(num_leaves > 0, "no leaves to merklelize"); - Self { strategy, snarks, num_roots } - } -} - -impl AggregationPreCircuit for PoseidonAggregationCircuit { - fn create( - self, - stage: CircuitBuilderStage, - break_points: Option, - lookup_bits: usize, - params: &ParamsKZG, - ) -> AggregationCircuit { - log::info!( - "New PoseidonAggregationCircuit | num_snarks: {} | num_roots: {}", - self.snarks.len(), - self.num_roots - ); - // aggregate the snarks - let mut aggregation = PublicAggregationCircuit::new(self.snarks).private( - stage, - break_points, - lookup_bits, - params, - ); - let previous_instances = &aggregation.previous_instances; - - let builder = aggregation.inner.circuit.0.builder.take(); - // TODO: should reuse GateChip from aggregation circuit, but can't refactor right now - let gate = GateChip::default(); - let _chip = DummyEccChip(&gate); - let loader = Halo2Loader::::new(_chip, builder); - // load field elements from prev instances to Scalar - let mut poseidon_leaves = vec![vec![]; self.num_roots]; - for prev_instance in previous_instances { - for hashes in prev_instance.chunks_exact(self.num_roots) { - for (hash, poseidon_leaves) in hashes.iter().zip_eq(poseidon_leaves.iter_mut()) { - poseidon_leaves.push(loader.scalar_from_assigned(*hash)); - } - } - } - let mut poseidon = Poseidon::from_spec(&loader, POSEIDON_SPEC.clone()); - let poseidon_roots = poseidon_leaves - .into_iter() - .map(|leaves| match self.strategy { - HashStrategy::Tree => { - poseidon_tree_root(&mut poseidon, leaves, &[]).into_assigned() - } - HashStrategy::Onion => { - poseidon_onion(&mut poseidon, leaves.into_iter().map(|leaf| leaf.into())) - .into_assigned() - } - }) - .collect_vec(); - // put builder back - aggregation.inner.circuit.0.builder.replace(loader.take_ctx()); - // add new public instances - aggregation.inner.assigned_instances.extend(poseidon_roots); - - aggregation - } -} - -// Special circuit, not worth generalizing to avoid confusion: - -/// Special circuit just for aggregating [`crate::batch_query::response::block_header::MultiBlockCircuit`] -/// -/// Assumes public instances of previous `snarks`, excluding old accumulators, have the form: -/// * ... -/// * `historical_mmr_keccak`: an H256, 2 field elements in hi-lo form -/// * `recent_mmr_keccak`: an H256, 2 field elements in hi-lo form -/// -/// The circuit passes through all previous public instances, excluding accumulators **and** excluding `historical_mmr_keccak` and `recent_mmr_keccak`. -/// -/// The circuit constrains that `historical_mmr_keccak[i] = historical_mmr_keccak[j]` and `recent_mmr_keccak[i] = recent_mmr_keccak[j]` for all `i, j` -/// -/// Public instances of the circuit is the accumulator, followed by: -/// * ...Pass through previous instances -/// * `historical_mmr_keccak` -/// * `recent_mmr_keccak` -#[derive(Clone, Debug)] -pub struct MultiBlockAggregationCircuit { - pub snarks: Vec<(Snark, bool)>, -} - -impl MultiBlockAggregationCircuit { - pub fn new(snarks: Vec<(Snark, bool)>) -> Self { - Self { snarks } - } -} - -impl AggregationPreCircuit for MultiBlockAggregationCircuit { - fn create( - self, - stage: CircuitBuilderStage, - break_points: Option, - lookup_bits: usize, - params: &ParamsKZG, - ) -> AggregationCircuit { - log::info!("New VerifyVsMmrAggregationCircuit | num_snarks: {}", self.snarks.len(),); - // aggregate the snarks - let mut aggregation = PublicAggregationCircuit::new(self.snarks).private( - stage, - break_points, - lookup_bits, - params, - ); - let previous_instances = &aggregation.previous_instances; - let len0 = previous_instances[0].len(); - - let mut builder = aggregation.inner.circuit.0.builder.borrow_mut(); - let ctx = builder.main(FIRST_PHASE); - for prev_instance in previous_instances.iter().skip(1) { - let len = prev_instance.len(); - for i in 1..=4 { - ctx.constrain_equal(&previous_instances[0][len0 - i], &prev_instance[len - i]); - } - } - drop(builder); - // add new public instances - aggregation.inner.assigned_instances.extend( - previous_instances - .iter() - .flat_map(|instance| instance[..instance.len() - 4].to_vec()) - .chain(previous_instances[0][len0 - 4..].to_vec()), - ); - - aggregation - } -} diff --git a/axiom-eth/src/batch_query/hash/keccak.rs b/axiom-eth/src/batch_query/hash/keccak.rs deleted file mode 100644 index 5c0ecee3..00000000 --- a/axiom-eth/src/batch_query/hash/keccak.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Helper module for doing Keccak hashes -use crate::{ - batch_query::{response::FixedByteArray, EccInstructions}, - keccak::KeccakChip, - Field, -}; -use ethers_core::utils::keccak256; -use halo2_base::{gates::GateInstructions, AssignedValue, Context}; -use lazy_static::lazy_static; - -lazy_static! { - static ref KECCAK_EMPTY_STRING: [u8; 32] = keccak256([]); -} - -pub fn keccak_packed( - ctx: &mut Context, - gate: &impl GateInstructions, - keccak: &mut KeccakChip, - words: FixedByteArray, -) -> FixedByteArray { - FixedByteArray(if words.0.is_empty() { - KECCAK_EMPTY_STRING - .iter() - .map(|b| ctx.load_witness(gate.get_field_element(*b as u64))) - .collect() - } else { - let hash_id = keccak.keccak_fixed_len(ctx, gate, words.0, None); - keccak.fixed_len_queries[hash_id].output_assigned.clone() - }) -} - -/// Assumes that `sel` is a bit (either 0 or 1). -/// Returns `bytes` if `sel` is 1, otherwise replaces every byte in `bytes` with 0. -pub(crate) fn bytes_select_or_zero( - ctx: &mut Context, - gate: &impl GateInstructions, - mut bytes: FixedByteArray, - sel: AssignedValue, -) -> FixedByteArray { - for byte in bytes.0.iter_mut() { - *byte = gate.mul(ctx, *byte, sel); - } - bytes -} diff --git a/axiom-eth/src/batch_query/hash/mod.rs b/axiom-eth/src/batch_query/hash/mod.rs deleted file mode 100644 index 1baa38b7..00000000 --- a/axiom-eth/src/batch_query/hash/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! We need to compute hash concentation and Merkle tree root for Poseidon and Keccak, -//! both in Halo2 and Native (Rust) -//! -//! We use the [`snark-verifier`] implementation of Poseidon which uses the `Loader` trait -//! to deal with Halo2 and Native loader simultaneously. - -mod keccak; -mod poseidon; -pub use keccak::*; -pub use poseidon::*; diff --git a/axiom-eth/src/batch_query/hash/poseidon.rs b/axiom-eth/src/batch_query/hash/poseidon.rs deleted file mode 100644 index 03fd1739..00000000 --- a/axiom-eth/src/batch_query/hash/poseidon.rs +++ /dev/null @@ -1,352 +0,0 @@ -use std::rc::Rc; - -use halo2_base::{ - halo2_proofs::halo2curves::{bn256::Fr, CurveAffine, FieldExt}, - AssignedValue, -}; -use itertools::Itertools; -use lazy_static::lazy_static; -use snark_verifier::{ - loader::{ - halo2::{Halo2Loader, Scalar}, - LoadedScalar, ScalarLoader, - }, - util::hash::Poseidon, -}; -use snark_verifier_sdk::{halo2::POSEIDON_SPEC, NativeLoader}; - -use crate::batch_query::EccInstructions; - -use self::shim::SelectLoader; - -lazy_static! { - pub static ref POSEIDON_EMPTY_ROOTS: Vec = generate_poseidon_empty_roots(32); -} - -/// An array of field elements that can be concatenated and hashed by Poseidon hasher. -/// Assumed to be of known fixed length. -#[derive(Clone, Debug)] -pub struct PoseidonWords(pub(crate) Vec); - -impl From for PoseidonWords { - fn from(x: L) -> Self { - Self(vec![x]) - } -} - -impl From> for PoseidonWords { - fn from(x: Option) -> Self { - Self(x.into_iter().collect()) - } -} - -impl AsRef<[L]> for PoseidonWords { - fn as_ref(&self) -> &[L] { - &self.0 - } -} - -impl PoseidonWords { - pub fn concat(&self, other: &Self) -> Self { - Self([&self.0[..], &other.0[..]].concat()) - } -} - -impl> PoseidonWords> { - pub fn from_witness( - loader: &Rc>, - witness: impl AsRef<[C::Scalar]>, - ) -> Self { - Self(witness.as_ref().iter().map(|x| loader.assign_scalar(*x)).collect()) - } -} - -/// A struct for an array of field elements that is either: -/// * of known fixed length `N`, or -/// * empty -/// If `is_some` is `None`, then the array is assumed to be of known fixed length `N`, in which case this is the same as `PoseidonWords`. -/// Otherwise `is_some` is a boolean value that indicates whether the array is empty or not. -/// In the case `is_some = Some(0)`, then `words` should still be a dummy array of the known fixed length `N`. -#[derive(Clone, Debug)] -pub struct OptPoseidonWords { - pub(crate) words: Vec, - pub(crate) is_some: Option, -} - -impl From for OptPoseidonWords { - fn from(x: L) -> Self { - Self { words: vec![x], is_some: None } - } -} - -impl From> for OptPoseidonWords { - fn from(x: PoseidonWords) -> Self { - Self { words: x.0, is_some: None } - } -} - -/// Computes the Poseidon hash of an array of field elements by adding them sequentially to buffer -/// and then squeezing once. -pub fn poseidon_packed, const T: usize, const RATE: usize>( - hasher: &mut Poseidon, - words: PoseidonWords, -) -> L { - hasher.clear(); // reset state - hasher.update(&words.0); - hasher.squeeze() -} - -pub(crate) fn poseidon_onion, const T: usize, const RATE: usize>( - hasher: &mut Poseidon, - leaves: impl IntoIterator>, -) -> L { - let mut leaves = leaves.into_iter(); - let mut onion = leaves.next().expect("leaves must be non-empty"); - for leaf in leaves { - onion = poseidon_packed(hasher, onion.concat(&leaf)).into(); - } - onion.0[0].clone() -} - -/// Computes the Poseidon Merkle root of a tree with leaves `leaves` -/// where each leaf is a either (1) a fixed length array of words or (2) empty array. -/// -/// The hash of two leaves is computed by concatenating the leaves and hashing the concatenation. -/// If there is a single leaf, we hash the leaf if it has length > 1, otherwise we return the leaf itself. -/// We assume the input is never a single leaf of the empty array. -/// -/// Returns the Merkle tree root as a single field element. -/// -/// Assumes `leaves` is non-empty. If `leaves` has length 1, then the leaf must be a fixed length array; in this case -/// we hash the leaf if it has length > 1, otherwise we return the leaf itself. -/// Does not assume `leaves.len()` is a power of two. If it is not, the tree is padded with leaves that are empty arrays. -/// -/// As an optimization, we pass in pre-computed empty Poseidon Merkle roots, where `poseidon_empty_roots[i]` -/// is the root of a tree of height `i + 1` with all empty leaves (so `poseidon_empty_roots[0] = poseidon([])`). -/// This function will panic if `poseidon_empty_roots.len()` < log2_floor( 2log2_ceil(leaves.len()) - leaves.len()). -// We could optimize even more by keeping a cache of the assigned constants for `poseidon_empty_roots`, -// but we'll avoid increasing code. -pub(crate) fn poseidon_tree_root( - hasher: &mut Poseidon, - leaves: Vec, - poseidon_empty_roots: &[F], -) -> L -where - F: FieldExt, - L: LoadedScalar, - L::Loader: SelectLoader, - W: Into>, -{ - let mut len = leaves.len(); - assert!(len > 0, "leaves must be non-empty"); - - if len == 1 { - let leaf: OptPoseidonWords<_> = leaves.into_iter().next().unwrap().into(); - assert!(leaf.is_some.is_none(), "single leaf must be fixed length array"); - let words = leaf.words; - assert!(!words.is_empty()); - if words.len() == 1 { - return words.into_iter().next().unwrap(); - } else { - return poseidon_packed(hasher, PoseidonWords(words)); - } - } - - let mut hashes = Vec::with_capacity((len + 1) / 2); - for mut pair in leaves.into_iter().chunks(2).into_iter() { - let left: OptPoseidonWords = pair.next().unwrap().into(); - let right: OptPoseidonWords = - pair.next().map(Into::into).unwrap_or_else(|| PoseidonWords(vec![]).into()); - hashes.push(poseidon_opt_pair(hasher, left, right)); - } - - len = (len + 1) / 2; - debug_assert_eq!(len, hashes.len()); - let mut level = 0; - while len > 1 { - for i in 0..(len + 1) / 2 { - let concat = if 2 * i + 1 < len { - vec![hashes[2 * i].clone(), hashes[2 * i + 1].clone()] - } else { - let empty_root = hashes[2 * i].loader().load_const( - poseidon_empty_roots.get(level).expect("poseidon_empty_roots too short"), - ); - vec![hashes[2 * i].clone(), empty_root] - }; - hashes[i] = poseidon_packed(hasher, PoseidonWords(concat)); - } - len = (len + 1) / 2; - level += 1; - } - hashes.into_iter().next().unwrap() -} - -/// Computes poseidon(left, right), taking into account the possibility that either left or right may be empty. -pub fn poseidon_opt_pair( - hasher: &mut Poseidon, - left: OptPoseidonWords, - right: OptPoseidonWords, -) -> L -where - F: FieldExt, - L: LoadedScalar, - L::Loader: SelectLoader, -{ - if let Some(is_some) = left.is_some { - let some_any = poseidon_opt_pair(hasher, PoseidonWords(left.words).into(), right.clone()); - let none_any = poseidon_opt_pair(hasher, PoseidonWords(vec![]).into(), right); - let loader = is_some.loader().clone(); - loader.select(some_any, none_any, is_some) - } else if let Some(is_some) = right.is_some { - let left = PoseidonWords(left.words); - let some_some = poseidon_packed(hasher, left.concat(&PoseidonWords(right.words))); - let some_none = poseidon_packed(hasher, left); - let loader = is_some.loader().clone(); - loader.select(some_some, some_none, is_some) - } else { - poseidon_packed(hasher, PoseidonWords([&left.words[..], &right.words[..]].concat())) - } -} - -/// Creates a Merkle proof proving inclusion of node `leaves[index]` into a tree with leaves `leaves`. -/// Assumes `leaves.len()` is a power of two. -pub fn create_merkle_proof( - hasher: &mut Poseidon, - leaves: Vec>, - index: usize, -) -> Vec> -where - F: FieldExt, - L: LoadedScalar, -{ - let mut len = leaves.len(); - assert!(len.is_power_of_two()); - let mut proof = Vec::with_capacity(len.ilog2() as usize); - let mut idx = index; - let mut current_hashes = leaves; - while len > 1 { - proof.push(current_hashes[idx ^ 1].clone()); - for i in 0..len / 2 { - current_hashes[i] = - poseidon_packed(hasher, current_hashes[2 * i].concat(¤t_hashes[2 * i + 1])) - .into(); - } - idx >>= 1; - len /= 2; - } - proof -} - -/// Computes the Poseidon Merkle root by traversing the Merkle proof. -pub fn traverse_merkle_proof( - hasher: &mut Poseidon, - proof: &[PoseidonWords], - leaf: PoseidonWords, - side: usize, -) -> PoseidonWords -where - F: FieldExt, - L: LoadedScalar, -{ - let mut current_hash = leaf; - for (i, node) in proof.iter().enumerate() { - if (side >> i) & 1 == 0 { - current_hash = poseidon_packed(hasher, current_hash.concat(node)).into(); - } else { - current_hash = poseidon_packed(hasher, node.concat(¤t_hash)).into(); - } - } - current_hash -} - -/// Assumes that `sel` is a bit (either 0 or 1). -/// Returns `word` if `sel` is 1, otherwise returns 0. -pub(crate) fn word_select_or_zero( - loader: &Rc>, - word: Scalar, - sel: AssignedValue, -) -> Scalar -where - F: FieldExt, - C: CurveAffine, - EccChip: EccInstructions, -{ - let sel = loader.scalar_from_assigned(sel); - word * &sel -} - -fn generate_poseidon_empty_roots(len: usize) -> Vec { - let mut hasher = Poseidon::from_spec(&NativeLoader, POSEIDON_SPEC.clone()); - let mut empty_roots = Vec::with_capacity(len); - empty_roots.push(poseidon_packed(&mut hasher, PoseidonWords(vec![]))); - for _ in 1..len { - let last = *empty_roots.last().unwrap(); - empty_roots.push(poseidon_packed(&mut hasher, PoseidonWords(vec![last, last]))); - } - empty_roots -} - -mod shim { - use snark_verifier::loader::ScalarLoader; - - pub trait SelectLoader: ScalarLoader + Clone { - fn select( - &self, - if_true: Self::LoadedScalar, - if_false: Self::LoadedScalar, - cond: Self::LoadedScalar, - ) -> Self::LoadedScalar; - } - - mod halo2_lib { - use crate::batch_query::EccInstructions; - use std::rc::Rc; - - use crate::{rlp::rlc::FIRST_PHASE, Field}; - use halo2_base::{gates::GateInstructions, halo2_proofs::halo2curves::CurveAffine}; - use snark_verifier::loader::halo2::Halo2Loader; - - use super::SelectLoader; - - impl SelectLoader for Rc> - where - C: CurveAffine, - C::Scalar: Field, - EccChip: EccInstructions, - { - fn select( - &self, - if_true: Self::LoadedScalar, - if_false: Self::LoadedScalar, - cond: Self::LoadedScalar, - ) -> Self::LoadedScalar { - let mut builder = self.ctx_mut(); - let ctx = builder.main(FIRST_PHASE); - let out = self.scalar_chip().select( - ctx, - if_true.into_assigned(), - if_false.into_assigned(), - cond.into_assigned(), - ); - self.scalar_from_assigned(out) - } - } - } - - mod native { - use halo2_base::halo2_proofs::halo2curves::FieldExt; - use snark_verifier_sdk::NativeLoader; - - use super::SelectLoader; - - impl SelectLoader for NativeLoader { - fn select(&self, if_true: F, if_false: F, cond: F) -> F { - if bool::from(cond.is_zero()) { - if_false - } else { - if_true - } - } - } - } -} diff --git a/axiom-eth/src/batch_query/mod.rs b/axiom-eth/src/batch_query/mod.rs deleted file mode 100644 index ade005b1..00000000 --- a/axiom-eth/src/batch_query/mod.rs +++ /dev/null @@ -1,92 +0,0 @@ -use halo2_base::{ - gates::{builder::GateThreadBuilder, GateChip}, - halo2_proofs::halo2curves::CurveAffine, - utils::{BigPrimeField, ScalarField}, - AssignedValue, -}; - -#[cfg(feature = "aggregation")] -pub mod aggregation; -pub mod hash; -pub mod response; -#[cfg(feature = "aggregation")] -pub mod scheduler; - -#[cfg(feature = "providers")] -#[cfg(test)] -mod tests; - -pub trait EccInstructions = - snark_verifier::loader::halo2::EccInstructions< - C, - Context = GateThreadBuilder, - ScalarChip = GateChip, - AssignedScalar = AssignedValue, - >; - -/// We unfortunately need Halo2Loader to use Poseidon in circuit, which requires `EccChip: EccInstructions`. -/// Since we don't actually use the EccInstructions, we make a dummy chip wrapping just `GateChip`. -#[derive(Clone, Debug)] -pub(crate) struct DummyEccChip<'a, C: CurveAffine>(&'a GateChip) -where - C::ScalarExt: ScalarField; - -impl<'a, C: CurveAffine> snark_verifier::loader::halo2::EccInstructions for DummyEccChip<'a, C> -where - C::ScalarExt: BigPrimeField, -{ - type Context = GateThreadBuilder; - type ScalarChip = GateChip; - type AssignedScalar = AssignedValue; - type AssignedCell = AssignedValue; - type AssignedEcPoint = (); - - fn scalar_chip(&self) -> &Self::ScalarChip { - self.0 - } - - fn assign_constant(&self, _: &mut Self::Context, _: C) -> Self::AssignedEcPoint { - unreachable!(); - } - - fn assign_point(&self, _: &mut Self::Context, _: C) -> Self::AssignedEcPoint { - unreachable!(); - } - - fn sum_with_const( - &self, - _: &mut Self::Context, - _: &[impl lazy_static::__Deref], - _: C, - ) -> Self::AssignedEcPoint { - unreachable!(); - } - - fn fixed_base_msm( - &mut self, - _: &mut Self::Context, - _: &[(impl lazy_static::__Deref, C)], - ) -> Self::AssignedEcPoint { - unreachable!(); - } - - fn variable_base_msm( - &mut self, - _: &mut Self::Context, - _: &[( - impl lazy_static::__Deref, - impl lazy_static::__Deref, - )], - ) -> Self::AssignedEcPoint { - unreachable!(); - } - - fn assert_equal( - &self, - _: &mut Self::Context, - _: &Self::AssignedEcPoint, - _: &Self::AssignedEcPoint, - ) { - unreachable!(); - } -} diff --git a/axiom-eth/src/batch_query/response/account.rs b/axiom-eth/src/batch_query/response/account.rs deleted file mode 100644 index bfa88c11..00000000 --- a/axiom-eth/src/batch_query/response/account.rs +++ /dev/null @@ -1,374 +0,0 @@ -//! Account Response -use super::*; -use crate::{ - batch_query::{ - hash::{ - bytes_select_or_zero, keccak_packed, poseidon_packed, poseidon_tree_root, - word_select_or_zero, - }, - response::storage::DEFAULT_STORAGE_QUERY, - DummyEccChip, - }, - keccak::{FixedLenRLCs, FnSynthesize, KeccakChip, VarLenRLCs}, - rlp::{ - builder::{RlcThreadBreakPoints, RlcThreadBuilder}, - rlc::FIRST_PHASE, - RlpChip, - }, - storage::{ - EthAccountTraceWitness, EthStorageChip, EthStorageInput, ACCOUNT_PROOF_MAX_DEPTH, - ACCOUNT_STATE_FIELD_IS_VAR_LEN, - }, - util::{bytes_be_to_u128, load_bool}, - EthChip, EthCircuitBuilder, EthPreCircuit, Field, ETH_LOOKUP_BITS, -}; -use ethers_core::types::Address; -#[cfg(feature = "providers")] -use ethers_providers::{JsonRpcClient, Provider}; -use halo2_base::{ - gates::{GateInstructions, RangeChip, RangeInstructions}, - halo2_proofs::halo2curves::bn256::G1Affine, - utils::ScalarField, - Context, -}; -use itertools::Itertools; -use lazy_static::lazy_static; -use serde::{Deserialize, Serialize}; -use snark_verifier::util::hash::Poseidon; -use snark_verifier_sdk::halo2::POSEIDON_SPEC; -use std::cell::RefCell; - -pub(crate) const STORAGE_ROOT_INDEX: usize = 2; - -/// | Account State Field | Max bytes | -/// |-------------------------|-------------| -/// | nonce | ≤8 | -/// | balance | ≤12 | -/// | storageRoot | 32 | -/// | codeHash | 32 | -/// -/// Struct that stores account state fields as an array of fixed length byte arrays. -/// For fields with variable length byte arrays, the byte arrays are left padded with 0s to the max fixed length. -#[derive(Clone, Debug)] -pub struct AccountState(Vec>); - -impl AccountState { - pub fn keccak( - &self, - ctx: &mut Context, - gate: &impl GateInstructions, - keccak: &mut KeccakChip, - ) -> FixedByteArray { - keccak_packed( - ctx, - gate, - keccak, - FixedByteArray(self.0.iter().map(|bytes| bytes.0.to_vec()).concat()), - ) - } -} - -/// A single response to an account query. -/// -/// | Field | Max bytes | -/// |-------------------------|-------------| -/// | stateRoot | 32 | -/// | address | 20 | -/// | accountState | | -/// -/// ``` -/// account_response = hash(stateRoot . address . hash_tree_root(account_state)) -/// ``` -/// This struct stores all the data necessary to compute the above hash. -#[derive(Clone, Debug)] -pub struct AccountResponse { - pub state_root: FixedByteArray, - pub address: FixedByteArray, - pub account_state: AccountState, -} - -impl AccountResponse { - pub fn from_witness( - witness: &EthAccountTraceWitness, - ctx: &mut Context, - gate: &impl GateInstructions, - ) -> Self { - let state_root = FixedByteArray(witness.mpt_witness.root_hash_bytes.clone()); - let address = FixedByteArray(witness.address.clone()); - let account_state = AccountState( - witness - .array_witness - .field_witness - .iter() - .enumerate() - .map(|(i, field)| { - if ACCOUNT_STATE_FIELD_IS_VAR_LEN[i] { - let field: ByteArray = field.into(); - field.to_fixed(ctx, gate) - } else { - field.into() - } - }) - .collect_vec(), - ); - Self { address, state_root, account_state } - } - - pub fn poseidon( - &self, - loader: &Rc>, - poseidon: &mut Poseidon, T, RATE>, - ) -> Scalar - where - F: Field, - C: CurveAffine, - EccChip: EccInstructions, - { - let account_state = - self.account_state.0.iter().map(|x| x.to_poseidon_words(loader)).collect_vec(); - // Uses fact that account_state length is power of 2 - let account_state_hash = poseidon_tree_root(poseidon, account_state, &[]); - let [state_root, address] = - [&self.state_root, &self.address].map(|x| x.to_poseidon_words(loader)); - poseidon_packed(poseidon, state_root.concat(&address).concat(&account_state_hash.into())) - } -} - -/// See [`MultiAccountCircuit`] for more details. -/// -/// Assumptions: -/// * `block_responses`, `account_responses`, `not_empty` are all of the same length, which is a **power of two**. -/// -/// Returns `(keccak_tree_root(full_account_responses.keccak), account_responses.keccak)` -pub fn get_account_response_keccak_root<'a, F: Field>( - ctx: &mut Context, - gate: &impl GateInstructions, - keccak: &mut KeccakChip, - block_numbers: impl IntoIterator>, - account_responses: impl IntoIterator>, - not_empty: impl IntoIterator>, -) -> FixedByteArray { - let full_responses: Vec<_> = block_numbers - .into_iter() - .zip_eq(account_responses) - .zip_eq(not_empty) - .map(|((bytes, account), not_empty)| { - let keccak_account_state = account.account_state.keccak(ctx, gate, keccak); - let hash = keccak_packed( - ctx, - gate, - keccak, - bytes.concat(&account.address).concat(&keccak_account_state), - ); - bytes_select_or_zero(ctx, gate, hash, not_empty).0 - }) - .collect(); - let keccak_root = keccak.merkle_tree_root(ctx, gate, &full_responses); - FixedByteArray(bytes_be_to_u128(ctx, gate, &keccak_root)) -} - -/// See [`MultiAccountCircuit`] for more details. -/// -/// Assumptions: -/// * `block_responses`, `account_responses`, `not_empty` are all of the same length, which is a **power of two**. -pub fn get_account_response_poseidon_roots( - loader: &Rc>, - poseidon: &mut Poseidon, T, RATE>, - block_responses: Vec<(F, FixedByteArray)>, - account_responses: &[AccountResponse], - not_empty: Vec>, -) -> Vec> -where - F: Field, - C: CurveAffine, - EccChip: EccInstructions, -{ - let (block_responses_keccak_hi_lo, full_responses): (Vec<_>, Vec<_>) = block_responses - .into_iter() - .zip_eq(account_responses.iter()) - .zip_eq(not_empty) - .map(|(((word, bytes), account), not_empty)| { - let account_hash = account.poseidon(loader, poseidon); - let word = loader.assign_scalar(word); - let block_response_keccak_hi_lo = bytes.to_poseidon_words(loader); - - let hash = poseidon_packed(poseidon, PoseidonWords(vec![word, account_hash])); - ( - block_response_keccak_hi_lo, - PoseidonWords::from(word_select_or_zero(loader, hash, not_empty)), - ) - }) - .unzip(); - - let [poseidon_root, block_response_root] = [full_responses, block_responses_keccak_hi_lo] - .map(|leaves| poseidon_tree_root(poseidon, leaves, &[]).into_assigned()); - vec![poseidon_root, block_response_root] -} - -// switching to just Fr for simplicity: - -/// The input datum for the circuit to generate multiple account responses. It is used to generate a circuit. -/// -/// Assumptions: -/// * `block_responses`, `queries`, `not_empty` are all of the same length, which is a **power of two**. -/// * `block_responses` has length greater than 1: the length 1 case still works but cannot be aggregated because -/// the single leaf of `block_responses[0].1` would get hashed as two words, whereas in a larger tree it gets -/// concatenated before hashing. -/// -/// The public instances of this circuit are 5 field elements: -/// * Keccak merkle tree root of `keccak(block_number[i] . address[i] . keccak_account_state[i])` over all queries: two field elements in hi-lo u128 format -/// * Poseidon merkle tree root of `full_response[i].poseidon := poseidon(block_responses[i].0 . account_responses[i].0)` over all queries: single field element -/// * Poseidon merkle tree root of `block_number[i]` over all queries: single field element -/// -/// Above `account_responses` refers to the hash of `AccountResponse`s generated by the circuit for all queries. -/// Since `block_number`s are given as private inputs, we need to expose a *Poseidon* merkle root of all `block_number`s to be checked again the BlockResponses. -// For compatibility with aggregation we keep all the poseidon roots together in the instance -#[derive(Clone, Debug, Hash, Serialize, Deserialize)] -pub struct MultiAccountCircuit { - /// The block responses are provided as UNCHECKED private inputs; they will be checked in a separate circuit - pub block_responses: Vec<(Fr, u32)>, - /// The account queries - pub queries: Vec, // re-use EthStorageInput but storage pf will be empty - /// Private input to allow full_response[i].hash to be `Fr::zero()` or `H256(0x0)` for empty response - pub not_empty: Vec, -} - -pub const ACCOUNT_INSTANCE_SIZE: usize = 4; -pub(crate) const KECCAK_ACCOUNT_FULL_RESPONSE_INDEX: usize = 0; -pub(crate) const ACCOUNT_FULL_RESPONSE_POSEIDON_INDEX: usize = 2; -pub(crate) const ACCOUNT_BLOCK_RESPONSE_KECCAK_INDEX: usize = 3; -pub(crate) const ACCOUNT_POSEIDON_ROOT_INDICES: &[usize] = - &[ACCOUNT_FULL_RESPONSE_POSEIDON_INDEX, ACCOUNT_BLOCK_RESPONSE_KECCAK_INDEX]; -pub(crate) const ACCOUNT_KECCAK_ROOT_INDICES: &[usize] = &[KECCAK_ACCOUNT_FULL_RESPONSE_INDEX]; - -impl MultiAccountCircuit { - /// Creates circuit inputs from raw data and does basic input validation. Number of queries must be power of two. - pub fn new( - block_responses: Vec<(Fr, u32)>, - queries: Vec, - not_empty: Vec, - ) -> Self { - assert!(block_responses.len() > 1); - assert_eq!(block_responses.len(), queries.len()); - assert_eq!(queries.len(), not_empty.len()); - assert!(queries.len().is_power_of_two(), "Number of queries must be a power of 2"); - Self { block_responses, queries, not_empty } - } - - /// Creates circuit inputs from a JSON-RPC provider. - #[cfg(feature = "providers")] - pub fn from_provider( - provider: &Provider

, - block_responses: Vec<(Fr, u32)>, - queries: Vec<(u64, Address)>, - not_empty: Vec, - ) -> Self { - use crate::providers::get_account_queries; - let queries = get_account_queries(provider, queries, ACCOUNT_PROOF_MAX_DEPTH); - Self::new(block_responses, queries, not_empty) - } - - /// Resizes inputs to `new_len` queries, using [`DEFAULT_ACCOUNT_QUERY`] for new queries. - pub fn resize_from( - mut block_responses: Vec<(Fr, u32)>, - mut queries: Vec, - mut not_empty: Vec, - new_len: usize, - ) -> Self { - block_responses.resize(new_len, (Fr::zero(), 0)); - queries.resize_with(new_len, || DEFAULT_ACCOUNT_QUERY.clone()); - not_empty.resize(new_len, false); - Self::new(block_responses, queries, not_empty) - } -} - -impl EthPreCircuit for MultiAccountCircuit { - fn create( - self, - mut builder: RlcThreadBuilder, - break_points: Option, - ) -> EthCircuitBuilder> { - let range = RangeChip::default(ETH_LOOKUP_BITS); - let chip = EthChip::new(RlpChip::new(&range, None), None); - let mut keccak = KeccakChip::default(); - // ================= FIRST PHASE ================ - let ctx = builder.gate_builder.main(FIRST_PHASE); - let queries = self - .queries - .into_iter() - .map(|query| { - let query = query.assign_account(ctx, &range); - (query.address.0, query.acct_pf) - }) - .collect_vec(); - let witness = - chip.parse_account_proofs_phase0(&mut builder.gate_builder, &mut keccak, queries); - // constrain all accounts exist - let ctx = builder.gate_builder.main(FIRST_PHASE); - let (account_responses, not_empty): (Vec<_>, Vec<_>) = witness - .iter() - .zip_eq(self.not_empty) - .map(|(w, not_empty)| { - let not_empty = load_bool(ctx, range.gate(), not_empty); - // we only check if the MPT key is not empty if `not_empty = true`; otherwise we don't care - let key_check = range.gate().mul(ctx, w.mpt_witness.slot_is_empty, not_empty); - range.gate().assert_is_const(ctx, &key_check, &Fr::zero()); - (AccountResponse::from_witness(w, ctx, range.gate()), not_empty) - }) - .unzip(); - let block_responses = self - .block_responses - .into_iter() - .map(|(word, num)| { - let keccak_bytes = FixedByteArray::new(ctx, &range, &num.to_be_bytes()); - (word, keccak_bytes) - }) - .collect_vec(); - // hash responses - let keccak_root = get_account_response_keccak_root( - ctx, - range.gate(), - &mut keccak, - block_responses.iter().map(|(_, bytes)| bytes), - &account_responses, - not_empty.clone(), - ); - let loader = - Halo2Loader::::new(DummyEccChip(range.gate()), builder.gate_builder); - let mut poseidon = Poseidon::from_spec(&loader, POSEIDON_SPEC.clone()); - - let mut assigned_instances = keccak_root.0; - assigned_instances.extend(get_account_response_poseidon_roots( - &loader, - &mut poseidon, - block_responses, - &account_responses, - not_empty, - )); - builder.gate_builder = loader.take_ctx(); - - // ================= SECOND PHASE ================ - EthCircuitBuilder::new( - assigned_instances, - builder, - RefCell::new(keccak), - range, - break_points, - move |builder: &mut RlcThreadBuilder, - rlp: RlpChip, - keccak_rlcs: (FixedLenRLCs, VarLenRLCs)| { - // ======== SECOND PHASE =========== - let chip = EthChip::new(rlp, Some(keccak_rlcs)); - chip.parse_account_proofs_phase1(builder, witness); - }, - ) - } -} - -lazy_static! { - pub static ref DEFAULT_ACCOUNT_QUERY: EthStorageInput = { - let mut query = DEFAULT_STORAGE_QUERY.clone(); - query.storage_pfs.clear(); - query - }; -} diff --git a/axiom-eth/src/batch_query/response/block_header.rs b/axiom-eth/src/batch_query/response/block_header.rs deleted file mode 100644 index d6000f7c..00000000 --- a/axiom-eth/src/batch_query/response/block_header.rs +++ /dev/null @@ -1,553 +0,0 @@ -//! Block Header Response -use std::cell::RefCell; - -use super::{mmr_verify::verify_mmr_proof, *}; -use crate::{ - batch_query::{ - hash::{ - bytes_select_or_zero, keccak_packed, poseidon_packed, poseidon_tree_root, - word_select_or_zero, OptPoseidonWords, POSEIDON_EMPTY_ROOTS, - }, - DummyEccChip, EccInstructions, - }, - block_header::{ - get_block_header_rlp_max_lens, EthBlockHeaderChip, EthBlockHeaderTraceWitness, - BLOCK_HEADER_FIELD_IS_VAR_LEN, BLOCK_NUMBER_INDEX, EXTRA_DATA_INDEX, - MIN_NUM_BLOCK_HEADER_FIELDS, NUM_BLOCK_HEADER_FIELDS, - }, - keccak::{FixedLenRLCs, FnSynthesize, KeccakChip, VarLenRLCs}, - rlp::{ - builder::{RlcThreadBreakPoints, RlcThreadBuilder}, - rlc::FIRST_PHASE, - RlpChip, - }, - util::{bytes_be_to_u128, is_zero_vec, load_bool}, - EthChip, EthCircuitBuilder, EthPreCircuit, Field, Network, ETH_LOOKUP_BITS, -}; -use ethers_core::types::{Block, H256}; -#[cfg(feature = "providers")] -use ethers_providers::{JsonRpcClient, Provider}; -use halo2_base::{ - gates::{GateInstructions, RangeChip, RangeInstructions}, - halo2_proofs::halo2curves::bn256::G1Affine, - utils::{bit_length, ScalarField}, - Context, - QuantumCell::Existing, -}; -use itertools::Itertools; -use lazy_static::lazy_static; -use serde::{Deserialize, Serialize}; -use snark_verifier::util::hash::Poseidon; -use snark_verifier_sdk::halo2::POSEIDON_SPEC; - -/// | Block Header Field | Max bytes | -/// |---------------------------|---------------| -/// | parentHash | 32 | -/// | ommersHash | 32 | -/// | beneficiary | 20 | -/// | stateRoot | 32 | -/// | transactionsRoot | 32 | -/// | receiptsRoot | 32 | -/// | logsBloom | 256 | -/// | difficulty | ≤7 | -/// | number | ≤4 | -/// | gasLimit | ≤4 | -/// | gasUsed | ≤4 | -/// | timestamp | ≤4 | -/// | extraData | ≤32 (mainnet) | -/// | mixHash | 32 | -/// | nonce | 8 | -/// | basefee (post-1559) | ≤6 or 0 | -/// | withdrawalsRoot (post-4895) | 32 or 0 | -/// -/// Struct that stores block header fields as an array of `ByteArray`s. The first [`MIN_NUM_BLOCK_HEADER_FIELDS`] -/// fields are of known fixed length, and the rest are either fixed length byte arrays or empty byte arrays. -/// For fields of `uint` type with variable length byte lens, the byte arrays are left padded with 0s to the max fixed length. -/// -/// We do something special for extraData because it is a variable length array of arbitrary bytes. In that case we -/// store `extraDataLength . extraDataRightPadded` as an array of field elements, where `extraDataRightPadded` -/// is right padded with 0s to max fixed length. -/// -/// Entry in the array consists of (bytes, is_some) -#[derive(Clone, Debug)] -pub struct BlockHeader { - as_list: [(FixedByteArray, Option>); NUM_BLOCK_HEADER_FIELDS], - extra_data_len: AssignedValue, -} - -/// A single response to a block header query. -/// -/// | Field | Max bytes | -/// |---------------------------|--------------| -/// | blockHash | 32 | -/// | blockNumber | ≤4 | -/// -/// ``` -/// block_response = hash(blockHash. blockNumber. hash_tree_root(block_header)) -/// ``` -/// This struct stores all the data necessary to compute the above hash. -/// -/// We store `blockNumber` twice because it needs to be accesses frequently with less hashing. -#[derive(Clone, Debug)] -pub struct BlockResponse { - pub block_hash: FixedByteArray, - pub block_header: BlockHeader, -} - -impl BlockResponse { - pub fn from_witness( - witness: &EthBlockHeaderTraceWitness, - ctx: &mut Context, - gate: &impl GateInstructions, - ) -> Self { - let block_hash = FixedByteArray(witness.block_hash.clone()); - let extra_data_len = witness.rlp_witness.field_witness[EXTRA_DATA_INDEX].field_len; - let block_header = BLOCK_HEADER_FIELD_IS_VAR_LEN - .iter() - .zip_eq(witness.rlp_witness.field_witness.iter()) - .enumerate() - .map(|(i, (is_var_len, witness))| { - if i == EXTRA_DATA_INDEX { - (FixedByteArray(witness.field_cells.clone()), None) - } else if i < MIN_NUM_BLOCK_HEADER_FIELDS { - // these fields are non-optional - let field = if *is_var_len { - // left pad with 0s to max len - ByteArray::from(witness).to_fixed(ctx, gate) - } else { - // checks to make sure actually fixed len - witness.into() - }; - (field, None) - } else { - let field = ByteArray::from(witness); - let var_len = field.var_len.unwrap(); - let is_empty = gate.is_zero(ctx, var_len); - let is_some = gate.not(ctx, is_empty); - let padded_field = if *is_var_len { - // left pad with 0s to max length - field.to_fixed(ctx, gate).0 - } else { - field.bytes - }; - (FixedByteArray(padded_field), Some(is_some)) - } - }) - .collect_vec(); - Self { - block_hash, - block_header: BlockHeader { as_list: block_header.try_into().unwrap(), extra_data_len }, - } - } - - pub fn keccak( - &self, - ctx: &mut Context, - gate: &impl GateInstructions, - keccak: &mut KeccakChip, - ) -> FixedByteArray { - let block_number_bytes = &self.block_header.as_list[BLOCK_NUMBER_INDEX].0; - keccak_packed(ctx, gate, keccak, self.block_hash.concat(block_number_bytes)) - } - - pub fn poseidon( - &self, - loader: &Rc>, - poseidon: &mut Poseidon, T, RATE>, - poseidon_empty_roots: &[F], - ) -> Scalar - where - F: Field, - C: CurveAffine, - EccChip: EccInstructions, - { - let header_as_words = self - .block_header - .as_list - .iter() - .enumerate() - .map(|(i, (bytes, is_some))| { - let mut words = bytes.to_poseidon_words(loader); - if i == EXTRA_DATA_INDEX { - // extra data is variable length, so we record (extraData.length . extraDataRightPadded) - let extra_data_len = - loader.scalar_from_assigned(self.block_header.extra_data_len); - words = PoseidonWords::from(extra_data_len).concat(&words); - } - OptPoseidonWords { - words: words.0, - is_some: is_some.map(|x| loader.scalar_from_assigned(x)), - } - }) - .collect_vec(); - - let block_number_words = PoseidonWords(header_as_words[8].words.clone()); - let header_poseidon = poseidon_tree_root(poseidon, header_as_words, poseidon_empty_roots); - - let block_hash = self.block_hash.to_poseidon_words(loader); - poseidon_packed( - poseidon, - block_hash.concat(&block_number_words).concat(&header_poseidon.into()), - ) - } -} - -/// See [`MultiBlockCircuit`] for more details. -/// -/// Returns `(keccak_tree_root(block_responses.keccak), block_responses.keccak)` -pub fn get_block_response_keccak_root( - ctx: &mut Context, - gate: &impl GateInstructions, - keccak: &mut KeccakChip, - block_responses: &[BlockResponse], - not_empty: Vec>, -) -> (FixedByteArray, Vec>) { - let block_responses = block_responses - .iter() - .zip_eq(not_empty) - .map(|(block_response, not_empty)| { - let hash = block_response.keccak(ctx, gate, keccak); - bytes_select_or_zero(ctx, gate, hash, not_empty) - }) - .collect_vec(); - let keccak_root = keccak.merkle_tree_root(ctx, gate, &block_responses); - (FixedByteArray(bytes_be_to_u128(ctx, gate, &keccak_root)), block_responses) -} - -/// See [`MultiBlockCircuit`] for more details. -/// -pub fn get_block_response_poseidon_roots( - loader: &Rc>, - poseidon: &mut Poseidon, T, RATE>, - block_responses: &[BlockResponse], - not_empty: Vec>, - poseidon_empty_roots: &[F], -) -> Vec> -where - F: Field, - C: CurveAffine, - EccChip: EccInstructions, -{ - let block_responses = block_responses - .iter() - .zip_eq(not_empty) - .map(|(block_response, not_empty)| { - let hash = block_response.poseidon(loader, poseidon, poseidon_empty_roots); - word_select_or_zero(loader, hash, not_empty) - }) - .collect_vec(); - let poseidon_root = - poseidon_tree_root(poseidon, block_responses, poseidon_empty_roots).into_assigned(); - vec![poseidon_root] -} - -// switching to just Fr for simplicity: - -/// The input datum for the circuit to generate multiple block responses. It is used to generate a circuit. -/// Additionally checks that all block hashes in a response column are in a given -/// Merkle Mountain Range (MMR). The MMR will be a commitment to a contiguous list of block hashes, for block -/// numbers `[0, mmr_list_len)`. -/// -/// Assumptions: -/// -/// -/// Assumptions: -/// * `header_rlp_encodings`, `not_empty`, `block_hashes`, `block_numbers`, `headers_poseidon`, `mmr_proofs` have the same length, which is a power of two. -/// * `header_rlp_encodings` has length greater than 1: the length 1 case still works but cannot be aggregated because -/// the single leaf of `block_responses[0].keccak` would get Poseidon hashed into a single word, whereas in a larger -/// tree it gets concatenated before hashing. -/// * `mmr_list_len < 2^MMR_MAX_NUM_PEAKS` -/// * `mmr_list_len >= 2^BLOCK_BATCH_DEPTH`, i.e., `mmr_num_peaks > BLOCK_BATCH_DEPTH` where `mmr_num_peaks := bit_length(mmr_list_len)` -/// -/// The public instances of this circuit are [`BLOCK_INSTANCE_SIZE`] field elements: -/// * Keccak merkle tree root of `keccakPacked(blockHash[i] . blockNumber[i])` over all queries: two field elements in hi-lo u128 format -/// * Poseidon merkle tree root of `block_responses[i].poseidon` over all queries: single field element -/// * `keccak256(abi.encodePacked(mmr[BLOCK_BATCH_DEPTH..]))` as 2 field elements, H256 in hi-lo form. -/// * `keccak256(abi.encodePacked(mmr[..BLOCK_BATCH_DEPTH]))` as 2 field elements, H256 in hi-lo form. -/// -/// Above `block_responses` refers to the hash of `BlockResponse`s generated by the circuit for all queries. -/// -/// To be clear, `abi.encodedPacked(mmr[d..]) = mmr[d] . mmr[d + 1] . ... . mmr[mmr_num_peaks - 1]` where `.` is concatenation of byte arrays. -/// -/// If a block entry has `not_empty = false`, then the MMR proof is skipped. -#[derive(Clone, Debug, Hash, Serialize, Deserialize)] -pub struct MultiBlockCircuit { - /// The RLP-encoded block headers - pub header_rlp_encodings: Vec>, - /// Private input to allow block_responses[i] to be `(Fr::zero(), H256::zero())` for empty entry - // This is needed so we don't need to do the MMR proof - pub not_empty: Vec, - pub network: Network, - - /// Merkle Mountain Range of block hashes for blocks `[0, mmr_list_len)`, in *increasing* order of peak size. - /// Resized with 0x0 to a fixed length. - pub mmr: [H256; MMR_MAX_NUM_PEAKS], - /// Length of the original list that `mmr` is a commitment to. - pub mmr_list_len: usize, - /// `mmr_proofs[i]` is a Merkle proof of `block_hashes[i]` into `mmr`. Resized so `mmr_proofs[i].len() = mmr.len() - 1` - pub mmr_proofs: Vec<[H256; MMR_MAX_NUM_PEAKS - 1]>, -} - -pub const MMR_MAX_NUM_PEAKS: usize = 32; // assuming block number stays in u32, < 2^32 -/// The AxiomV1Core smart contract only stores Merkle Mountain Range of Merkle roots of block hashes of contiguous segments -/// of blocks of length 2BLOCK_BATCH_DEPTH. -pub const BLOCK_BATCH_DEPTH: usize = 10; - -pub const BLOCK_INSTANCE_SIZE: usize = 7; -pub(crate) const KECCAK_BLOCK_RESPONSE_INDEX: usize = 0; -pub(crate) const BLOCK_RESPONSE_POSEIDON_INDEX: usize = 2; -pub const BLOCK_POSEIDON_ROOT_INDICES: &[usize] = &[BLOCK_RESPONSE_POSEIDON_INDEX]; -pub const BLOCK_KECCAK_ROOT_INDICES: &[usize] = &[KECCAK_BLOCK_RESPONSE_INDEX]; - -impl MultiBlockCircuit { - /// Creates circuit inputs from raw RLP encodings. Panics if number of blocks is not a power of 2. - pub fn new( - mut header_rlps: Vec>, - not_empty: Vec, - network: Network, - mut mmr: Vec, - mmr_list_len: usize, - mmr_proofs: Vec>, - ) -> Self { - assert!(header_rlps.len() > 1); - assert_eq!(header_rlps.len(), not_empty.len()); - assert!(header_rlps.len().is_power_of_two(), "Number of blocks must be a power of 2"); - // resize RLPs - let (header_rlp_max_bytes, _) = get_block_header_rlp_max_lens(network); - for rlp in &mut header_rlps { - rlp.resize(header_rlp_max_bytes, 0); - } - assert_eq!(header_rlps.len(), mmr_proofs.len()); - - mmr.resize(MMR_MAX_NUM_PEAKS, H256::zero()); - let mmr_proofs = mmr_proofs - .into_iter() - .map(|mut proof| { - proof.resize(MMR_MAX_NUM_PEAKS - 1, H256::zero()); - proof.try_into().unwrap() - }) - .collect(); - Self { - header_rlp_encodings: header_rlps, - not_empty, - network, - mmr: mmr.try_into().unwrap(), - mmr_list_len, - mmr_proofs, - } - } - - /// Creates circuit inputs using JSON-RPC provider. Panics if provider error or any block is not found. - /// - /// Assumes that `network` is the same as the provider's network. - #[cfg(feature = "providers")] - pub fn from_provider( - provider: &Provider

, - block_numbers: Vec, - not_empty: Vec, - network: Network, - mmr: Vec, - mmr_list_len: usize, - mmr_proofs: Vec>, - ) -> Self { - use crate::providers::{get_block_rlp, get_blocks}; - - let header_rlp_encodings = get_blocks(provider, block_numbers) - .unwrap() - .into_iter() - .map(|block| get_block_rlp(&block.expect("block not found"))) - .collect(); - Self::new(header_rlp_encodings, not_empty, network, mmr, mmr_list_len, mmr_proofs) - } - - pub fn resize_from( - mut header_rlps: Vec>, - mut not_empty: Vec, - network: Network, - mmr: Vec, - mmr_list_len: usize, - mut mmr_proofs: Vec>, - new_len: usize, - ) -> Self { - header_rlps.resize_with(new_len, || GENESIS_BLOCK_RLP.to_vec()); - not_empty.resize(new_len, false); - mmr_proofs.resize(new_len, vec![]); - Self::new(header_rlps, not_empty, network, mmr, mmr_list_len, mmr_proofs) - } -} - -impl EthPreCircuit for MultiBlockCircuit { - fn create( - self, - mut builder: RlcThreadBuilder, - break_points: Option, - ) -> EthCircuitBuilder> { - let range = RangeChip::default(ETH_LOOKUP_BITS); - let gate = range.gate(); - let chip = EthChip::new(RlpChip::new(&range, None), None); - let mut keccak = KeccakChip::default(); - // ================= FIRST PHASE ================ - let witness = chip.decompose_block_headers_phase0( - &mut builder.gate_builder, - &mut keccak, - self.header_rlp_encodings, - self.network, - ); - let ctx = builder.gate_builder.main(FIRST_PHASE); - let block_responses = - witness.iter().map(|w| BlockResponse::from_witness(w, ctx, range.gate())).collect_vec(); - let not_empty = - self.not_empty.into_iter().map(|b| load_bool(ctx, range.gate(), b)).collect_vec(); - - // verify mmr - let mmr_list_len = ctx.load_witness(gate.get_field_element(self.mmr_list_len as u64)); - let mmr_bits = gate.num_to_bits(ctx, mmr_list_len, MMR_MAX_NUM_PEAKS); // implicitly range checks that mmr_list_len < 2^MMR_MAX_NUM_PEAKS - - let mmr = self - .mmr - .into_iter() - .map(|peak| FixedByteArray::new(ctx, &range, peak.as_bytes())) - .collect_vec(); - // check that mmr peaks agree with mmr_bits - for (bit, peak) in mmr_bits.iter().zip_eq(mmr.iter()) { - let no_peak = is_zero_vec(ctx, gate, &peak.0); - let is_peak = gate.not(ctx, no_peak); - ctx.constrain_equal(&is_peak, bit); - } - // verify mmr proofs - block_responses.iter().zip(self.mmr_proofs).zip_eq(not_empty.clone()).for_each( - |((response, mmr_proof), not_empty)| { - let block_hash = response.block_hash.clone(); - let block_number_be = &response.block_header.as_list[BLOCK_NUMBER_INDEX].0 .0; - // this is done again later in poseidon, so a duplicate for code conciseness - let block_number = bytes_be_to_u128(ctx, gate, block_number_be).pop().unwrap(); - let mmr_proof = mmr_proof - .into_iter() - .map(|node| FixedByteArray::new(ctx, &range, node.as_bytes())) - .collect_vec(); - verify_mmr_proof( - ctx, - &range, - &mut keccak, - &mmr, - mmr_list_len, - &mmr_bits, - block_number, - block_hash, - mmr_proof, - not_empty, - ); - }, - ); - // mmr_num_peaks = bit_length(mmr_list_len) = MMR_MAX_NUM_PEAKS - num_leading_zeros(mmr_list_len) - let mut is_leading = Constant(Fr::one()); - let mut num_leading_zeros = ctx.load_zero(); - for bit in mmr_bits.iter().rev() { - // is_zero = 1 - bit - // is_leading = is_leading * (is_zero) - is_leading = Existing(gate.mul_not(ctx, *bit, is_leading)); - num_leading_zeros = gate.add(ctx, num_leading_zeros, is_leading); - } - let max_num_peaks = gate.get_field_element(MMR_MAX_NUM_PEAKS as u64); - let num_peaks = gate.sub(ctx, Constant(max_num_peaks), num_leading_zeros); - let truncated_num_peaks = - gate.sub(ctx, num_peaks, Constant(gate.get_field_element(BLOCK_BATCH_DEPTH as u64))); - range.range_check(ctx, truncated_num_peaks, bit_length(MMR_MAX_NUM_PEAKS as u64)); // ensures not negative - let truncated_mmr_bytes = - gate.mul(ctx, truncated_num_peaks, Constant(gate.get_field_element(32u64))); - let keccak_id = keccak.keccak_var_len( - ctx, - &range, - mmr[BLOCK_BATCH_DEPTH..].iter().flat_map(|bytes| bytes.0.clone()).collect(), - None, - truncated_mmr_bytes, - 0, - ); - let keccak_bytes = keccak.var_len_queries[keccak_id].output_assigned.clone(); - let historical_mmr_keccak = bytes_be_to_u128(ctx, gate, &keccak_bytes); - - let recent_mmr_keccak_bytes = keccak_packed( - ctx, - gate, - &mut keccak, - FixedByteArray( - mmr[..BLOCK_BATCH_DEPTH].iter().flat_map(|bytes| bytes.0.clone()).collect(), - ), - ); - let recent_mmr_keccak = bytes_be_to_u128(ctx, gate, recent_mmr_keccak_bytes.as_ref()); - - // keccak responses - let (keccak_root, _) = get_block_response_keccak_root( - ctx, - range.gate(), - &mut keccak, - &block_responses, - not_empty.clone(), - ); - // poseidon responses - let loader = - Halo2Loader::::new(DummyEccChip(range.gate()), builder.gate_builder); - let mut poseidon = Poseidon::from_spec(&loader, POSEIDON_SPEC.clone()); - - let mut assigned_instances = keccak_root.0; - assigned_instances.extend( - get_block_response_poseidon_roots( - &loader, - &mut poseidon, - &block_responses, - not_empty, - &POSEIDON_EMPTY_ROOTS, - ) - .into_iter() - .chain(historical_mmr_keccak) - .chain(recent_mmr_keccak), - ); - - builder.gate_builder = loader.take_ctx(); - - // ================= SECOND PHASE ================ - EthCircuitBuilder::new( - assigned_instances, - builder, - RefCell::new(keccak), - range, - break_points, - move |builder: &mut RlcThreadBuilder, - rlp: RlpChip, - keccak_rlcs: (FixedLenRLCs, VarLenRLCs)| { - // ======== SECOND PHASE =========== - let chip = EthChip::new(rlp, Some(keccak_rlcs)); - chip.decompose_block_headers_phase1(builder, witness); - }, - ) - } -} - -pub const GENESIS_BLOCK_RLP: &[u8] = &[ - 249, 2, 20, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 160, 29, 204, 77, 232, 222, 199, 93, 122, 171, 133, 181, 103, 182, 204, 212, - 26, 211, 18, 69, 27, 148, 138, 116, 19, 240, 161, 66, 253, 64, 212, 147, 71, 148, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 160, 215, 248, 151, 79, 181, 172, 120, 217, - 172, 9, 155, 154, 213, 1, 139, 237, 194, 206, 10, 114, 218, 209, 130, 122, 23, 9, 218, 48, 88, - 15, 5, 68, 160, 86, 232, 31, 23, 27, 204, 85, 166, 255, 131, 69, 230, 146, 192, 248, 110, 91, - 72, 224, 27, 153, 108, 173, 192, 1, 98, 47, 181, 227, 99, 180, 33, 160, 86, 232, 31, 23, 27, - 204, 85, 166, 255, 131, 69, 230, 146, 192, 248, 110, 91, 72, 224, 27, 153, 108, 173, 192, 1, - 98, 47, 181, 227, 99, 180, 33, 185, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 133, 4, 0, 0, 0, 0, 128, 130, 19, 136, 128, 128, 160, - 17, 187, 232, 219, 78, 52, 123, 78, 140, 147, 124, 28, 131, 112, 228, 181, 237, 51, 173, 179, - 219, 105, 203, 219, 122, 56, 225, 229, 11, 27, 130, 250, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136, 0, 0, 0, 0, 0, 0, 0, 66, -]; - -lazy_static! { - pub static ref GENESIS_BLOCK: Block = serde_json::from_str(r#" - {"hash":"0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3","parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","miner":"0x0000000000000000000000000000000000000000","stateRoot":"0xd7f8974fb5ac78d9ac099b9ad5018bedc2ce0a72dad1827a1709da30580f0544","transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","number":"0x0","gasUsed":"0x0","gasLimit":"0x1388","extraData":"0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","timestamp":"0x0","difficulty":"0x400000000","totalDifficulty":"0x400000000","sealFields":[],"uncles":[],"transactions":[],"size":"0x21c","mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0000000000000042","baseFeePerGas":null} - "#).unwrap(); -} diff --git a/axiom-eth/src/batch_query/response/mmr_verify.rs b/axiom-eth/src/batch_query/response/mmr_verify.rs deleted file mode 100644 index 3b369550..00000000 --- a/axiom-eth/src/batch_query/response/mmr_verify.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Verify all block hashes in BlockResponse column against a given Merkle Mountain Range - -use halo2_base::{ - gates::{GateInstructions, RangeInstructions}, - utils::ScalarField, - AssignedValue, Context, - QuantumCell::{Constant, Existing, Witness}, -}; -use itertools::Itertools; - -use crate::{ - batch_query::hash::keccak_packed, keccak::KeccakChip, util::select_array_by_indicator, Field, -}; - -use super::FixedByteArray; - -/// Input assumptions which must hold but which are not constrained in the circuit: -/// * `mmr` is Merkle Mountain Range in *increasing* order of peak size. Array of fixed length 32 byte arrays. -/// Array `mmr` is resized to a fixed max length. -/// * `mmr_list_len` is the length of the original list that `mmr` is a commitment to. -/// * `mmr_bits` is the same length as `mmr`. `mmr_bits[i]` is a bit that is 1 if `mmr[i]` is a non-empty peak, and 0 otherwise. In other words, `mmr_bits` is the little-endian bit representation of `mmr_list_len`. -#[allow(clippy::too_many_arguments)] -pub fn verify_mmr_proof( - ctx: &mut Context, - range: &impl RangeInstructions, - keccak: &mut KeccakChip, - mmr: &[impl AsRef<[AssignedValue]>], - mmr_list_len: AssignedValue, - mmr_bits: &[AssignedValue], - list_id: AssignedValue, // the index in underlying list - leaf: FixedByteArray, // the leaf node at `list_id` in underlying list - merkle_proof: Vec>, - not_empty: AssignedValue, // actually do the proof check -) { - assert!(!mmr.is_empty()); - let gate = range.gate(); - assert_eq!(mmr.len(), mmr_bits.len()); - let index_bits = range.gate().num_to_bits(ctx, list_id, mmr.len()); - range.check_less_than(ctx, list_id, mmr_list_len, mmr.len()); - // count how many leading (big-endian) bits `mmr_bits` and `index_bits` have in common - let mut agree = Constant(F::one()); - let mut num_leading_agree = ctx.load_zero(); - for (a, b) in mmr_bits.iter().rev().zip(index_bits.iter().rev()) { - let is_equal = bit_is_equal(ctx, gate, *a, *b); - agree = Existing(gate.mul(ctx, agree, is_equal)); - num_leading_agree = gate.add(ctx, num_leading_agree, agree); - } - // if num_leading_agree = mmr.len() that means peak_id = mmr_list_len is outside of this MMR - let max_peak_id = gate.get_field_element(mmr.len() as u64 - 1); - let peak_id = gate.sub(ctx, Constant(max_peak_id), num_leading_agree); - - // we merkle prove `leaf` into `mmr[peak_id]` using `index_bits[..peak_id]` as the "side" - assert_eq!(merkle_proof.len() + 1, mmr.len()); // max depth of a peak is mmr.len() - 1 - let mut intermediate_hashes = Vec::with_capacity(mmr.len()); - intermediate_hashes.push(leaf); - // last index_bit is never used: if it were 1 then leading bit of mmr_bits would also have to be 1 - for (side, node) in index_bits.into_iter().zip(merkle_proof) { - let cur = intermediate_hashes.last().unwrap(); - // Possible optimization: if merkle_proof consists of unassigned witnesses, they can be assigned while `select`ing here. We avoid this low-level optimization for code clarity for now. - let concat = cur - .0 - .iter() - .chain(node.0.iter()) - .zip_eq(node.0.iter().chain(cur.0.iter())) - .map(|(a, b)| gate.select(ctx, *b, *a, side)) - .collect_vec(); - let hash = keccak_packed(ctx, gate, keccak, FixedByteArray(concat)); - intermediate_hashes.push(hash); - } - let peak_indicator = gate.idx_to_indicator(ctx, peak_id, mmr.len()); - // get mmr[peak_id] - debug_assert_eq!(mmr[0].as_ref().len(), 32); - let peak = select_array_by_indicator(ctx, gate, mmr, &peak_indicator); - let proof_peak = select_array_by_indicator(ctx, gate, &intermediate_hashes, &peak_indicator); - for (a, b) in peak.into_iter().zip_eq(proof_peak) { - let a = gate.mul(ctx, a, not_empty); - let b = gate.mul(ctx, b, not_empty); - ctx.constrain_equal(&a, &b); - } -} - -/// Assumes `a, b` are both bits. -/// -/// Returns `a == b` as a bit. -pub fn bit_is_equal( - ctx: &mut Context, - gate: &impl GateInstructions, - a: AssignedValue, - b: AssignedValue, -) -> AssignedValue { - // (a == b) = 1 - (a - b)^2 - let diff = gate.sub(ctx, a, b); - // | 1 - (a-b)^2 | a-b | a-b | 1 | - let out_val = F::one() - diff.value().square(); - ctx.assign_region([Witness(out_val), Existing(diff), Existing(diff), Constant(F::one())], [0]); - ctx.get(-4) -} diff --git a/axiom-eth/src/batch_query/response/mod.rs b/axiom-eth/src/batch_query/response/mod.rs deleted file mode 100644 index dc061f7f..00000000 --- a/axiom-eth/src/batch_query/response/mod.rs +++ /dev/null @@ -1,176 +0,0 @@ -//! The containers for different query response types - -use super::hash::PoseidonWords; -use super::EccInstructions; -use crate::rlp::RlpFieldWitness; -use crate::util::bytes_be_var_to_fixed; -use crate::{mpt::AssignedBytes, util::bytes_be_to_uint}; -use halo2_base::gates::{GateChip, RangeInstructions}; -use halo2_base::halo2_proofs::halo2curves::bn256::Fr; -use halo2_base::halo2_proofs::halo2curves::CurveAffine; -use halo2_base::{ - gates::GateInstructions, utils::ScalarField, AssignedValue, Context, QuantumCell::Constant, -}; -use snark_verifier::loader::halo2::{Halo2Loader, Scalar}; -use std::iter; -use std::rc::Rc; - -pub mod account; -pub mod block_header; -pub mod mmr_verify; -pub mod native; -pub mod row_consistency; -pub mod storage; - -/// An assigned byte array of known fixed length. -#[derive(Clone, Debug)] -pub struct FixedByteArray(pub AssignedBytes); - -impl<'a, F: ScalarField> From<&'a RlpFieldWitness> for FixedByteArray { - fn from(value: &'a RlpFieldWitness) -> Self { - assert_eq!(value.field_len.value().get_lower_32() as usize, value.field_cells.len()); - Self(value.field_cells.clone()) - } -} - -impl AsRef<[AssignedValue]> for FixedByteArray { - fn as_ref(&self) -> &[AssignedValue] { - &self.0 - } -} - -/// An assigned byte array. Entries of `bytes` assumed to be bytes. -/// -/// If `var_len` is `None`, then the byte array is assumed to be of known fixed length. -/// Otherwise, `var_len` is the variable length of the byte array, and it is assumed that `bytes` has been right padded by 0s to a max fixed length. -#[derive(Clone, Debug)] -pub struct ByteArray { - pub bytes: AssignedBytes, - pub var_len: Option>, -} - -impl<'a, F: ScalarField> From<&'a RlpFieldWitness> for ByteArray { - fn from(value: &'a RlpFieldWitness) -> Self { - Self { var_len: Some(value.field_len), bytes: value.field_cells.clone() } - } -} - -impl From> for ByteArray { - fn from(value: FixedByteArray) -> Self { - Self { var_len: None, bytes: value.0 } - } -} - -impl ByteArray { - /// Evaluates a variable-length byte string to a big endian number. - /// - /// If the resulting number is larger than the size of the scalar field `F`, then the result - /// is modulo the prime of the scalar field. (We do not recommend using it in this setting.) - pub fn evaluate( - &self, - ctx: &mut Context, - gate: &impl GateInstructions, - ) -> AssignedValue { - if let Some(len) = self.var_len { - evaluate_byte_array(ctx, gate, &self.bytes, len) - } else { - bytes_be_to_uint(ctx, gate, &self.bytes, self.bytes.len()) - } - } - - /// Converts a variable-length byte array to a fixed-length byte array by left padding with 0s. - /// Assumes that `self.bytes` has been right padded with 0s to a max fixed length. - pub fn to_fixed( - self, - ctx: &mut Context, - gate: &impl GateInstructions, - ) -> FixedByteArray { - FixedByteArray(if let Some(len) = self.var_len { - bytes_be_var_to_fixed(ctx, gate, &self.bytes, len, self.bytes.len()) - } else { - self.bytes - }) - } -} - -impl FixedByteArray { - /// Loads bytes as witnesses and range checks each witness to be 8 bits. - pub fn new(ctx: &mut Context, range: &impl RangeInstructions, bytes: &[u8]) -> Self { - let bytes = - ctx.assign_witnesses(bytes.iter().map(|x| range.gate().get_field_element(*x as u64))); - // range check bytes - for byte in &bytes { - range.range_check(ctx, *byte, 8); - } - Self(bytes) - } - - /// Loads bytes as constants. - pub fn new_const(ctx: &mut Context, gate: &impl GateInstructions, bytes: &[u8]) -> Self { - let bytes = - bytes.iter().map(|b| ctx.load_constant(gate.get_field_element(*b as u64))).collect(); - Self(bytes) - } - - /// Evaluates a fixed-length byte string to a big endian number. - /// - /// If the resulting number is larger than the size of the scalar field `F`, then the result - /// is modulo the prime of the scalar field. (We do not recommend using it in this setting.) - pub fn evaluate( - &self, - ctx: &mut Context, - gate: &impl GateInstructions, - ) -> AssignedValue { - bytes_be_to_uint(ctx, gate, &self.0, self.0.len()) - } - - pub fn to_poseidon_words( - &self, - loader: &Rc>, - ) -> PoseidonWords> - where - C: CurveAffine, - EccChip: EccInstructions, - { - assert!(F::CAPACITY >= 128); - if self.0.is_empty() { - return PoseidonWords(vec![]); - } - let mut builder = loader.ctx_mut(); - let gate: &GateChip = &loader.scalar_chip(); - let ctx = builder.main(0); - PoseidonWords(if 8 * self.0.len() <= F::CAPACITY as usize { - vec![loader.scalar_from_assigned(self.evaluate(ctx, gate))] - } else { - self.0 - .chunks(16) - .map(|chunk| { - loader.scalar_from_assigned(bytes_be_to_uint(ctx, gate, chunk, chunk.len())) - }) - .collect() - }) - } - - pub fn concat(&self, other: &Self) -> Self { - Self([&self.0[..], &other.0[..]].concat()) - } -} - -/// Evaluate a variable length byte array `array[..len]` to a big endian number -pub fn evaluate_byte_array( - ctx: &mut Context, - gate: &impl GateInstructions, - array: &[AssignedValue], - len: AssignedValue, -) -> AssignedValue { - let f_256 = gate.get_field_element(256); - if !array.is_empty() { - let incremental_evals = - gate.accumulated_product(ctx, iter::repeat(Constant(f_256)), array.iter().copied()); - let len_minus_one = gate.sub(ctx, len, Constant(F::one())); - // if `len = 0` then `len_minus_one` will be very large, so `select_from_idx` will return 0. - gate.select_from_idx(ctx, incremental_evals.iter().copied(), len_minus_one) - } else { - ctx.load_zero() - } -} diff --git a/axiom-eth/src/batch_query/response/native.rs b/axiom-eth/src/batch_query/response/native.rs deleted file mode 100644 index 6b3914b5..00000000 --- a/axiom-eth/src/batch_query/response/native.rs +++ /dev/null @@ -1,263 +0,0 @@ -/// The native implementation of the {Poseidon, Keccak} response of queries. -use crate::{ - batch_query::hash::{poseidon_packed, poseidon_tree_root, PoseidonWords}, - block_header::{ - EXTRA_DATA_INDEX, GOERLI_EXTRA_DATA_MAX_BYTES, MAINNET_EXTRA_DATA_MAX_BYTES, - MAINNET_HEADER_FIELDS_MAX_BYTES, NUM_BLOCK_HEADER_FIELDS, - }, - providers::get_block_rlp, - storage::{EthBlockStorageInput, EthStorageInput}, - Network, -}; -use ethers_core::{ - types::{Address, Block, H256}, - utils::keccak256, -}; -use halo2_base::halo2_proofs::halo2curves::FieldExt; -use itertools::Itertools; -use serde::{Deserialize, Serialize}; -use snark_verifier::util::hash::Poseidon; - -use super::storage::DEFAULT_STORAGE_QUERY; - -#[derive(Clone, Debug)] -pub(crate) struct NativeBlockResponse { - pub block_hash: PoseidonWords, - pub header_list: Vec>, - pub header_poseidon: F, -} - -/// Computes -/// ``` -/// block_response = hash(blockHash . blockNumber . hash_tree_root(blockHeader)) -/// ``` -/// where `hash` is {Poseidon, Keccak} -/// -/// Also returns block header as list of PoseidonWords. -pub(crate) fn get_block_response( - poseidon: &mut Poseidon, - block: Block, - network: Network, -) -> ((F, H256), NativeBlockResponse) { - let mut header_list = Vec::with_capacity(32); - header_list.push(block.parent_hash.0.to_vec()); - header_list.push(block.uncles_hash.0.to_vec()); - header_list.push(block.author.unwrap().0.to_vec()); - header_list.push(block.state_root.0.to_vec()); - header_list.push(block.transactions_root.0.to_vec()); - header_list.push(block.receipts_root.0.to_vec()); - header_list.push(block.logs_bloom.unwrap().0.to_vec()); - let mut difficulty = [0u8; 32]; - block.difficulty.to_big_endian(&mut difficulty); - let difficulty = difficulty[32 - MAINNET_HEADER_FIELDS_MAX_BYTES[7]..].to_vec(); - header_list.push(difficulty); - let mut number = [0u8; 8]; - block.number.unwrap().to_big_endian(&mut number); - let number = number[8 - MAINNET_HEADER_FIELDS_MAX_BYTES[8]..].to_vec(); - header_list.push(number.clone()); - let mut gas_limit = [0u8; 32]; - block.gas_limit.to_big_endian(&mut gas_limit); - let gas_limit = gas_limit[32 - MAINNET_HEADER_FIELDS_MAX_BYTES[9]..].to_vec(); - header_list.push(gas_limit); - let mut gas_used = [0u8; 32]; - block.gas_used.to_big_endian(&mut gas_used); - let gas_used = gas_used[32 - MAINNET_HEADER_FIELDS_MAX_BYTES[10]..].to_vec(); - header_list.push(gas_used); - let mut timestamp = [0u8; 32]; - block.timestamp.to_big_endian(&mut timestamp); - let timestamp = timestamp[32 - MAINNET_HEADER_FIELDS_MAX_BYTES[11]..].to_vec(); - header_list.push(timestamp); - let extra_data_len = match network { - Network::Mainnet => MAINNET_EXTRA_DATA_MAX_BYTES, - Network::Goerli => GOERLI_EXTRA_DATA_MAX_BYTES, - }; - let mut extra_data = vec![0u8; extra_data_len]; - extra_data[..block.extra_data.len()].copy_from_slice(&block.extra_data); - header_list.push(extra_data); - header_list.push(block.mix_hash.unwrap().0.to_vec()); - header_list.push(block.nonce.unwrap().0.to_vec()); - header_list.push( - block - .base_fee_per_gas - .map(|uint| { - let mut bytes = [0u8; 32]; - uint.to_big_endian(&mut bytes); - bytes[32 - MAINNET_HEADER_FIELDS_MAX_BYTES[15]..].to_vec() - }) - .unwrap_or_default(), - ); - header_list.push(block.withdrawals_root.map(|root| root.0.to_vec()).unwrap_or_default()); - assert_eq!( - header_list.len(), - NUM_BLOCK_HEADER_FIELDS, - "Discrepancy in assumed max number of block header fields. Has there been a hard fork recently?" - ); - let mut header_list = header_list.iter().map(|x| bytes_to_poseidon_words(x)).collect_vec(); - header_list[EXTRA_DATA_INDEX].0.insert(0, F::from(block.extra_data.len() as u64)); - let mut depth = header_list.len().ilog2(); - if 1 << depth != header_list.len() { - depth += 1; - } - header_list.resize(1 << depth, PoseidonWords(vec![])); - let header_poseidon = poseidon_tree_root(poseidon, header_list.clone(), &[]); - - let block_hash = block.hash.unwrap(); - let response_keccak = keccak256([block_hash.as_bytes(), &number[..]].concat()); - let block_hash = bytes_to_poseidon_words(block_hash.as_bytes()); - let response_poseidon = poseidon_packed( - poseidon, - block_hash.concat(&bytes_to_poseidon_words(&number[..])).concat(&header_poseidon.into()), - ); - ( - (response_poseidon, response_keccak.into()), - NativeBlockResponse { block_hash, header_list, header_poseidon }, - ) -} - -#[derive(Clone, Debug)] -pub(crate) struct NativeAccountResponse { - pub state_root: PoseidonWords, - pub address: PoseidonWords, - pub state_list: Vec>, -} - -/// Computes -/// ``` -/// account_response = hash(stateRoot . address . hash_tree_root(account_state)) -/// ``` -/// where `hash` is Poseidon -pub(crate) fn get_account_response( - poseidon: &mut Poseidon, - input: &EthStorageInput, -) -> ((F, Vec), NativeAccountResponse) { - let state_list = input.acct_state.iter().map(|x| bytes_to_poseidon_words(x)).collect_vec(); - let state_poseidon = poseidon_tree_root(poseidon, state_list.clone(), &[]); - let state_keccak = keccak256(input.acct_state.concat()); - let response_keccak = [input.addr.as_bytes(), &state_keccak].concat(); - let state_root = bytes_to_poseidon_words(input.acct_pf.root_hash.as_bytes()); - let address = bytes_to_poseidon_words(input.addr.as_bytes()); - let response_poseidon = - poseidon_packed(poseidon, state_root.concat(&address).concat(&state_poseidon.into())); - ( - (response_poseidon, response_keccak), - NativeAccountResponse { state_root, address, state_list }, - ) -} - -/// Computes -/// ``` -/// hash(block_response . account_response) -/// ``` -pub fn get_full_account_response( - poseidon: &mut Poseidon, - (block_response, block_number): (F, u32), - account_response: (F, Vec), -) -> (F, H256) { - ( - poseidon_packed(poseidon, PoseidonWords(vec![block_response, account_response.0])), - keccak256([block_number.to_be_bytes().to_vec(), account_response.1].concat()).into(), - ) -} - -#[derive(Clone, Debug)] -pub struct NativeStorageResponse { - pub storage_root: PoseidonWords, - pub slot: PoseidonWords, - pub value: PoseidonWords, -} - -/// Computes -/// ``` -/// storage_response = hash(storageRoot . slot . value) -/// ``` -/// where `hash` is {Poseidon, Keccak} and `value` is left padded to 32 bytes. -pub fn get_storage_response( - poseidon: &mut Poseidon, - input: &EthStorageInput, -) -> ((F, Vec), NativeStorageResponse) { - assert_eq!(input.storage_pfs.len(), 1); - let (slot, _value, proof) = input.storage_pfs.last().unwrap(); - let storage_root = proof.root_hash; - let mut value = [0u8; 32]; - _value.to_big_endian(&mut value); - - let response_keccak = [slot.as_bytes(), &value[..]].concat(); - let [storage_root, slot, value] = - [storage_root.as_bytes(), slot.as_bytes(), &value[..]].map(bytes_to_poseidon_words); - let response_poseidon = poseidon_packed(poseidon, storage_root.concat(&slot).concat(&value)); - ((response_poseidon, response_keccak), NativeStorageResponse { storage_root, slot, value }) -} - -/// Computes -/// ``` -/// hash(block_response . account_response . storage_response) -/// ``` -pub fn get_full_storage_response( - poseidon: &mut Poseidon, - block_response: (F, u32), - account_response: (F, Address), - storage_response: (F, Vec), -) -> (F, H256) { - ( - poseidon_packed( - poseidon, - PoseidonWords(vec![block_response.0, account_response.0, storage_response.0]), - ), - keccak256( - [&block_response.1.to_be_bytes(), account_response.1.as_bytes(), &storage_response.1] - .concat(), - ) - .into(), - ) -} - -// PoseidonWords with NativeLoader -pub(crate) fn bytes_to_poseidon_words(bytes: &[u8]) -> PoseidonWords { - PoseidonWords(if bytes.is_empty() { - vec![] - } else if bytes.len() < 32 { - vec![evaluate_bytes(bytes)] - } else { - bytes.chunks(16).map(evaluate_bytes).collect() - }) -} - -/// Assumes `F::from_repr` uses little endian. -pub(crate) fn evaluate_bytes(bytes_be: &[u8]) -> F { - let mut bytes_le = F::Repr::default(); - assert!(bytes_be.len() < bytes_le.as_ref().len()); - for (le, be) in bytes_le.as_mut().iter_mut().zip(bytes_be.iter().rev()) { - *le = *be; - } - F::from_repr(bytes_le).unwrap() -} - -/// Query for a block header, optional account state, and optional storage proof(s). -#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct FullStorageQuery { - pub block_number: u64, - pub addr_slots: Option<(Address, Vec)>, -} - -/// Response with a block header, optional account state, and optional storage proof(s). -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct FullStorageResponse { - /// Assumes `block` is an existing block, not a pending block. - pub block: Block, - pub account_storage: Option, -} - -impl From for EthBlockStorageInput { - fn from(value: FullStorageResponse) -> Self { - let number = value.block.number.unwrap(); - let block_hash = value.block.hash.unwrap(); - let block_header = get_block_rlp(&value.block); - Self { - block: value.block, - block_number: number.as_u32(), - block_hash, - block_header, - storage: value.account_storage.unwrap_or_else(|| DEFAULT_STORAGE_QUERY.clone()), - } - } -} diff --git a/axiom-eth/src/batch_query/response/row_consistency.rs b/axiom-eth/src/batch_query/response/row_consistency.rs deleted file mode 100644 index f6bb5c31..00000000 --- a/axiom-eth/src/batch_query/response/row_consistency.rs +++ /dev/null @@ -1,326 +0,0 @@ -//! Checks consistency of `BlockResponse`, `AccountResponse` and `StorageResponse` -//! by decommiting hash and going row-by-row. -use std::env::var; -use std::str::FromStr; - -use ethers_core::types::H256; -use halo2_base::gates::builder::{ - CircuitBuilderStage, GateThreadBuilder, MultiPhaseThreadBreakPoints, RangeCircuitBuilder, - RangeWithInstanceCircuitBuilder, -}; -use halo2_base::halo2_proofs::halo2curves::bn256::G1Affine; -use itertools::Itertools; -use lazy_static::lazy_static; -use serde::{Deserialize, Serialize}; -use snark_verifier::{loader::ScalarLoader, util::hash::Poseidon}; -use snark_verifier_sdk::{halo2::POSEIDON_SPEC, NativeLoader}; - -use super::storage::DEFAULT_STORAGE_QUERY; -use super::{native::get_block_response, *}; -use crate::batch_query::hash::poseidon_tree_root; -use crate::batch_query::response::block_header::{GENESIS_BLOCK, GENESIS_BLOCK_RLP}; -use crate::batch_query::response::native::NativeBlockResponse; -use crate::batch_query::DummyEccChip; -use crate::Field; -use crate::{ - batch_query::{ - hash::{create_merkle_proof, poseidon_packed, traverse_merkle_proof}, - response::{ - account::STORAGE_ROOT_INDEX, - native::{ - get_account_response, get_storage_response, NativeAccountResponse, - NativeStorageResponse, - }, - }, - }, - block_header::{BLOCK_NUMBER_INDEX, STATE_ROOT_INDEX}, - rlp::rlc::FIRST_PHASE, - storage::EthBlockStorageInput, - util::load_bool, - Network, -}; - -/// The input data for a circuit that checks consistency of columns for `BlockResponse`, `AccountResponse` and `StorageResponse`. -/// -/// Assumptions: -/// * `responses`, `block_not_empty`, `account_not_empty` and `storage_not_empty` to all be the same length, and length is a power of two. -/// -/// Public instances consist of [`ROW_CIRCUIT_NUM_INSTANCES`] field elements: -/// * `poseidon_tree_root(block_responses.poseidon)` -/// * `poseidon_tree_root(block_responses.keccak)` -/// * `poseidon_tree_root(full_account_responses.poseidon)` -/// * `poseidon_tree_root([account_not_empty[i] ? block_number[i] : 0x0 for all i])` -/// * `poseidon_tree_root(account_responses.keccak)` -/// * `poseidon_tree_root(full_storage_responses.poseidon)` -/// * `poseidon_tree_root([storage_not_empty[i] ? block_number[i] : 0x0 for all i])` -/// * `poseidon_tree_root([storage_not_empty[i] ? address[i] : 0x0 for all i])` -/// -/// Since a Poseidon hash is `0` with vanishingly small probability, the Poseidon Merkle roots above commit to the data of whether a column entry is empty or not. -/// `block_responses[i]` is zero if `block_not_empty[i]` is false, and similarly for `full_account_responses[i]` and `full_storage_responses[i]`. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RowConsistencyCircuit { - pub responses: Vec, - pub block_not_empty: Vec, - pub account_not_empty: Vec, - pub storage_not_empty: Vec, - pub network: Network, -} - -pub(crate) const ROW_CIRCUIT_NUM_INSTANCES: usize = 6; -pub(crate) const ROW_BLOCK_POSEIDON_INDEX: usize = 0; -pub(crate) const ROW_ACCT_POSEIDON_INDEX: usize = 1; -pub(crate) const ROW_ACCT_BLOCK_KECCAK_INDEX: usize = 2; -pub(crate) const ROW_STORAGE_POSEIDON_INDEX: usize = 3; -pub(crate) const ROW_STORAGE_BLOCK_KECCAK_INDEX: usize = 4; -pub(crate) const ROW_STORAGE_ACCT_KECCAK_INDEX: usize = 5; - -impl RowConsistencyCircuit { - pub fn new( - responses: Vec, - block_not_empty: Vec, - account_not_empty: Vec, - storage_not_empty: Vec, - network: Network, - ) -> Self { - assert!(responses.len().is_power_of_two()); - assert_eq!(responses.len(), account_not_empty.len()); - assert_eq!(account_not_empty.len(), storage_not_empty.len()); - for ((&block_not_empty, &account_not_empty), &storage_not_empty) in - block_not_empty.iter().zip_eq(account_not_empty.iter()).zip_eq(storage_not_empty.iter()) - { - assert_eq!(account_not_empty, account_not_empty && block_not_empty); - assert_eq!(storage_not_empty, storage_not_empty && account_not_empty); - } - Self { responses, block_not_empty, account_not_empty, storage_not_empty, network } - } - - pub fn resize_from( - mut responses: Vec, - mut block_not_empty: Vec, - mut account_not_empty: Vec, - mut storage_not_empty: Vec, - network: Network, - new_len: usize, - ) -> Self { - responses.resize_with(new_len, || DEFAULT_ROW_CONSISTENCY_INPUT.clone()); - block_not_empty.resize(new_len, false); - account_not_empty.resize(new_len, false); - storage_not_empty.resize(new_len, false); - Self::new(responses, block_not_empty, account_not_empty, storage_not_empty, network) - } - - pub fn verify( - self, - loader: &Rc>, - hasher: &mut Poseidon, T, RATE>, - native_hasher: &mut Poseidon, - ) -> Vec> - where - C: CurveAffine, - C::Scalar: Field, - EccChip: EccInstructions, - { - let mut row_data = - [(); ROW_CIRCUIT_NUM_INSTANCES].map(|_| Vec::with_capacity(self.responses.len())); - - let mut tmp_builder = loader.ctx_mut(); - let ctx = tmp_builder.main(FIRST_PHASE); - let gate: &GateChip = &loader.scalar_chip(); - let ((block_not_empty, account_not_empty), storage_not_empty): ((Vec<_>, Vec<_>), Vec<_>) = - self.block_not_empty - .into_iter() - .zip_eq(self.account_not_empty.into_iter()) - .zip_eq(self.storage_not_empty.into_iter()) - .map(|((block_not_empty, account_not_empty), storage_not_empty)| { - let [block_not_empty, account_not_empty, storage_not_empty] = - [block_not_empty, account_not_empty, storage_not_empty] - .map(|x| load_bool(ctx, gate, x)); - let account_not_empty = gate.mul(ctx, block_not_empty, account_not_empty); - // storage can only be empty if account is empty - let storage_not_empty = gate.mul(ctx, account_not_empty, storage_not_empty); - ((block_not_empty, account_not_empty), storage_not_empty) - }) - .unzip(); - drop(tmp_builder); - - for (((full_response, block_not_empty), account_not_empty), storage_not_empty) in self - .responses - .into_iter() - .zip(block_not_empty) - .zip(account_not_empty) - .zip(storage_not_empty) - { - // ================== - // load poseidon hashes of responses as private witnesses and merkle proof into relevant fields - // ================== - // generate block header natively - let ( - (_block_res_p, _block_res_k), - NativeBlockResponse { block_hash, header_list, header_poseidon: _ }, - ) = get_block_response(native_hasher, full_response.block, self.network); - // merkle proof state root all the way into block_response_poseidon - // load witnesses (loader) - let [state_root, block_hash, block_number] = - [&header_list[STATE_ROOT_INDEX], &block_hash, &header_list[BLOCK_NUMBER_INDEX]] - .map(|x| PoseidonWords::from_witness(loader, x)); - // create merkle proof for state root - let merkle_proof = create_merkle_proof(native_hasher, header_list, STATE_ROOT_INDEX); - let merkle_proof = merkle_proof - .into_iter() - .map(|w| PoseidonWords::from_witness(loader, w.0)) - .collect_vec(); - // use proof to compute root - let header_poseidon = - traverse_merkle_proof(hasher, &merkle_proof, state_root.clone(), STATE_ROOT_INDEX); - let mut block_response_poseidon = - poseidon_packed(hasher, block_hash.concat(&block_number).concat(&header_poseidon)); - debug_assert_eq!(block_response_poseidon.assigned().value(), &_block_res_p); - block_response_poseidon *= loader.scalar_from_assigned(block_not_empty); - row_data[ROW_BLOCK_POSEIDON_INDEX] - .push(PoseidonWords::from(block_response_poseidon.clone())); - - // generate account response natively - let ( - (_account_res_p, _), - NativeAccountResponse { state_root: _state_root, state_list, address }, - ) = get_account_response(native_hasher, &full_response.storage); - let [storage_root, _state_root, address] = - [&state_list[STORAGE_ROOT_INDEX], &_state_root, &address] - .map(|x| PoseidonWords::from_witness(loader, x)); - // create merkle proof for storage root - let merkle_proof = create_merkle_proof(native_hasher, state_list, STORAGE_ROOT_INDEX); - let merkle_proof = merkle_proof - .into_iter() - .map(|w| PoseidonWords::from_witness(loader, w.0)) - .collect_vec(); - let state_poseidon = traverse_merkle_proof( - hasher, - &merkle_proof, - storage_root.clone(), - STORAGE_ROOT_INDEX, - ); - let account_response_poseidon = - poseidon_packed(hasher, _state_root.concat(&address).concat(&state_poseidon)); - debug_assert_eq!(account_response_poseidon.assigned().value(), &_account_res_p); - let mut full_acct_res_p = poseidon_packed( - hasher, - PoseidonWords(vec![ - block_response_poseidon.clone(), - account_response_poseidon.clone(), - ]), - ); - - // generate storage response natively - let (_, NativeStorageResponse { storage_root: _, slot, value }) = - get_storage_response(native_hasher, &full_response.storage); - let [slot, value] = [&slot, &value].map(|x| PoseidonWords::from_witness(loader, x)); - let storage_response_poseidon = - poseidon_packed(hasher, storage_root.concat(&slot).concat(&value)); - let mut full_storage_res_p = poseidon_packed( - hasher, - PoseidonWords(vec![ - block_response_poseidon, - account_response_poseidon, - storage_response_poseidon, - ]), - ); - - // ================== - // check row consistency - // ================== - let [account_not_empty, storage_not_empty] = - [account_not_empty, storage_not_empty].map(|x| loader.scalar_from_assigned(x)); - // state_root from block response must equal _state_root from account response unless account is empty - // both roots are H256 in hi-lo form; we do not range check these since they are Poseidon committed to - for (lhs, rhs) in state_root.0.into_iter().zip_eq(_state_root.0.into_iter()) { - loader.assert_eq( - "state root consistency", - &(lhs * &account_not_empty), - &(rhs * &account_not_empty), - ); - } - // we do not need to enforce storage_root since it was direclty used in computation of storage response above - - // in theory the block_response.keccak in the account response could be anything if the account is empty, but for simplicity we enforce that it should be H256::zero() - let [acct_block_res_k, st_block_res_k] = - [&account_not_empty, &storage_not_empty].map(|not_empty| { - PoseidonWords(block_number.0.iter().map(|x| x.clone() * not_empty).collect()) - }); - // in theory the account_response.keccak in the storage response could be anything if the storage is empty, but for simplicity we enforce that it should be H256::zero() - let st_acct_res_k = - PoseidonWords(address.0.iter().map(|x| x.clone() * &storage_not_empty).collect()); - // full account response (poseidon) should be 0 if account is empty - full_acct_res_p *= account_not_empty; - // full storage response (poseidon) should be 0 if storage is empty - full_storage_res_p *= storage_not_empty; - - row_data[ROW_ACCT_POSEIDON_INDEX].push(full_acct_res_p.into()); - row_data[ROW_ACCT_BLOCK_KECCAK_INDEX].push(acct_block_res_k); - row_data[ROW_STORAGE_POSEIDON_INDEX].push(full_storage_res_p.into()); - row_data[ROW_STORAGE_BLOCK_KECCAK_INDEX].push(st_block_res_k); - row_data[ROW_STORAGE_ACCT_KECCAK_INDEX].push(st_acct_res_k); - } - row_data - .into_iter() - .map(|leaves| poseidon_tree_root(hasher, leaves, &[]).into_assigned()) - .collect() - } -} - -impl RowConsistencyCircuit { - fn create( - self, - stage: CircuitBuilderStage, - break_points: Option, - ) -> RangeWithInstanceCircuitBuilder { - let builder = GateThreadBuilder::new(stage == CircuitBuilderStage::Prover); - - let gate = GateChip::default(); - let loader = Halo2Loader::new(DummyEccChip::(&gate), builder); - let mut poseidon = Poseidon::from_spec(&loader, POSEIDON_SPEC.clone()); - let mut native_poseidon = Poseidon::from_spec(&NativeLoader, POSEIDON_SPEC.clone()); - - let assigned_instances = self.verify(&loader, &mut poseidon, &mut native_poseidon); - let builder = loader.take_ctx(); - RangeWithInstanceCircuitBuilder::new( - match stage { - CircuitBuilderStage::Mock => RangeCircuitBuilder::mock(builder), - CircuitBuilderStage::Keygen => RangeCircuitBuilder::keygen(builder), - CircuitBuilderStage::Prover => { - RangeCircuitBuilder::prover(builder, break_points.unwrap()) - } - }, - assigned_instances, - ) - } - - pub fn create_circuit( - self, - stage: CircuitBuilderStage, - break_points: Option, - k: u32, - ) -> RangeWithInstanceCircuitBuilder { - let circuit = self.create(stage, break_points); - - #[cfg(not(feature = "production"))] - if stage != CircuitBuilderStage::Prover { - let minimum_rows = var("UNUSABLE_ROWS").map(|s| s.parse().unwrap_or(10)).unwrap_or(10); - circuit.config(k, Some(minimum_rows)); - } - circuit - } -} - -lazy_static! { - /// Default row. NOTE: block and storage are NOT consistent. Assumed that account and storage should be marked "empty". - pub static ref DEFAULT_ROW_CONSISTENCY_INPUT: EthBlockStorageInput = EthBlockStorageInput { - block: GENESIS_BLOCK.clone(), - block_hash: H256::from_str( - "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3", - ) - .unwrap(), - block_header: GENESIS_BLOCK_RLP.to_vec(), - block_number: 0u32, - storage: DEFAULT_STORAGE_QUERY.clone(), - }; -} diff --git a/axiom-eth/src/batch_query/response/storage.rs b/axiom-eth/src/batch_query/response/storage.rs deleted file mode 100644 index bec164df..00000000 --- a/axiom-eth/src/batch_query/response/storage.rs +++ /dev/null @@ -1,412 +0,0 @@ -//! Storage Response -use std::{cell::RefCell, str::FromStr}; - -use super::*; -use crate::{ - batch_query::{ - hash::{ - bytes_select_or_zero, keccak_packed, poseidon_packed, poseidon_tree_root, - word_select_or_zero, - }, - DummyEccChip, EccInstructions, - }, - keccak::{FixedLenRLCs, FnSynthesize, KeccakChip, VarLenRLCs}, - mpt::MPTFixedKeyInput, - providers::{get_acct_list, get_acct_rlp}, - rlp::{ - builder::{RlcThreadBreakPoints, RlcThreadBuilder}, - rlc::FIRST_PHASE, - RlpChip, - }, - storage::{ - EthStorageChip, EthStorageInput, EthStorageTraceWitness, ACCOUNT_PROOF_VALUE_MAX_BYTE_LEN, - STORAGE_PROOF_VALUE_MAX_BYTE_LEN, {ACCOUNT_PROOF_MAX_DEPTH, STORAGE_PROOF_MAX_DEPTH}, - }, - util::{bytes_be_to_u128, load_bool}, - EthChip, EthCircuitBuilder, EthPreCircuit, Field, ETH_LOOKUP_BITS, -}; -use ethers_core::{ - types::{Address, EIP1186ProofResponse, H256}, - utils::keccak256, -}; -#[cfg(feature = "providers")] -use ethers_providers::{JsonRpcClient, Provider}; -use halo2_base::{ - gates::{GateInstructions, RangeChip, RangeInstructions}, - halo2_proofs::halo2curves::bn256::G1Affine, - utils::ScalarField, - Context, -}; -use itertools::Itertools; -use lazy_static::lazy_static; -use rlp::Encodable; -use serde::{Deserialize, Serialize}; -use snark_verifier::util::hash::Poseidon; -use snark_verifier_sdk::halo2::POSEIDON_SPEC; - -/// A single response to a storage slot query. -/// | Field | Max bytes | -/// |-------------------------|--------------| -/// | storageRoot | 32 | -/// | slot | 32 | -/// | value | ≤32 | -/// -/// We define `storage_response = hash(storageRoot . slot . value)` -/// -/// This struct stores the data needed to compute the above hash. -#[derive(Clone, Debug)] -pub struct StorageResponse { - pub storage_root: FixedByteArray, - pub slot: FixedByteArray, - pub value: FixedByteArray, -} - -impl StorageResponse { - pub fn from_witness( - witness: &EthStorageTraceWitness, - ctx: &mut Context, - gate: &impl GateInstructions, - ) -> Self { - let storage_root = FixedByteArray(witness.mpt_witness.root_hash_bytes.clone()); - let slot = FixedByteArray(witness.slot.clone()); - let value: ByteArray = (&witness.value_witness.witness).into(); - let value = value.to_fixed(ctx, gate); - Self { storage_root, slot, value } - } - - pub fn poseidon( - &self, - loader: &Rc>, - poseidon: &mut Poseidon, T, RATE>, - ) -> Scalar - where - F: Field, - C: CurveAffine, - EccChip: EccInstructions, - { - let [storage_root, slot, value] = - [&self.storage_root, &self.slot, &self.value].map(|x| x.to_poseidon_words(loader)); - poseidon_packed(poseidon, storage_root.concat(&slot).concat(&value)) - } -} - -/// See [`MultiStorageCircuit`] for more details. -/// -/// Assumptions: -/// * `block_responses`, `account_responses`, `storage_responses`, `not_empty` are all of the same length, which is a **power of two**. -/// -/// Returns `keccak_tree_root(full_storage_responses.keccak)` -pub fn get_storage_response_keccak_root<'a, F: Field>( - ctx: &mut Context, - gate: &impl GateInstructions, - keccak: &mut KeccakChip, - block_numbers: impl IntoIterator>, - addresses: impl IntoIterator>, - storage_responses: impl IntoIterator>, - not_empty: impl IntoIterator>, -) -> FixedByteArray { - let full_responses: Vec<_> = block_numbers - .into_iter() - .zip_eq(addresses) - .zip_eq(storage_responses) - .zip_eq(not_empty) - .map(|(((block_num, address), storage), not_empty)| { - let slot_value = storage.slot.concat(&storage.value); - // keccak_storage = keccak(block_response . acct_response . storage_response) - let hash = - keccak_packed(ctx, gate, keccak, block_num.concat(address).concat(&slot_value)); - bytes_select_or_zero(ctx, gate, hash, not_empty).0 - }) - .collect(); - let keccak_root = keccak.merkle_tree_root(ctx, gate, &full_responses); - FixedByteArray(bytes_be_to_u128(ctx, gate, &keccak_root)) -} - -/// See [`MultiStorageCircuit`] for more details. -/// -/// Assumptions: -/// * `block_responses`, `account_responses`, `storage_responses`, `not_empty` are all of the same length, which is a **power of two**. -pub fn get_storage_response_poseidon_roots( - loader: &Rc>, - poseidon: &mut Poseidon, T, RATE>, - block_responses: Vec<(F, FixedByteArray)>, - account_responses: Vec<(F, FixedByteArray)>, - storage_responses: &[StorageResponse], - not_empty: Vec>, -) -> Vec> -where - F: Field, - C: CurveAffine, - EccChip: EccInstructions, -{ - let ((block_numbers, addresses), full_responses): ((Vec<_>, Vec<_>), Vec<_>) = block_responses - .into_iter() - .zip_eq(account_responses.into_iter()) - .zip_eq(storage_responses.iter()) - .zip_eq(not_empty.into_iter()) - .map(|(((block_response, acct_response), storage), not_empty)| { - let storage_response = storage.poseidon(loader, poseidon); - let block_number = block_response.1.to_poseidon_words(loader); - let address = acct_response.1.to_poseidon_words(loader); - // full_response = hash(block_response . acct_response . storage_response) - let hash = poseidon_packed( - poseidon, - PoseidonWords(vec![ - loader.assign_scalar(block_response.0), - loader.assign_scalar(acct_response.0), - storage_response, - ]), - ); - ( - (block_number, address), - PoseidonWords::from(word_select_or_zero(loader, hash, not_empty)), - ) - }) - .unzip(); - let [poseidon_root, block_number_root, address_root] = - [full_responses, block_numbers, addresses] - .map(|leaves| poseidon_tree_root(poseidon, leaves, &[]).into_assigned()); - vec![poseidon_root, block_number_root, address_root] -} - -// switching to just Fr for simplicity: - -/// The input datum for the circuit to generate multiple storage responses. It is used to generate a circuit. -/// -/// Assumptions: -/// * `block_responses`, `account_responses`, `queries`, `not_empty` are all of the same length, which is a **power of two**. -/// * `block_responses` has length greater than 1: the length 1 case still works but cannot be aggregated because -/// the single leaf of `block_responses[0].1` (and of `account_responses[0].1`) would get hashed as two words, -/// whereas in a larger tree it gets concatenated before hashing. -/// -/// The public instances of this circuit are 5 field elements: -/// * Keccak merkle root of `keccak(block_number[i] . address[i], slot[i], value[i])` over all queries: two field elements in hi-lo u128 format -/// * Poseidon merkle root of `poseidon(block_responses[i].0 . account_responses[i].0, storage_responses[i].0)` over all queries: single field element -/// * Poseidon merkle root of `block_number[i]` over all queries: single field element -/// * Poseidon merkle root of `address[i]` over all queries: single field element -/// -/// Above `storage_responses` refers to the hash of `StorageResponse`s generated by the circuit for all queries. -/// Since `block_number, address` are given as private inputs, we expose Poseidon merkle roots of them as public inputs to be checked against BlockResponse and AccountResponse. -#[derive(Clone, Debug, Hash, Serialize, Deserialize)] -pub struct MultiStorageCircuit { - /// The block responses are provided as UNCHECKED private inputs; they will be checked in a separate circuit - pub block_responses: Vec<(Fr, u32)>, - /// The account responses are provided as UNCHECKED private inputs; they will be checked in a separate circuit - pub account_responses: Vec<(Fr, Address)>, - /// The storage queries - pub queries: Vec, - /// Private input to allow full_response_hash[i] to be `Fr::zero()` or `H256(0x0)` for empty response - pub not_empty: Vec, -} - -pub const STORAGE_INSTANCE_SIZE: usize = 5; - -pub(crate) const KECCAK_STORAGE_FULL_RESPONSE_INDEX: usize = 0; -pub(crate) const STORAGE_KECCAK_ROOT_INDICES: &[usize] = &[KECCAK_STORAGE_FULL_RESPONSE_INDEX]; - -pub(crate) const STORAGE_FULL_RESPONSE_POSEIDON_INDEX: usize = 2; -pub(crate) const STORAGE_BLOCK_RESPONSE_KECCAK_INDEX: usize = 3; -pub(crate) const STORAGE_ACCOUNT_RESPONSE_KECCAK_INDEX: usize = 4; -pub(crate) const STORAGE_POSEIDON_ROOT_INDICES: &[usize] = &[ - STORAGE_FULL_RESPONSE_POSEIDON_INDEX, - STORAGE_BLOCK_RESPONSE_KECCAK_INDEX, - STORAGE_ACCOUNT_RESPONSE_KECCAK_INDEX, -]; - -impl MultiStorageCircuit { - /// Creates circuit inputs from raw data. Does basic sanity checks. Number of queries must be a power of two. - pub fn new( - block_responses: Vec<(Fr, u32)>, - account_responses: Vec<(Fr, Address)>, - queries: Vec, - not_empty: Vec, - ) -> Self { - assert!(queries.len() > 1); - assert_eq!(block_responses.len(), account_responses.len()); - assert_eq!(block_responses.len(), not_empty.len()); - assert_eq!(queries.len(), not_empty.len()); - assert!(queries.len().is_power_of_two(), "Number of queries must be a power of 2"); - Self { block_responses, account_responses, queries, not_empty } - } - - /// Creates circuit inputs from a JSON-RPC provider. - #[cfg(feature = "providers")] - pub fn from_provider( - provider: &Provider

, - block_responses: Vec<(Fr, u32)>, - account_responses: Vec<(Fr, Address)>, - queries: Vec<(u64, Address, H256)>, - not_empty: Vec, - ) -> Self { - use crate::providers::get_storage_queries; - let queries = get_storage_queries( - provider, - queries, - ACCOUNT_PROOF_MAX_DEPTH, - STORAGE_PROOF_MAX_DEPTH, - ); - Self::new(block_responses, account_responses, queries, not_empty) - } - - pub fn resize_from( - mut block_responses: Vec<(Fr, u32)>, - mut account_responses: Vec<(Fr, Address)>, - mut queries: Vec, - mut not_empty: Vec, - new_len: usize, - ) -> Self { - block_responses.resize(new_len, (Fr::zero(), 0)); - account_responses.resize(new_len, (Fr::zero(), Address::zero())); - queries.resize_with(new_len, || DEFAULT_STORAGE_QUERY.clone()); - not_empty.resize(new_len, false); - Self::new(block_responses, account_responses, queries, not_empty) - } -} - -impl EthPreCircuit for MultiStorageCircuit { - fn create( - self, - mut builder: RlcThreadBuilder, - break_points: Option, - ) -> EthCircuitBuilder> { - let range = RangeChip::default(ETH_LOOKUP_BITS); - let chip = EthChip::new(RlpChip::new(&range, None), None); - let mut keccak = KeccakChip::default(); - // ================= FIRST PHASE ================ - let ctx = builder.gate_builder.main(FIRST_PHASE); - let queries = self - .queries - .into_iter() - .map(|query| { - let query = query.assign_storage(ctx, &range); - (query.slot.0, query.storage_pf) - }) - .collect_vec(); - let witness = - chip.parse_storage_proofs_phase0(&mut builder.gate_builder, &mut keccak, queries); - let ctx = builder.gate_builder.main(FIRST_PHASE); - let (mut storage_responses, not_empty): (Vec<_>, Vec<_>) = witness - .iter() - .zip_eq(self.not_empty) - .map(|(w, not_empty)| { - let not_empty = load_bool(ctx, range.gate(), not_empty); - (StorageResponse::from_witness(w, ctx, range.gate()), not_empty) - }) - .unzip(); - // set slot value to uint256(0) when the slot does not exist in the storage trie - for (w, storage) in witness.iter().zip(storage_responses.iter_mut()) { - // constrain the MPT proof must have non-zero depth to exclude the unsupported case of an empty storage trie - let depth_is_zero = range.gate.is_zero(ctx, w.mpt_witness.depth); - let depth_is_nonzero = range.gate.not(ctx, depth_is_zero); - range.gate.assert_is_const(ctx, &depth_is_nonzero, &Fr::one()); - - let slot_is_empty = w.mpt_witness.slot_is_empty; - for byte in &mut storage.value.0 { - *byte = range.gate().mul_not(ctx, slot_is_empty, *byte); - } - } - let block_responses = self - .block_responses - .into_iter() - .map(|(word, num)| { - let keccak_bytes = FixedByteArray::new(ctx, &range, &num.to_be_bytes()); - (word, keccak_bytes) - }) - .collect_vec(); - let account_responses = self - .account_responses - .into_iter() - .map(|(word, addr)| { - let keccak_bytes = FixedByteArray::new(ctx, &range, addr.as_bytes()); - (word, keccak_bytes) - }) - .collect_vec(); - // hash responses - let keccak_root = get_storage_response_keccak_root( - ctx, - range.gate(), - &mut keccak, - block_responses.iter().map(|(_, bytes)| bytes), - account_responses.iter().map(|(_, bytes)| bytes), - &storage_responses, - not_empty.clone(), - ); - - let loader = - Halo2Loader::::new(DummyEccChip(range.gate()), builder.gate_builder); - let mut poseidon = Poseidon::from_spec(&loader, POSEIDON_SPEC.clone()); - - let mut assigned_instances = keccak_root.0; - assigned_instances.extend(get_storage_response_poseidon_roots( - &loader, - &mut poseidon, - block_responses, - account_responses, - &storage_responses, - not_empty, - )); - builder.gate_builder = loader.take_ctx(); - - // ================= SECOND PHASE ================ - EthCircuitBuilder::new( - assigned_instances, - builder, - RefCell::new(keccak), - range, - break_points, - move |builder: &mut RlcThreadBuilder, - rlp: RlpChip, - keccak_rlcs: (FixedLenRLCs, VarLenRLCs)| { - // ======== SECOND PHASE =========== - let chip = EthChip::new(rlp, Some(keccak_rlcs)); - chip.parse_storage_proofs_phase1(builder, witness); - }, - ) - } -} - -lazy_static! { - pub static ref DEFAULT_STORAGE_PROOF: EIP1186ProofResponse = serde_json::from_str( - r#"{"address":"0x01d5b501c1fc0121e1411970fb79c322737025c2","balance":"0x0","codeHash":"0xa633dd6234bb961a983a1a1e6d22088dfbbd623dede31449808b7b1eda575b7e","nonce":"0x1","storageHash":"0x209f60372065d53cb2b7d98ffbb1c6c3dcb60c2a30317619da189ccb2c6bad55","accountProof":["0xf90211a01dd0fec2b3f9d15468345ac46f6f57fc94d605c3fea7ca46938b3ecc125d3a4ca0850577263ea02739b6f62b8be0295a75550f80003a11842e5fe5d5afe30ac227a097b6edf8102741cc9b6d71ff2f642ddf33dd92c9ae8aba278517e0b91d2fbaa6a0e09ee77f537b2be646ecd82b264eac2c1845dd386a2935ac3313784158f31dd1a05efaa9476bdcbc6d6b4ddd0f05469824a9050d0492f1964f172beb29048a34bda0787865ffaac02ab287f93750b4dda4210bec374f7ffe3d5ea1d868154b2cc4bca01b046ca2bba9829937a96a9a33a49640a21d73ceaa31c96773cef9ac1cd5971ca037a0acaec88503df71185243fab25f327d07518dc7955da9450fcaa544137fc9a0e7b8d8f71ac648d0627c1c3a384763ce76b11550bc754f39c73ef686b0f0eddea03a7580472f61533e60be8e3d07c99446ad82dbf281546ca319d9324957bccf64a0d23283f05bc9bfa37c6b848832ccb94eef6e5a122d7c01a8fac8682b5f5a6724a0985070f0f845671b55f27534b013f183259ad19fe85e5473dd391a691eedfd99a028d103e427c16fedaa9cf5c038a85c86b901906fcb9bd856f45f4cc3ebee2562a06420015278390e56ec77f1f7dc6974f662da50060403be62b3e1e1913555a12aa0847f86badfd749741d5fca3d45459423fdf246368dfb73ee5e1359b9d3a81768a0f3f59fe568d6e11a0bc5f530ba75845529cf615b7365270a37c739180d37c9b080","0xf90211a0591bec78bcafecec4293f55fcc09da0eb48a77bf5f7790507c1fdce6416d4490a09e087976bc3828fba335f473c7a98df55a734375db72eda2201cdda3392b8af1a075e0854232f5c27abb59a60df7d875a1a87372461b28e4de5d16672b8a190af0a009522bf32fc4d343e40a3e64fd2915ee86694de766480e1135b47b5f2e3742bfa0b318c26d659f0ca40072bf8a37b7d1effa956844f839ae59918ec4695a257aefa0d7ccbc5b52984e066d5129aa0ccc3b4a09fbd43dbd59ecd2175c5ba8dacfa76aa0a7e1e7d10f3dcd1ae968a8cedd5b3cb859cb1eccd659e8d41da0174f68ffe291a08215e04c748b8e7260accc0506fe42c51ceb40d7fad92cb77abe2984cb7398f9a095eff22ca4336d5a65a772b46f8f0ac85b7dc6661108c667d0e7a7addd0bfed0a07ba3addc959ca81049c5dc65667d9d6a73484446fe2f3fe09b480c88d37f72dba02c6fa34d13c6e61ce4163b5590ceed16d4385a8a4dd51a391cedf98b3d94c153a0f525881d63bf204a5f7d8e60696581245dc3a3819963e9c7f587cfe6c3068f1ca0b47f5c965b1ce8ab9418f0570ad7b3b4b6296f9b3cba0b181d528d63e288dce2a0a5cf76a3d817e0fc74e47b15e1fc0eca75c4515120dfba7652cf3f637e5603a7a018c157a9be63ea00f432f94bacac4433cc32af504c616dc23fbd44fcd508d8e7a0c799da8c943c40802bc5553fa2efd5ffe1cc63fc2d3bccdca80c8e42e1c9d5a680","0xf90211a0c1d8a57bb338396435d7ac695fa8d74afdb65691ae29901d5e6bde73fb6b2ea3a00f44d497551cceaab46983cc1d79d3e8e2e4389f985bdaafbede19bbea73d8e4a0d6b55d1744d625bd588749810060e73eb411fe440a256f20580f46a39bdd1e8da0b506b59abbc8efd27e3d7b6aba7ce645c945a6cf3ecd8997d4a0dda0fad3c40da0b1abdd1ae9e9e4b19a583ea4ec0036c20c564fb421713d77c66de5106a21294aa07ef4df175184c42cc2ce7c9a88bb8769a3fd54a0d695fdbab7de0fe69bb4431fa06c537d7439d232a0cd16b17f509cba3c9f148e4e1d3ee2847e1fca9714378f96a06ef083d4b9ee50b9577d00cbc3e725cf154f7e531127c8c16fc8b449b25f73a7a0eb41fea54f05ea279ac0f44a31541fed62dad135faaa280570f549a924c27b66a09a85cc6a4bd49e2777247f00b4753f66612045013c7055e8ffe0ff2c54ebeca4a0ae0372d3b2998eed3f808f7cdd32c2fabb249ffa0ae1971bb3649fc58a160ceca0b185f343a574a15ca2f24d1ec776c0976fdff168bc26e9eef6dbc9ec09afcf0ea0c636a2dc92e4afdcd8430786f61e6d373de50d095e52ca463339c198d5359699a00a3d985dc08852e742257921a3762a14ef7894902ab81b8803383718e6bbb328a0842bccd655d45b45de291da6220e08286f421af7ab93223b23487b06a9dd8613a0e2a5a0f6e34ab2a72f2c31b064f34680cfd6d249314d62c5dfa6a036022c000480","0xf90211a043517cac2a83a56c58aa8f6c850c449f62e832d3fb8a9c2197334ef13719ba13a0ff9f673042cf317d247a1a4c0daa4998f593cba1ba40e80ed60a48d382b52e55a0c3bf4ce63800fa68a0314dcfa32849f873c82141ef622a6c11dbc8b7ee940a85a0a70d654d0718c839cc39971edb2ec70c3258828a523b6a155dcb20e81c48cf70a0f0635baf8c1a8a0ac4a03b73ab73d615c7a8eebaddc35ca0a50ca0ef0962bbf6a01202be6997900e1c699f8a2b7a2cd672c620f81fe8dcb2a407d1ed1c0fa56392a0a9773fca2325dd223ee16dc4c4cc3833a6949460ff13958cba2332173e44b829a0f1d33803fc9608a4b0ef316335297c5e7d22aadb3b5ace10b67e691ec126d11ea0d1bf809ac42ce2fa1f9df3c2d99843cf8a5d9f3c54650ee82dd9666ea7b47d24a0c3eada190e706f5be791c34bb7f35a83514246f3d8ff05bd1f78d6afa1a7f2f6a0740224e6867c4264d22e2617688b54bb3a6800a287056f2a55bcaf43f010fb75a03811a47a97b63f394e3b6fad9dc48bf03021ec993a7c2b3a7e8daa3a77ff78c9a0d3df72d1f0e4152d7c529b9ddce5598021c3004a1428fcee8304b6295687d439a099ea137fc015f2bf06247754bbd21ddbd891e914aa48e2db817afb7c36115f8da03001015ae6d4d974ec474445c8e09f7acd78fba0b34069f0f4ceff48618bd571a0ba1d7ce92b926a621e10b3dbceb60a42059ff3883fb3644c3fb51c96a3cfaa9480","0xf90211a0b0f95458177d0353a5b90006078160e0b2d87686230fee05f229feaf20b4d394a0da99a1217fc5aa21888eb2f845a0598690f6ac8d9cc1d48c294f8a83e75b8eaca0bf541d94d39c03263a6b42184efec7e896a58df03bd5933aae9d7fc6dd83db7da03f46a7a06b873492728188390004981c20562264ca270443ea06e4f55e3bab41a091a645a1d5936bf4ead6177a1225b8d6b92199aa3bea18a8bac109615218a53ca00065275af4349b8296593480173b24de236ec8944e5ff6b4c63682e4eed00a91a0a3ec50593073d617e7ae4cdb22192197c34577857fb532684127bf3b7774702ea0e339fc34ed27c2070926b6fe029c51630fb99381e21813b998933aa6575a4322a034d29a9082f9951c53991f299720d4a833feab0ee24dbccac629bc4dcb79ccc0a089e19dd8d40e42a3902dacc8727fdc5b13ee631d5ff09bd7d77b1536a9bd429da0088c7a47f46758b7d78233b9c116afeff2b11cd7e3bad52bc9a3a84d3c5fd3caa017a3e64263dcd5a4b5143963d87eec7c717d19b4e47aeb4b91c18039c62d502fa0be8d80129c4255b6cc9696f845ae5d07894f2505f32f62263d937ecf26834a91a0884fecc706698cabaf7fcf141bea6030167a93b745c5d232eeaab9078d579494a0df2b6f2700fd2df95b3349f261b1c8f9573dd41dfb4bbd47d2a3be9a1b0b90c2a036576ca9089c79fb55d210de4db554d9e638191fa38e1a37b60683a7d12b5dc180","0xf90211a0270f956b324726c987eaa3cded56b9bef8aaf385458daab7acdd70bfd203ee28a031fdda7c3b21ef9eab72b3e64b7fd3bec6b79a200697f7f4b5156a478e7833fba08dccb69de56beabb4084d50f35fc9cd45f7127ca254b529b13c625838ee15a33a0fad147b7a08030aa316015d13d0cc8a3dbadd3c107dfa7853b5dfffac9a5a738a01e03b5dddc0c2f6666b1c75b29909e7db6cfc26050a2ebbc08788a34e332a280a090b012cd85d1fe14241a1f58c8b02715f6b2d7835e9e5b61422ae05124034536a0113d75e61d8f40e30c4ef41974d81a99978a257af9f187086cd3e768ab20b2caa0a3ca2c0ba374ac77bb2e852dafc5169bfddacca5955a7dc43e8cb6d1d1ea9fbaa0f32d3b24f5495371dfbe423e74889e1d08028517dba2a8bab415cdb09a70ce07a0e5e71a4b10aca494fefe50259e497ba4d64d65f719c170bc9dca2a8178e9e8a3a03795fbe7c7fb69733db3e70e04ac1a297181214f0fca67c15480b7adf95cf52ba0e647bb44f3f3150774d478476b7977f7c2e1c2484746c46263ff74e7c5a9078aa07d7d448016779bd028116dea371aebe74036ed8356bcc07ce457a5b7bdd737dfa0100b0edbd15981e7f97fb293dee6a2fa18df5b9399caf20535158f5009d3c7f1a0a13439769cf90bcd6cd9d311e20f2a87ec616354314e3e1b4b2ff083ff4fc5dca0468329217298d1ffa8e41eab4678d02c7f589eca3c935f0dd191bdf44646947080","0xf90111a00e3484808a216730502daa7ab011fdcc6eb6df9dfcf65a97a170223b71fb0ac080a085a500be60f07ff9d75dcda7c1a9149a15219218188153b9b0227fa4bc523d75a0314d5dceb46c93156dd04a452d883b33bc55532b9f753530576c962cf80dc31880808080a0228d682f2312e51f484d2a3c3e53e987536e5a08afdc5bdd91345eff0c6c07f48080a0a478b8506e6cffc9dddb38a334a15a112d2ed155c2eb02438a4a329821ddf0dea0d37bd3a8c6197e501c6259a6bb50da18900c253f06eabec3b4c17633214c450da076eca457a853dfbfca74edb837bbd53865c0ba85734cece65f06242a6d6fe25c80a03aefcdb44bcd162364bafeddae1b7f8e2c7575e1c1b43032056650e17aab470480","0xf8669d373d3b2842e26222b7332b138c965d6e98a2f70add6324f24cc764fa8eb846f8440180a0209f60372065d53cb2b7d98ffbb1c6c3dcb60c2a30317619da189ccb2c6bad55a0a633dd6234bb961a983a1a1e6d22088dfbbd623dede31449808b7b1eda575b7e"],"storageProof":[{"key":"0x0000000000000000000000000000000000000000000000000000000000000000","proof":["0xf8518080a000ebce886332cd57419a907349ecfbd07043791d641877410ca470e69dcdd9f48080808080808080a06e3176d9ae4126e5bee6550649d603653ecde555bb105b6a81178f8908fb473e8080808080","0xf7a0390decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e5639594f0e3b9aada6d89ddeb34aab7e9cd1744cf90d82f"],"value":"0xf0e3b9aada6d89ddeb34aab7e9cd1744cf90d82f"}]}"# - ).unwrap(); - - pub static ref DEFAULT_STORAGE_QUERY: EthStorageInput = { - let pf = &DEFAULT_STORAGE_PROOF; - let addr = Address::from_str("0x01d5b501c1fc0121e1411970fb79c322737025c2").unwrap(); - let state_root = H256::from_str("0x32b26146b9b2a3ea68eb74585a124f912b8cbfe788696c7a86a79c91086c89f0").unwrap(); - - let acct_key = H256(keccak256(addr)); - let acct_state = get_acct_list(pf); - let acct_pf = MPTFixedKeyInput { - path: acct_key, - value: get_acct_rlp(pf), - root_hash: state_root, - proof: pf.account_proof.iter().map(|x| x.to_vec()).collect(), - value_max_byte_len: ACCOUNT_PROOF_VALUE_MAX_BYTE_LEN, - max_depth: ACCOUNT_PROOF_MAX_DEPTH, - slot_is_empty: false, - }; - let storage_pfs = pf - .storage_proof - .iter() - .map(|storage_pf| { - let path = H256(keccak256(storage_pf.key)); - let value = storage_pf.value.rlp_bytes().to_vec(); - ( - storage_pf.key, - storage_pf.value, - MPTFixedKeyInput { - path, - value, - root_hash: pf.storage_hash, - proof: storage_pf.proof.iter().map(|x| x.to_vec()).collect(), - value_max_byte_len: STORAGE_PROOF_VALUE_MAX_BYTE_LEN, - max_depth: STORAGE_PROOF_MAX_DEPTH, - slot_is_empty: false, - }, - ) - }) - .collect(); - EthStorageInput { addr, acct_state, acct_pf, storage_pfs } - }; -} diff --git a/axiom-eth/src/batch_query/scheduler/circuit_types.rs b/axiom-eth/src/batch_query/scheduler/circuit_types.rs deleted file mode 100644 index c31eb96f..00000000 --- a/axiom-eth/src/batch_query/scheduler/circuit_types.rs +++ /dev/null @@ -1,183 +0,0 @@ -use std::{hash::Hash, path::Path}; - -use itertools::Itertools; -use serde::{Deserialize, Serialize}; - -use crate::{ - util::{scheduler, AggregationConfigPinning, EthConfigPinning, Halo2ConfigPinning}, - Network, -}; - -/// Schema to aggregate the proof of 2total_arity rows of either a single response column or a response table. -/// -/// Let `n = start_arity` and `N = total_arity`. We recursively prove snarks for 2n, 2n, 2n + 1, ..., 2N - 1 entries, for a total of 2N entries. -/// -/// At level `i`, we prove 2n + max(0, i - 1) entries in a single snark and then recursively aggregate this snark with the snark for levels `> i`. The last level is level `N - n`. -/// -/// At level `> 0` we use `PublicAggregationCircuit` for the aggregation. At level `0` we use `{Merkle,Poseidon}AggregationCircuit` to compute Merkle tree roots. -#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct ExponentialSchema { - pub start_arity: usize, - pub total_arity: usize, - pub level: usize, -} - -impl ExponentialSchema { - pub fn new(start_arity: usize, total_arity: usize, level: usize) -> Self { - assert!(start_arity <= total_arity); - assert!(level <= total_arity - start_arity); - Self { start_arity, total_arity, level } - } - - pub fn next(&self) -> Self { - Self::new(self.start_arity, self.total_arity, self.level + 1) - } - - pub fn level_arity(&self) -> usize { - self.start_arity + self.level.saturating_sub(1) - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub enum InitialCircuitType { - Account, - Storage, - RowConsistency, -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct ResponseCircuitType { - pub initial_type: InitialCircuitType, - pub schema: ExponentialSchema, - pub aggregate: bool, -} - -impl scheduler::CircuitType for ResponseCircuitType { - fn name(&self) -> String { - // mostly for human readability, does not need to be collision resistant (we just manually delete old pkeys) - let prefix = match self.initial_type { - InitialCircuitType::Account => "account".to_string(), - InitialCircuitType::Storage => "storage".to_string(), - InitialCircuitType::RowConsistency => "row".to_string(), - }; - if !self.aggregate { - format!("{prefix}_{}", self.schema.level_arity()) - } else { - format!( - "{prefix}_{}_{}_{}", - self.schema.start_arity, self.schema.total_arity, self.schema.level - ) - } - } - - fn get_degree_from_pinning(&self, path: impl AsRef) -> u32 { - let col_degree = |agg: bool| match agg { - false => EthConfigPinning::from_path(path.as_ref()).degree(), - true => AggregationConfigPinning::from_path(path.as_ref()).degree(), - }; - match self.initial_type { - InitialCircuitType::Account => col_degree(self.aggregate), - InitialCircuitType::Storage => col_degree(self.aggregate), - InitialCircuitType::RowConsistency => { - AggregationConfigPinning::from_path(path).degree() - } - } - } -} - -/// Schema to aggregate the proof of 2arities.sum() rows. -/// -/// * `arities`: At level `i` we prove 2arities\[i\] entries if i = 0 or aggregate 2arities\[i\] previous proofs. -/// Here level is “reverse” from depth, so the root has the highest level, while leaf is level 0. -#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct BlockVerifyVsMmrCircuitType { - pub network: Network, - pub arities: Vec, -} - -impl scheduler::CircuitType for BlockVerifyVsMmrCircuitType { - fn name(&self) -> String { - format!("{}_block_{}", self.network, self.arities.iter().join("_")) - } - fn get_degree_from_pinning(&self, pinning_path: impl AsRef) -> u32 { - if self.arities.len() == 1 { - EthConfigPinning::from_path(pinning_path.as_ref()).degree() - } else { - AggregationConfigPinning::from_path(pinning_path.as_ref()).degree() - } - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct FinalAssemblyCircuitType { - /// Performs `round` rounds of SNARK verification using `PublicAggregationCircuit` on the final circuit. - /// This is used to reduce circuit size and final EVM verification gas costs. - pub round: usize, - pub network: Network, - pub block_arities: Vec, - pub account_schema: ExponentialSchema, - pub storage_schema: ExponentialSchema, - pub row_schema: ExponentialSchema, -} - -impl FinalAssemblyCircuitType { - pub fn new( - round: usize, - network: Network, - block_arities: Vec, - account_schema: ExponentialSchema, - storage_schema: ExponentialSchema, - row_schema: ExponentialSchema, - ) -> Self { - let block_arity = block_arities.iter().sum::(); - assert_eq!(block_arity, account_schema.total_arity); - assert_eq!(block_arity, storage_schema.total_arity); - assert_eq!(block_arity, row_schema.total_arity); - Self { round, network, block_arities, account_schema, storage_schema, row_schema } - } -} - -impl scheduler::CircuitType for FinalAssemblyCircuitType { - fn name(&self) -> String { - format!("final_{}", self.round) - } - - fn get_degree_from_pinning(&self, pinning_path: impl AsRef) -> u32 { - if self.round == 0 { - EthConfigPinning::from_path(pinning_path.as_ref()).degree() - } else { - AggregationConfigPinning::from_path(pinning_path.as_ref()).degree() - } - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub enum CircuitType { - /// Circuit to either create a response column or check consistency of the response table - Response(ResponseCircuitType), - /// Circuit to verify block responses against block hash MMR - VerifyVsMmr(BlockVerifyVsMmrCircuitType), - /// Circuit to aggregate all response columns into a single response table and verify consistency. - Final(FinalAssemblyCircuitType), -} - -impl scheduler::CircuitType for CircuitType { - fn name(&self) -> String { - match self { - CircuitType::Response(circuit_type) => circuit_type.name(), - CircuitType::VerifyVsMmr(circuit_type) => circuit_type.name(), - CircuitType::Final(circuit_type) => circuit_type.name(), - } - } - fn get_degree_from_pinning(&self, pinning_path: impl AsRef) -> u32 { - match self { - CircuitType::Response(circuit_type) => { - circuit_type.get_degree_from_pinning(pinning_path) - } - CircuitType::VerifyVsMmr(circuit_type) => { - circuit_type.get_degree_from_pinning(pinning_path) - } - CircuitType::Final(circuit_type) => circuit_type.get_degree_from_pinning(pinning_path), - } - } -} diff --git a/axiom-eth/src/batch_query/scheduler/mod.rs b/axiom-eth/src/batch_query/scheduler/mod.rs deleted file mode 100644 index df70668c..00000000 --- a/axiom-eth/src/batch_query/scheduler/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod circuit_types; -mod precircuits; -pub mod router; -pub mod tasks; diff --git a/axiom-eth/src/batch_query/scheduler/precircuits.rs b/axiom-eth/src/batch_query/scheduler/precircuits.rs deleted file mode 100644 index 3b57ebcc..00000000 --- a/axiom-eth/src/batch_query/scheduler/precircuits.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::env::var; - -use halo2_base::{ - gates::builder::CircuitBuilderStage, - halo2_proofs::{ - halo2curves::bn256::{Bn256, Fr}, - poly::{commitment::Params, kzg::commitment::ParamsKZG}, - }, -}; - -use crate::{ - batch_query::{ - aggregation::{ - FinalResponseAssemblyCircuit, MultiBlockAggregationCircuit, PoseidonAggregationCircuit, - }, - response::row_consistency::RowConsistencyCircuit, - }, - util::{ - circuit::{PinnableCircuit, PreCircuit}, - AggregationConfigPinning, EthConfigPinning, Halo2ConfigPinning, - }, - AggregationPreCircuit, -}; - -// MultiBlockCircuit, MultiAccountCircuit, MultiStorageCircuit are all EthPreCircuits, which auto-implement PreCircuit -// Rust does not allow two different traits to both auto-implement PreCircuit (because it cannot then determine conflicting implementations), so the rest we do manually: - -impl PreCircuit for RowConsistencyCircuit { - type Pinning = AggregationConfigPinning; - - fn create_circuit( - self, - stage: CircuitBuilderStage, - pinning: Option, - params: &ParamsKZG, - ) -> impl PinnableCircuit { - let break_points = pinning.map(|p| p.break_points()); - RowConsistencyCircuit::create_circuit(self, stage, break_points, params.k()) - } -} - -impl PreCircuit for PoseidonAggregationCircuit { - type Pinning = AggregationConfigPinning; - - fn create_circuit( - self, - stage: CircuitBuilderStage, - pinning: Option, - params: &ParamsKZG, - ) -> impl PinnableCircuit { - // look for lookup_bits either from pinning, if available, or from env var - let lookup_bits = pinning - .as_ref() - .map(|p| p.params.lookup_bits) - .or_else(|| var("LOOKUP_BITS").map(|v| v.parse().unwrap()).ok()) - .expect("LOOKUP_BITS is not set"); - let break_points = pinning.map(|p| p.break_points()); - AggregationPreCircuit::create_circuit(self, stage, break_points, lookup_bits, params) - } -} - -impl PreCircuit for MultiBlockAggregationCircuit { - type Pinning = AggregationConfigPinning; - - fn create_circuit( - self, - stage: CircuitBuilderStage, - pinning: Option, - params: &ParamsKZG, - ) -> impl PinnableCircuit { - // look for lookup_bits either from pinning, if available, or from env var - let lookup_bits = pinning - .as_ref() - .map(|p| p.params.lookup_bits) - .or_else(|| var("LOOKUP_BITS").map(|v| v.parse().unwrap()).ok()) - .expect("LOOKUP_BITS is not set"); - let break_points = pinning.map(|p| p.break_points()); - AggregationPreCircuit::create_circuit(self, stage, break_points, lookup_bits, params) - } -} - -impl PreCircuit for FinalResponseAssemblyCircuit { - type Pinning = EthConfigPinning; - - fn create_circuit( - self, - stage: CircuitBuilderStage, - pinning: Option, - params: &ParamsKZG, - ) -> impl PinnableCircuit { - // look for lookup_bits either from pinning, if available, or from env var - let lookup_bits = pinning - .as_ref() - .map(|p| p.params.lookup_bits.unwrap()) - .or_else(|| var("LOOKUP_BITS").map(|v| v.parse().unwrap()).ok()) - .expect("LOOKUP_BITS is not set"); - let break_points = pinning.map(|p| p.break_points()); - FinalResponseAssemblyCircuit::create_circuit(self, stage, break_points, lookup_bits, params) - } -} diff --git a/axiom-eth/src/batch_query/scheduler/router.rs b/axiom-eth/src/batch_query/scheduler/router.rs deleted file mode 100644 index 8af26d1f..00000000 --- a/axiom-eth/src/batch_query/scheduler/router.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::path::Path; - -use any_circuit_derive::AnyCircuit; -use halo2_base::halo2_proofs::{ - halo2curves::bn256::{Bn256, G1Affine}, - plonk::ProvingKey, - poly::kzg::commitment::ParamsKZG, -}; -use itertools::Itertools; -use snark_verifier_sdk::Snark; - -use crate::{ - batch_query::{ - aggregation::{ - FinalResponseAssemblyCircuit, HashStrategy, MultiBlockAggregationCircuit, - PoseidonAggregationCircuit, - }, - response::{ - account::MultiAccountCircuit, - block_header::MultiBlockCircuit, - row_consistency::{RowConsistencyCircuit, ROW_CIRCUIT_NUM_INSTANCES}, - storage::MultiStorageCircuit, - }, - }, - util::{ - circuit::{AnyCircuit, PublicAggregationCircuit}, - scheduler::{self, EthScheduler}, - }, -}; - -use super::tasks::{ResponseInput, Task}; - -#[allow(clippy::large_enum_variant)] -#[derive(Clone, Debug, AnyCircuit)] -pub enum CircuitRouter { - // response circuits - InitialAccount(MultiAccountCircuit), - InitialStorage(MultiStorageCircuit), - RowConsistency(RowConsistencyCircuit), - BlockVerifyVsMmr(MultiBlockCircuit), - // aggregation circuits - FinalAssembly(FinalResponseAssemblyCircuit), - Passthrough(PublicAggregationCircuit), - Poseidon(PoseidonAggregationCircuit), - BlockVerifyMmrAggregation(MultiBlockAggregationCircuit), -} - -pub type BatchQueryScheduler = EthScheduler; - -impl scheduler::Scheduler for BatchQueryScheduler { - type Task = Task; - type CircuitRouter = CircuitRouter; - - fn get_circuit(&self, task: Task, prev_snarks: Vec) -> CircuitRouter { - match task { - Task::Final(final_task) => { - if final_task.circuit_type.round != 0 { - assert_eq!(prev_snarks.len(), 1); - return CircuitRouter::Passthrough(PublicAggregationCircuit::new( - prev_snarks.into_iter().map(|snark| (snark, true)).collect(), - )); - } - let circuit_type = &final_task.circuit_type; - let block_has_acc = circuit_type.block_arities.len() != 1; - let [account_has_acc, storage_has_acc, row_has_acc] = [ - &circuit_type.account_schema, - &circuit_type.storage_schema, - &circuit_type.row_schema, - ] - .map(|schema| schema.start_arity != schema.total_arity); - let [block_snark, account_snark, storage_snark, row_snark]: [_; 4] = - prev_snarks.try_into().unwrap(); - CircuitRouter::FinalAssembly(FinalResponseAssemblyCircuit::new( - (block_snark, block_has_acc), - (account_snark, account_has_acc), - (storage_snark, storage_has_acc), - (row_snark, row_has_acc), - )) - } - Task::Response(task) => { - if task.aggregate { - let acc = (task.schema.level + 1) - != (task.schema.total_arity - task.schema.start_arity); - let prev_snarks = prev_snarks - .into_iter() - .zip_eq([false, acc]) - .map(|(snark, has_acc)| (snark, has_acc)) - .collect_vec(); - if !matches!(&task.input, ResponseInput::Row(_)) || task.schema.level != 0 { - CircuitRouter::Passthrough(PublicAggregationCircuit::new(prev_snarks)) - } else { - // final aggregation of row consistency should do poseidon hash onion - CircuitRouter::Poseidon(PoseidonAggregationCircuit::new( - HashStrategy::Onion, - prev_snarks, - ROW_CIRCUIT_NUM_INSTANCES, // all poseidon roots - )) - } - } else { - match task.input { - ResponseInput::Account(input) => CircuitRouter::InitialAccount(input), - ResponseInput::Storage(input) => CircuitRouter::InitialStorage(input), - ResponseInput::Row(input) => CircuitRouter::RowConsistency(input), - } - } - } - Task::BlockVerifyVsMmr(task) => { - if task.arities.len() == 1 { - CircuitRouter::BlockVerifyVsMmr(task.input) - } else { - let has_acc = task.arities.len() > 2; - CircuitRouter::BlockVerifyMmrAggregation(MultiBlockAggregationCircuit { - snarks: prev_snarks.into_iter().map(|snark| (snark, has_acc)).collect(), - }) - } - } - } - } -} diff --git a/axiom-eth/src/batch_query/scheduler/tasks.rs b/axiom-eth/src/batch_query/scheduler/tasks.rs deleted file mode 100644 index 3b904d98..00000000 --- a/axiom-eth/src/batch_query/scheduler/tasks.rs +++ /dev/null @@ -1,400 +0,0 @@ -use ethers_core::{ - types::{Address, H256}, - utils::keccak256, -}; -use halo2_base::halo2_proofs::halo2curves::bn256::Fr; -use itertools::Itertools; -use serde::{Deserialize, Serialize}; -use snark_verifier::util::hash::Poseidon; -use snark_verifier_sdk::{halo2::POSEIDON_SPEC, NativeLoader}; - -use crate::{ - batch_query::response::{ - account::MultiAccountCircuit, - block_header::MultiBlockCircuit, - native::{get_account_response, get_block_response, FullStorageResponse}, - row_consistency::RowConsistencyCircuit, - storage::{MultiStorageCircuit, DEFAULT_STORAGE_QUERY}, - }, - storage::{EthBlockStorageInput, EthStorageInput}, - util::scheduler, -}; - -use super::circuit_types::{ - BlockVerifyVsMmrCircuitType, CircuitType, ExponentialSchema, FinalAssemblyCircuitType, - InitialCircuitType, ResponseCircuitType, -}; - -/// The input queries for {block, account, storage} column task. -/// -/// The lengths of the queries do not need to be a power of two, because we will -/// pad it with "default" entries to optimize caching. -#[allow(clippy::large_enum_variant)] -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum ResponseInput { - Account(MultiAccountCircuit), - Storage(MultiStorageCircuit), - Row(RowConsistencyCircuit), -} - -impl ResponseInput { - fn initial_type(&self) -> InitialCircuitType { - match self { - ResponseInput::Account(_) => InitialCircuitType::Account, - ResponseInput::Storage(_) => InitialCircuitType::Storage, - ResponseInput::Row(_) => InitialCircuitType::RowConsistency, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ResponseTask { - pub input: ResponseInput, - pub schema: ExponentialSchema, - pub aggregate: bool, -} - -impl scheduler::Task for ResponseTask { - type CircuitType = ResponseCircuitType; - - fn circuit_type(&self) -> Self::CircuitType { - ResponseCircuitType { - initial_type: self.input.initial_type(), - schema: self.schema.clone(), - aggregate: self.aggregate, - } - } - - /// This needs to be collision-resistant because we are using file system for caching right now. - // Not the most efficient, but we're just going to serialize the whole task and keccak it - fn name(&self) -> String { - let hash = match &self.input { - ResponseInput::Row(_) => { - // something about `Block` makes this hard to serialize with bincode - keccak256(serde_json::to_vec(&self).expect("failed to serialize task")) - } - _ => keccak256(bincode::serialize(&self).expect("failed to serialize task")), - }; - format!("{:?}", H256(hash)) - } - - fn dependencies(&self) -> Vec { - if !self.aggregate || self.schema.level == self.schema.total_arity - self.schema.start_arity - { - return vec![]; - } - let arity = self.schema.level_arity(); - let mid = 1 << arity; - let prev_inputs = match &self.input { - ResponseInput::Account(input) => { - let (block_responses1, block_responses2) = input.block_responses.split_at(mid); - let (queries1, queries2) = input.queries.split_at(mid); - let (not_empty1, not_empty2) = input.not_empty.split_at(mid); - [block_responses1, block_responses2] - .zip([queries1, queries2]) - .zip([not_empty1, not_empty2]) - .map(|((block_responses, queries), not_empty)| MultiAccountCircuit { - block_responses: block_responses.to_vec(), - queries: queries.to_vec(), - not_empty: not_empty.to_vec(), - }) - .map(ResponseInput::Account) - } - ResponseInput::Storage(input) => { - let (block_responses1, block_responses2) = input.block_responses.split_at(mid); - let (account_responses1, account_responses2) = - input.account_responses.split_at(mid); - let (queries1, queries2) = input.queries.split_at(mid); - let (not_empty1, not_empty2) = input.not_empty.split_at(mid); - [block_responses1, block_responses2] - .zip([account_responses1, account_responses2]) - .zip([queries1, queries2]) - .zip([not_empty1, not_empty2]) - .map(|(((block_responses, account_responses), queries), not_empty)| { - MultiStorageCircuit { - block_responses: block_responses.to_vec(), - account_responses: account_responses.to_vec(), - queries: queries.to_vec(), - not_empty: not_empty.to_vec(), - } - }) - .map(ResponseInput::Storage) - } - ResponseInput::Row(input) => { - let (responses1, responses2) = input.responses.split_at(mid); - let (block_not_empty1, block_not_empty2) = input.block_not_empty.split_at(mid); - let (account_not_empty1, account_not_empty2) = - input.account_not_empty.split_at(mid); - let (storage_not_empty1, storage_not_empty2) = - input.storage_not_empty.split_at(mid); - [responses1, responses2] - .zip([block_not_empty1, block_not_empty2]) - .zip([account_not_empty1, account_not_empty2]) - .zip([storage_not_empty1, storage_not_empty2]) - .map( - |(((responses, block_not_empty), account_not_empty), storage_not_empty)| { - RowConsistencyCircuit { - responses: responses.to_vec(), - block_not_empty: block_not_empty.to_vec(), - account_not_empty: account_not_empty.to_vec(), - storage_not_empty: storage_not_empty.to_vec(), - network: input.network, - } - }, - ) - .map(ResponseInput::Row) - } - }; - let next_schema = self.schema.next(); - let agg = self.schema.level != (self.schema.total_arity - self.schema.start_arity - 1); - prev_inputs - .into_iter() - .zip([self.schema.clone(), next_schema]) - .zip([false, agg]) - .map(|((input, schema), aggregate)| Self { input, schema, aggregate }) - .collect() - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct BlockVerifyVsMmrTask { - pub input: MultiBlockCircuit, - pub arities: Vec, -} - -impl scheduler::Task for BlockVerifyVsMmrTask { - type CircuitType = BlockVerifyVsMmrCircuitType; - - fn circuit_type(&self) -> Self::CircuitType { - BlockVerifyVsMmrCircuitType { network: self.input.network, arities: self.arities.clone() } - } - - fn name(&self) -> String { - format!( - "{:?}", - H256(keccak256(bincode::serialize(&self).expect("failed to serialize task"))) - ) - } - - fn dependencies(&self) -> Vec { - assert!(!self.arities.is_empty()); - if self.arities.len() == 1 { - return vec![]; - } - let arity = self.arities.last().unwrap(); - let prev_arities = self.arities[..self.arities.len() - 1].to_vec(); - let prev_arity = prev_arities.iter().sum::(); - let chunk_size = 1 << prev_arity; - let num_chunks = 1 << arity; - - let mut prev_inputs = self - .input - .header_rlp_encodings - .chunks(chunk_size) - .zip_eq(self.input.not_empty.chunks(chunk_size)) - .zip_eq(self.input.mmr_proofs.chunks(chunk_size)) - .map(|((header_rlps, not_empty), mmr_proofs)| { - MultiBlockCircuit::resize_from( - header_rlps.to_vec(), - not_empty.to_vec(), - self.input.network, - self.input.mmr.to_vec(), - self.input.mmr_list_len, - mmr_proofs.iter().map(|pf| pf.to_vec()).collect(), - chunk_size, - ) - }) - .collect_vec(); - let dup = prev_inputs[0].clone(); - prev_inputs.resize(num_chunks, dup); - - prev_inputs.into_iter().map(|input| Self { input, arities: prev_arities.clone() }).collect() - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct FinalAssemblyTask { - pub circuit_type: FinalAssemblyCircuitType, - pub input: Vec, - pub mmr: Vec, - pub mmr_num_blocks: usize, - pub mmr_proofs: Vec>, -} - -#[allow(clippy::large_enum_variant)] -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum Task { - Response(ResponseTask), - BlockVerifyVsMmr(BlockVerifyVsMmrTask), - Final(FinalAssemblyTask), -} - -impl scheduler::Task for Task { - type CircuitType = CircuitType; - - fn circuit_type(&self) -> Self::CircuitType { - match self { - Task::Response(task) => CircuitType::Response(task.circuit_type()), - Task::BlockVerifyVsMmr(task) => CircuitType::VerifyVsMmr(task.circuit_type()), - Task::Final(task) => CircuitType::Final(task.circuit_type.clone()), - } - } - - fn name(&self) -> String { - match self { - Task::Response(task) => task.name(), - Task::BlockVerifyVsMmr(task) => task.name(), - Task::Final(task) => { - format!( - "final_{:?}", - H256(keccak256(serde_json::to_vec(&task).expect("failed to serialize task"))) - ) - } - } - } - - fn dependencies(&self) -> Vec { - match self { - Task::Response(task) => task.dependencies().into_iter().map(Task::Response).collect(), - Task::BlockVerifyVsMmr(task) => { - task.dependencies().into_iter().map(Task::BlockVerifyVsMmr).collect() - } - Task::Final(task) => { - if task.circuit_type.round != 0 { - let mut circuit_type = task.circuit_type.clone(); - circuit_type.round -= 1; - return vec![Task::Final(FinalAssemblyTask { circuit_type, ..task.clone() })]; - } - let circuit_type = &task.circuit_type; - - let total_arity: usize = circuit_type.block_arities.iter().sum::(); - assert_eq!(total_arity, circuit_type.account_schema.total_arity); - assert_eq!(total_arity, circuit_type.storage_schema.total_arity); - assert_eq!(total_arity, circuit_type.row_schema.total_arity); - let mut account_schema = circuit_type.account_schema.clone(); - let mut storage_schema = circuit_type.storage_schema.clone(); - let mut row_schema = circuit_type.row_schema.clone(); - account_schema.level = 0; - storage_schema.level = 0; - row_schema.level = 0; - - let network = circuit_type.network; - let len = 1 << total_arity; - let mut block_header_rlps = Vec::with_capacity(len); - let mut block_headers_poseidon = Vec::with_capacity(len); - let mut block_responses = Vec::with_capacity(len); - let mut account_responses = Vec::with_capacity(len); - let mut storage_inputs = Vec::with_capacity(len); - let mut block_not_empty = Vec::with_capacity(len); - let mut account_not_empty = Vec::with_capacity(len); - let mut storage_not_empty = Vec::with_capacity(len); - let mut responses = Vec::with_capacity(len); - - let mut poseidon = Poseidon::from_spec(&NativeLoader, POSEIDON_SPEC.clone()); - let hasher = &mut poseidon; - - for input in &task.input { - let mut response = EthBlockStorageInput::from(input.clone()); - block_header_rlps.push(response.block_header.clone()); - let ((block_res_p, _block_res_k), block_res) = - get_block_response(hasher, input.block.clone(), network); - block_responses.push((block_res_p, input.block.number.unwrap().as_u32())); - block_not_empty.push(true); - block_headers_poseidon.push(block_res.header_poseidon); - - if let Some(acct_storage) = &input.account_storage { - let ((acct_res_p, _acct_res_k), _) = - get_account_response(hasher, acct_storage); - account_responses.push((acct_res_p, acct_storage.addr)); - account_not_empty.push(true); - if !acct_storage.storage_pfs.is_empty() { - assert!(acct_storage.storage_pfs.len() == 1); - storage_inputs.push(acct_storage.clone()); - storage_not_empty.push(true); - } else { - response.storage.storage_pfs = - DEFAULT_STORAGE_QUERY.storage_pfs.clone(); - storage_inputs.push(response.storage.clone()); - storage_not_empty.push(false); - } - } else { - account_responses.push((Fr::zero(), Address::zero())); - account_not_empty.push(false); - storage_inputs.push(DEFAULT_STORAGE_QUERY.clone()); - storage_not_empty.push(false); - } - responses.push(response); - } - - let block_task = MultiBlockCircuit::resize_from( - block_header_rlps, - block_not_empty.clone(), - network, - task.mmr.clone(), - task.mmr_num_blocks, - task.mmr_proofs.clone(), - len, - ); - let acct_inputs = storage_inputs - .iter() - .map(|input| EthStorageInput { storage_pfs: vec![], ..input.clone() }) - .collect_vec(); - let acct_task = MultiAccountCircuit::resize_from( - block_responses - .iter() - .zip(account_not_empty.iter()) - .map(|(res, &ne)| if ne { *res } else { (Fr::zero(), 0) }) - .collect_vec(), - acct_inputs, - account_not_empty.clone(), - len, - ); - let storage_task = MultiStorageCircuit::resize_from( - block_responses - .iter() - .zip(storage_not_empty.iter()) - .map(|(res, &ne)| if ne { *res } else { (Fr::zero(), 0) }) - .collect_vec(), - account_responses - .iter() - .zip(storage_not_empty.iter()) - .map(|(res, &ne)| if ne { *res } else { (Fr::zero(), Address::zero()) }) - .collect_vec(), - storage_inputs, - storage_not_empty.clone(), - len, - ); - let row_consistency_task = RowConsistencyCircuit::resize_from( - responses, - block_not_empty, - account_not_empty, - storage_not_empty, - network, - len, - ); - - vec![ - Task::BlockVerifyVsMmr(BlockVerifyVsMmrTask { - input: block_task, - arities: circuit_type.block_arities.clone(), - }), - Task::Response(ResponseTask { - aggregate: account_schema.start_arity != account_schema.total_arity, - schema: account_schema, - input: ResponseInput::Account(acct_task), - }), - Task::Response(ResponseTask { - aggregate: storage_schema.start_arity != storage_schema.total_arity, - schema: storage_schema, - input: ResponseInput::Storage(storage_task), - }), - Task::Response(ResponseTask { - aggregate: row_schema.start_arity != row_schema.total_arity, - schema: row_schema, - input: ResponseInput::Row(row_consistency_task), - }), - ] - } - } - } -} diff --git a/axiom-eth/src/batch_query/tests/account.rs b/axiom-eth/src/batch_query/tests/account.rs deleted file mode 100644 index 9ba1fa3d..00000000 --- a/axiom-eth/src/batch_query/tests/account.rs +++ /dev/null @@ -1,124 +0,0 @@ -use super::setup_provider; -use crate::{ - batch_query::{ - hash::{poseidon_tree_root, PoseidonWords}, - response::{ - account::MultiAccountCircuit, - native::{get_account_response, get_full_account_response}, - }, - tests::get_latest_block_number, - }, - rlp::builder::RlcThreadBuilder, - storage::EthStorageInput, - util::{encode_h256_to_field, h256_tree_root, EthConfigParams}, - EthPreCircuit, -}; -use ethers_core::types::H256; -use ff::Field; -use halo2_base::halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr}; -use itertools::Itertools; -use rand::{thread_rng, Rng}; -use rand_core::OsRng; -use snark_verifier::util::hash::Poseidon; -use snark_verifier_sdk::{halo2::POSEIDON_SPEC, NativeLoader}; -use std::env::set_var; -use test_log::test; - -pub fn native_account_instance( - block_responses: &[(Fr, u32)], - queries: &[EthStorageInput], - not_empty: &[bool], -) -> Vec { - // instance calculation natively for test validation - let mut poseidon = Poseidon::from_spec(&NativeLoader, POSEIDON_SPEC.clone()); - let block_numbers = - block_responses.iter().map(|res| PoseidonWords(vec![Fr::from(res.1 as u64)])).collect_vec(); - let block_num_root_p = poseidon_tree_root(&mut poseidon, block_numbers, &[]); - let (res_p, res_k): (Vec<_>, Vec<_>) = block_responses - .iter() - .zip_eq(queries.iter()) - .zip_eq(not_empty.iter()) - .map(|((block_response, query), not_empty)| { - let (acct_res, _) = get_account_response(&mut poseidon, query); - let (mut pos, mut kec) = - get_full_account_response(&mut poseidon, *block_response, acct_res); - if !not_empty { - pos = Fr::zero(); - kec = H256([0u8; 32]); - } - (pos, kec) - }) - .unzip(); - let root_p = poseidon_tree_root(&mut poseidon, res_p, &[]); - let root_k = h256_tree_root(&res_k); - let root_k = encode_h256_to_field(&root_k); - vec![root_k[0], root_k[1], root_p, block_num_root_p] -} - -fn test_mock_account_queries( - block_responses: Vec<(Fr, u32)>, - queries: Vec<(u64, &str)>, - not_empty: Vec, -) { - let params = EthConfigParams::from_path("configs/tests/account_query.json"); - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(¶ms).unwrap()); - let k = params.degree; - - let queries = queries - .into_iter() - .map(|(block_number, address)| { - let address = address.parse().unwrap(); - (block_number, address) - }) - .collect(); - - let input = - MultiAccountCircuit::from_provider(&setup_provider(), block_responses, queries, not_empty); - let instance = - native_account_instance(&input.block_responses, &input.queries, &input.not_empty); - let circuit = input.create_circuit(RlcThreadBuilder::mock(), None); - - MockProver::run(k, &circuit, vec![instance]).unwrap().assert_satisfied(); -} - -#[test] -fn test_mock_account_queries_simple() { - let queries = vec![ - (17143006, "0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B"), - (17143000, "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"), - (15000000, "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"), - (15411056, "0x1c479675ad559DC151F6Ec7ed3FbF8ceE79582B6"), - ]; - let mut rng = thread_rng(); - let block_responses = queries.iter().map(|_| (Fr::random(OsRng), rng.gen())).collect(); - let not_empty = vec![true; queries.len()]; - test_mock_account_queries(block_responses, queries, not_empty); -} - -#[test] -fn test_mock_account_queries_genesis() { - // address existing in block 0 - let address = "0x756F45E3FA69347A9A973A725E3C98bC4db0b5a0"; - let mut rng = thread_rng(); - let latest = get_latest_block_number(); - let mut queries: Vec<_> = (0..7).map(|_| (rng.gen_range(0..latest), address)).collect(); - queries.push((0, address)); - let block_responses = queries.iter().map(|_| (Fr::random(OsRng), rng.gen())).collect(); - let mut not_empty = vec![true; queries.len()]; - for ne in not_empty.iter_mut().take(queries.len() / 2) { - *ne = false; - } - test_mock_account_queries(block_responses, queries, not_empty); -} - -#[test] -fn test_mock_account_queries_empty() { - let address = "0x0000000000000000000000000000000000000000"; - let mut rng = thread_rng(); - let latest = get_latest_block_number(); - let mut queries: Vec<_> = (0..7).map(|_| (rng.gen_range(0..latest), address)).collect(); - queries.push((0, address)); - let block_responses = queries.iter().map(|_| (Fr::random(OsRng), rng.gen())).collect(); - let not_empty = vec![false; queries.len()]; - test_mock_account_queries(block_responses, queries, not_empty); -} diff --git a/axiom-eth/src/batch_query/tests/aggregation.rs b/axiom-eth/src/batch_query/tests/aggregation.rs deleted file mode 100644 index 24ee5049..00000000 --- a/axiom-eth/src/batch_query/tests/aggregation.rs +++ /dev/null @@ -1,206 +0,0 @@ -use crate::{ - batch_query::{ - aggregation::{HashStrategy, PoseidonAggregationCircuit}, - response::row_consistency::{RowConsistencyCircuit, ROW_CIRCUIT_NUM_INSTANCES}, - tests::storage::get_full_storage_inputs_nouns, - }, - storage::EthBlockStorageInput, - AggregationPreCircuit, Network, -}; - -use halo2_base::{ - gates::builder::CircuitBuilderStage, halo2_proofs::dev::MockProver, utils::fs::gen_srs, -}; -use itertools::Itertools; -use rand::{thread_rng, Rng}; - -use snark_verifier_sdk::{gen_pk, halo2::gen_snark_shplonk, LIMBS}; -use std::env::set_var; -use test_log::test; - -/*fn create_account_snark( - provider: &Provider, - block_responses: Vec<(Fr, H256)>, - queries: Vec<(u64, Address)>, - not_empty: Vec, -) -> Snark { - let params = EthConfigParams::from_path("configs/tests/account_query.json"); - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(¶ms).unwrap()); - let k = params.degree; - - let input = MultiAccountCircuit::from_provider(provider, block_responses, queries, not_empty); - let params = gen_srs(k); - let circuit = input.create_circuit(CircuitBuilderStage::Mock, None, ¶ms); - let pk = gen_pk(¶ms, &circuit, None); - gen_snark_shplonk(¶ms, &pk, circuit, None::<&str>) -} - -fn test_mock_account_aggregation_depth( - block_responses: Vec<(Fr, H256)>, - queries: Vec<(u64, &str)>, - not_empty: Vec, - depth: usize, -) { - let provider = setup_provider(); - let chunk_size = queries.len() >> depth; - assert!(chunk_size != 0, "depth too large"); - let queries = queries - .into_iter() - .map(|(block_number, address)| { - let address = address.parse().unwrap(); - (block_number, address) - }) - .collect_vec(); - let snarks = block_responses - .chunks(chunk_size) - .zip(queries.chunks(chunk_size).zip(not_empty.chunks(chunk_size))) - .into_iter() - .map(|(block_responses, (queries, not_empty))| { - ( - create_account_snark( - &provider, - block_responses.to_vec(), - queries.to_vec(), - not_empty.to_vec(), - ), - false, - ) - }) - .collect_vec(); - - let input = MerkleAggregationCircuit::new( - HashStrategy::Tree, - snarks, - ACCOUNT_INSTANCE_SIZE, - ACCOUNT_POSEIDON_ROOT_INDICES.to_vec(), - ACCOUNT_KECCAK_ROOT_INDICES.to_vec(), - ); - let agg_k = 20; - let config_params = EthConfigParams { - degree: agg_k, - lookup_bits: Some(agg_k as usize - 1), - unusable_rows: 50, - ..Default::default() - }; - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(&config_params).unwrap()); - let agg_params = gen_srs(agg_k); - let circuit = - input.create_circuit(CircuitBuilderStage::Mock, None, agg_k as usize - 1, &agg_params); - - let mut instance = circuit.instance(); - MockProver::run(agg_k, &circuit, vec![instance.clone()]).unwrap().assert_satisfied(); - // now check against expected - let queries = get_account_queries(&provider, queries, ACCOUNT_PROOF_MAX_DEPTH); - let native_instance = native_account_instance(&block_responses, &queries, ¬_empty); - assert_eq!(instance.split_off(4 * LIMBS), native_instance); -} - -#[test] -fn test_mock_account_aggregation() { - let mut queries = vec![ - (17143006, "0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B"), - (17143000, "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"), - (15000000, "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"), - (15411056, "0x1c479675ad559DC151F6Ec7ed3FbF8ceE79582B6"), - ]; - let address = "0x756F45E3FA69347A9A973A725E3C98bC4db0b5a0"; - let mut rng = thread_rng(); - let latest = get_latest_block_number(); - let queries2: Vec<_> = (0..4).map(|_| (rng.gen_range(0..latest), address)).collect(); - queries.extend(queries2); - - let block_resp = queries.iter().map(|_| (Fr::random(OsRng), H256::random())).collect_vec(); - let not_empty = vec![true; queries.len()]; - - test_mock_account_aggregation_depth(block_resp.clone(), queries.clone(), not_empty.clone(), 1); - test_mock_account_aggregation_depth(block_resp, queries, not_empty, 2); -}*/ - -fn test_mock_row_consistency_aggregation_depth( - responses: Vec, - block_not_empty: Vec, - account_not_empty: Vec, - storage_not_empty: Vec, - depth: usize, -) { - assert!(responses.len().is_power_of_two()); - let chunk_size = responses.len() >> depth; - assert!(chunk_size != 0, "depth too large"); - let k = 20 - depth as u32; - let params = gen_srs(k); - let mut pk = None; - let snarks = - responses - .chunks(chunk_size) - .zip(block_not_empty.chunks(chunk_size).zip( - account_not_empty.chunks(chunk_size).zip(storage_not_empty.chunks(chunk_size)), - )) - .into_iter() - .map(|(response, (block_not_empty, (account_not_empty, storage_not_empty)))| { - let input = RowConsistencyCircuit::new( - response.to_vec(), - block_not_empty.to_vec(), - account_not_empty.to_vec(), - storage_not_empty.to_vec(), - Network::Mainnet, - ); - let circuit = input.create_circuit(CircuitBuilderStage::Mock, None, k); - if pk.is_none() { - pk = Some(gen_pk(¶ms, &circuit, None)); - } - (gen_snark_shplonk(¶ms, pk.as_ref().unwrap(), circuit, None::<&str>), false) - }) - .collect_vec(); - - let input = - PoseidonAggregationCircuit::new(HashStrategy::Tree, snarks, ROW_CIRCUIT_NUM_INSTANCES); - let agg_k = 20; - let agg_params = gen_srs(agg_k); - let lookup_bits = agg_k as usize - 1; - set_var("LOOKUP_BITS", lookup_bits.to_string()); - let circuit = AggregationPreCircuit::create_circuit( - input, - CircuitBuilderStage::Mock, - None, - lookup_bits, - &agg_params, - ); - let mut instance = circuit.instance(); - MockProver::run(agg_k, &circuit, vec![instance.clone()]).unwrap().assert_satisfied(); - - // now check against non-aggregated - let input = RowConsistencyCircuit::new( - responses, - block_not_empty, - account_not_empty, - storage_not_empty, - Network::Mainnet, - ); - set_var("LOOKUP_BITS", "0"); - let circuit = input.create_circuit(CircuitBuilderStage::Mock, None, agg_k); - assert_eq!(instance.split_off(4 * LIMBS), circuit.instance()); -} - -#[test] -fn test_mock_row_consistency_aggregation_nouns() { - let responses = get_full_storage_inputs_nouns(128); - let mut rng = thread_rng(); - let block_not_empty = responses.iter().map(|_| rng.gen_bool(0.8)).collect_vec(); - let account_not_empty = block_not_empty.iter().map(|ne| rng.gen_bool(0.8) && *ne).collect_vec(); - let storage_not_empty = - account_not_empty.iter().map(|ne| rng.gen_bool(0.8) && *ne).collect_vec(); - test_mock_row_consistency_aggregation_depth( - responses.clone(), - block_not_empty.clone(), - account_not_empty.clone(), - storage_not_empty.clone(), - 1, - ); - test_mock_row_consistency_aggregation_depth( - responses, - block_not_empty, - account_not_empty, - storage_not_empty, - 3, - ); -} diff --git a/axiom-eth/src/batch_query/tests/block_header.rs b/axiom-eth/src/batch_query/tests/block_header.rs deleted file mode 100644 index 4d547e72..00000000 --- a/axiom-eth/src/batch_query/tests/block_header.rs +++ /dev/null @@ -1,106 +0,0 @@ -use super::{setup_provider, setup_provider_goerli}; -use crate::{ - batch_query::{ - hash::poseidon_tree_root, - response::{ - block_header::{ - MultiBlockCircuit, GENESIS_BLOCK, GENESIS_BLOCK_RLP, MMR_MAX_NUM_PEAKS, - }, - native::get_block_response, - }, - tests::get_latest_block_number, - }, - providers::{get_block_rlp, get_blocks}, - rlp::builder::RlcThreadBuilder, - util::{encode_h256_to_field, h256_tree_root, EthConfigParams}, - EthPreCircuit, Network, -}; -use ethers_core::{types::H256, utils::keccak256}; -use halo2_base::halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr}; -use itertools::Itertools; -use rand::Rng; -use rand_chacha::ChaChaRng; -use rand_core::SeedableRng; -use snark_verifier::util::hash::Poseidon; -use snark_verifier_sdk::{halo2::POSEIDON_SPEC, NativeLoader}; -use std::env::set_var; -use test_log::test; - -fn test_mock_block_queries( - block_numbers: Vec, - not_empty: Vec, - network: Network, - expected: bool, -) { - let params = EthConfigParams::from_path("configs/tests/block_query.json"); - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(¶ms).unwrap()); - let k = params.degree; - - let provider = match network { - Network::Mainnet => setup_provider(), - Network::Goerli => setup_provider_goerli(), - }; - let mmr_len = *block_numbers.iter().max().unwrap_or(&0) as usize + 1; - let blocks = get_blocks(&provider, block_numbers).unwrap(); - let header_rlps = blocks - .iter() - .map(|block| get_block_rlp(block.as_ref().expect("block not found"))) - .collect_vec(); - // this is just to get mmr_bit check to pass, we do not check real mmr in this test - let mmr = (0..MMR_MAX_NUM_PEAKS) - .map(|i| H256::from_low_u64_be((mmr_len as u64 >> i) & 1u64)) - .collect_vec(); - let mmr_proofs = vec![vec![]; header_rlps.len()]; - let input = - MultiBlockCircuit::new(header_rlps, not_empty.clone(), network, mmr, mmr_len, mmr_proofs); - // instance calculation natively for test validation - let mut poseidon = Poseidon::from_spec(&NativeLoader, POSEIDON_SPEC.clone()); - let (res_p, res_k): (Vec<_>, Vec<_>) = blocks - .into_iter() - .zip_eq(not_empty) - .map(|(block, not_empty)| { - let ((mut pos, mut kec), _) = - get_block_response(&mut poseidon, block.unwrap(), network); - if !not_empty { - pos = Fr::zero(); - kec = H256([0u8; 32]); - } - (pos, kec) - }) - .unzip(); - let root_p = poseidon_tree_root(&mut poseidon, res_p, &[]); - let root_k = h256_tree_root(&res_k); - let root_k = encode_h256_to_field(&root_k); - - let circuit = input.create_circuit(RlcThreadBuilder::mock(), None); - let instance = circuit.instance(); - for (a, b) in [root_k[0], root_k[1], root_p].into_iter().zip(instance.iter()) { - assert_eq!(a, *b); - } - - if expected { - MockProver::run(k, &circuit, vec![instance]).unwrap().assert_satisfied(); - } -} - -#[test] -fn test_mock_block_queries_random() { - let len = 32; - let mut rng = ChaChaRng::from_seed([0u8; 32]); - let latest = get_latest_block_number(); - let block_numbers = (0..len).map(|_| rng.gen_range(0..latest)).collect_vec(); - // test instance generation but not mock prover since no mmr proof - test_mock_block_queries(block_numbers.clone(), vec![true; len], Network::Mainnet, false); - // test circuit but not hash values - test_mock_block_queries(block_numbers, vec![false; len], Network::Mainnet, true); -} - -#[test] -fn test_genesis_block() { - let block = get_blocks(&setup_provider(), [0]).unwrap().pop().unwrap().unwrap(); - assert_eq!(GENESIS_BLOCK.clone(), block); - assert_eq!( - format!("{:?}", H256(keccak256(GENESIS_BLOCK_RLP))), - "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" - ); -} diff --git a/axiom-eth/src/batch_query/tests/mod.rs b/axiom-eth/src/batch_query/tests/mod.rs deleted file mode 100644 index 1c0b8e26..00000000 --- a/axiom-eth/src/batch_query/tests/mod.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::providers::{GOERLI_PROVIDER_URL, MAINNET_PROVIDER_URL}; -use ethers_providers::{Http, Middleware, Provider, RetryClient}; -use std::env::var; -use tokio::runtime::Runtime; - -mod account; -#[cfg(feature = "aggregation")] -mod aggregation; -mod block_header; -mod row_consistency; -#[cfg(feature = "aggregation")] -mod scheduler; -mod storage; - -fn setup_provider() -> Provider> { - let infura_id = var("INFURA_ID").expect("INFURA_ID environmental variable not set"); - let provider_url = format!("{MAINNET_PROVIDER_URL}{infura_id}"); - Provider::new_client(&provider_url, 10, 500).expect("could not instantiate HTTP Provider") -} - -fn setup_provider_goerli() -> Provider> { - let infura_id = var("INFURA_ID").expect("INFURA_ID environmental variable not set"); - let provider_url = format!("{GOERLI_PROVIDER_URL}{infura_id}"); - Provider::new_client(&provider_url, 10, 500).expect("could not instantiate HTTP Provider") -} - -fn get_latest_block_number() -> u64 { - let provider = setup_provider(); - let rt = Runtime::new().unwrap(); - rt.block_on(provider.get_block_number()).unwrap().as_u64() -} diff --git a/axiom-eth/src/batch_query/tests/row_consistency.rs b/axiom-eth/src/batch_query/tests/row_consistency.rs deleted file mode 100644 index 56bf67da..00000000 --- a/axiom-eth/src/batch_query/tests/row_consistency.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::{ - batch_query::{ - hash::{poseidon_tree_root, PoseidonWords}, - response::{ - native::{ - get_account_response, get_block_response, get_full_account_response, - get_full_storage_response, get_storage_response, - }, - row_consistency::{ - RowConsistencyCircuit, ROW_ACCT_POSEIDON_INDEX, ROW_BLOCK_POSEIDON_INDEX, - ROW_STORAGE_POSEIDON_INDEX, - }, - }, - tests::storage::get_full_storage_inputs_nouns, - }, - Network, -}; -use halo2_base::{gates::builder::CircuitBuilderStage, halo2_proofs::dev::MockProver}; -use snark_verifier::util::hash::Poseidon; -use snark_verifier_sdk::{halo2::POSEIDON_SPEC, NativeLoader}; -use test_log::test; - -fn test_mock_row_consistency_nouns_gen(k: u32, num_rows: usize) { - let network = Network::Mainnet; - assert!(num_rows.is_power_of_two()); - let responses = get_full_storage_inputs_nouns(num_rows); - - // compute expected roots - let mut poseidon = Poseidon::from_spec(&NativeLoader, POSEIDON_SPEC.clone()); - let mut block_responses: Vec> = vec![]; - let mut full_acct_responses: Vec> = vec![]; - let mut full_st_responses: Vec> = vec![]; - for responses in &responses { - let (block_res, _) = get_block_response(&mut poseidon, responses.block.clone(), network); - let (acct_res, _) = get_account_response(&mut poseidon, &responses.storage); - let block_res = (block_res.0, responses.block.number.unwrap().as_u32()); - let full_acct_res = get_full_account_response(&mut poseidon, block_res, acct_res.clone()); - let acct_res = (acct_res.0, responses.storage.addr); - let (storage_res, _) = get_storage_response(&mut poseidon, &responses.storage); - let full_st_res = - get_full_storage_response(&mut poseidon, block_res, acct_res, storage_res); - block_responses.push(block_res.0.into()); - full_acct_responses.push(full_acct_res.0.into()); - full_st_responses.push(full_st_res.0.into()); - } - let block_root = poseidon_tree_root(&mut poseidon, block_responses, &[]); - let full_acct_root = poseidon_tree_root(&mut poseidon, full_acct_responses, &[]); - let full_st_root = poseidon_tree_root(&mut poseidon, full_st_responses, &[]); - // - - let input = RowConsistencyCircuit::new( - responses, - vec![true; num_rows], - vec![true; num_rows], - vec![true; num_rows], - network, - ); - let circuit = input.create_circuit(CircuitBuilderStage::Mock, None, k); - - let instance = circuit.instance(); - assert_eq!(instance[ROW_BLOCK_POSEIDON_INDEX], block_root); - assert_eq!(instance[ROW_ACCT_POSEIDON_INDEX], full_acct_root); - assert_eq!(instance[ROW_STORAGE_POSEIDON_INDEX], full_st_root); - MockProver::run(k, &circuit, vec![instance]).unwrap().assert_satisfied(); -} - -#[test] -fn test_mock_row_consistency_nouns() { - test_mock_row_consistency_nouns_gen(19, 128); -} diff --git a/axiom-eth/src/batch_query/tests/scheduler.rs b/axiom-eth/src/batch_query/tests/scheduler.rs deleted file mode 100644 index c8a6931b..00000000 --- a/axiom-eth/src/batch_query/tests/scheduler.rs +++ /dev/null @@ -1,551 +0,0 @@ -use super::setup_provider; -use crate::{ - batch_query::{ - response::{ - account::MultiAccountCircuit, - block_header::{MultiBlockCircuit, BLOCK_BATCH_DEPTH}, - native::FullStorageQuery, - row_consistency::RowConsistencyCircuit, - storage::MultiStorageCircuit, - }, - scheduler::{ - circuit_types::{ - BlockVerifyVsMmrCircuitType, ExponentialSchema, FinalAssemblyCircuitType, - }, - router::BatchQueryScheduler, - tasks::{BlockVerifyVsMmrTask, FinalAssemblyTask, ResponseInput, ResponseTask, Task}, - }, - tests::storage::get_full_storage_queries_nouns_single_block, - }, - providers::{get_blocks, get_full_storage_queries}, - storage::{ - EthBlockStorageInput, {ACCOUNT_PROOF_MAX_DEPTH, STORAGE_PROOF_MAX_DEPTH}, - }, - util::{ - get_merkle_mountain_range, - scheduler::{Scheduler, Task as _}, - }, - Network, -}; -use ethers_core::{ - types::{Address, H256}, - utils::keccak256, -}; -use halo2_base::{halo2_proofs::halo2curves::bn256::Fr, utils::bit_length}; -use itertools::Itertools; -use std::{fs::File, path::PathBuf, str::FromStr}; -use test_log::test; - -fn test_scheduler(network: Network) -> BatchQueryScheduler { - BatchQueryScheduler::new( - network, - false, - false, - PathBuf::from("configs/tests/batch_query"), - PathBuf::from("data/tests/batch_query"), - ) -} - -#[test] -fn test_scheduler_account() { - let schema: ExponentialSchema = serde_json::from_reader( - File::open("configs/tests/batch_query/schema.account.json").unwrap(), - ) - .unwrap(); - let len = 1 << schema.total_arity; - let queries = [ - (17143006, "0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B"), - (17143000, "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"), - (15000000, "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"), - (15411056, "0x1c479675ad559DC151F6Ec7ed3FbF8ceE79582B6"), - ] - .map(|(num, address)| (num, address.parse().unwrap())) - .to_vec(); - let block_responses = queries.iter().map(|_| (Fr::zero(), 0)).collect(); - let not_empty = vec![true; queries.len()]; - - let scheduler = test_scheduler(Network::Mainnet); - let input = - MultiAccountCircuit::from_provider(&setup_provider(), block_responses, queries, not_empty); - let input = MultiAccountCircuit::resize_from( - input.block_responses, - input.queries, - input.not_empty, - len, - ); - let task = ResponseTask { - aggregate: schema.total_arity != schema.start_arity, - schema, - input: ResponseInput::Account(input), - }; - - scheduler.get_snark(Task::Response(task)); -} - -#[test] -fn test_scheduler_storage() { - let schema: ExponentialSchema = serde_json::from_reader( - File::open("configs/tests/batch_query/schema.storage.json").unwrap(), - ) - .unwrap(); - let len = 1 << schema.total_arity; - let queries = get_full_storage_queries_nouns_single_block(len, 14985438); - let responses: Vec<_> = get_full_storage_queries( - &setup_provider(), - queries, - ACCOUNT_PROOF_MAX_DEPTH, - STORAGE_PROOF_MAX_DEPTH, - ) - .unwrap() - .into_iter() - .map(|response| EthBlockStorageInput::from(response).storage) - .collect(); - let not_empty = vec![true; len]; - - let scheduler = test_scheduler(Network::Mainnet); - let input = MultiStorageCircuit::new( - vec![(Fr::zero(), 0); len], - vec![(Fr::zero(), Address::zero()); len], - responses, - not_empty, - ); - let task = ResponseTask { - aggregate: schema.total_arity != schema.start_arity, - schema, - input: ResponseInput::Storage(input), - }; - - scheduler.get_snark(Task::Response(task)); -} - -#[test] -fn test_scheduler_row_consistency() { - let schema: ExponentialSchema = - serde_json::from_reader(File::open("configs/tests/batch_query/schema.row.json").unwrap()) - .unwrap(); - let len = 1 << schema.total_arity; - let queries = get_full_storage_queries_nouns_single_block(len, 12985438); - let responses: Vec<_> = get_full_storage_queries( - &setup_provider(), - queries, - ACCOUNT_PROOF_MAX_DEPTH, - STORAGE_PROOF_MAX_DEPTH, - ) - .unwrap() - .into_iter() - .map(EthBlockStorageInput::from) - .collect(); - - let scheduler = test_scheduler(Network::Mainnet); - let input = RowConsistencyCircuit::new( - responses, - vec![true; len], - vec![true; len], - vec![true; len], - Network::Mainnet, - ); - let task = ResponseTask { - aggregate: schema.total_arity != schema.start_arity, - schema, - input: ResponseInput::Row(input), - }; - - scheduler.get_snark(Task::Response(task)); -} - -#[test] -fn test_scheduler_verify_vs_mmr() { - let block_number = 12_985_438; - let merkle_proof = [ - "0xebd5fc0be32c2298e2ee18dac2db5b1508a64741ba7186dd59229ec0620d9d64", - "0x9139f12e0f47241172143fc5d0b0245b5ffbcdf9130da9efb14c155f6036697e", - "0x97f5d40bc9a10e06b5eef7609a3e364f30ab10675d22fbc3304179a381b39b18", - "0xc8c07e6c877f0cd60903d376c1aa911f47c96d3967d989101ed8b873cf6e38de", - "0x96cf53edbe3be783378433c518939a7e0b4e657edb6558b1f6e14edc0a125a18", - "0xfa3448a664e9406ffdc3b53e24f06fcf6b576621f854e421385bd1603ea257ee", - "0x9dffc8cb737d72da73df5e333bb7716cfec51e4b761281c6c7ff4db55689911c", - "0xef3fb7b7274810ec5bc63e7c248ea7dfe26d95abcd8bcb8d97b1f5fb617b8dc8", - "0x6a4d92e38592f280afc95efe5dd178a37c155bfad49759db7a066d597bc804d3", - "0x7db79de6d79e2ff264f4d171243f5038b575b380d31b052dda979e28fae7fc08", - "0x3106ece6d5a3c317f17c9313e7d0a3cd73649662301f50fdcedc67254b3fe153", - "0x902c8cf11e8d5cf14137e632061a52574515a2254fbd3b70cfc85a45f9dbcb4a", - "0xc48c7fe69133ac6f0c2200e600a3c15fe1832577156bc8851a7538403eafadfa", - "0x4434e3730dbe222cb8b98703748da1f07f05564c64ea66fe4765484ea982f5d6", - "0x69d2bc461de5dba21f741bf757d60ec8a92c3f29e417cb99fa76459bc3e86278", - "0xe18396e487f6c0bcd73a2d4c4c8c3583be7edefe59f20b2ce67c7f363b8a856a", - "0xa10b0dd9e041c793d0dbdf615bee9e18c3f6e3b191469bbb8cc9912d5d228050", - "0xa51d50eb9feaaf85b7ddacb99f71886135f1c4f59de3e788a5e29a485d5fdce5", - "0xa46b70512bfe0b85498e28ae8187cfadff9e58680b84ddcde450cd880ea489b1", - "0x33552dfc75e340bca3c698e4fb486ae540d07cf2a845465575cff24d866a161a", - "0x0fec590ac8394abe8477b828bf31b470d95772b3f331ff5be34ba0a899975a17", - ] - .into_iter() - .map(|s| H256::from_str(s).unwrap()) - .collect_vec(); - - let schema: BlockVerifyVsMmrCircuitType = - serde_json::from_reader(File::open("configs/tests/batch_query/schema.block.json").unwrap()) - .unwrap(); - let arity: usize = schema.arities.iter().sum(); - let len = 1 << arity; - let mmr_list_len = 16_525_312; - let mmr = vec![H256::zero(); BLOCK_BATCH_DEPTH] - .into_iter() - .chain(MMR_16525312.iter().map(|s| H256::from_str(s).unwrap())) - .collect(); - let block_numbers = vec![block_number; len]; - - let scheduler = test_scheduler(Network::Mainnet); - let input = MultiBlockCircuit::from_provider( - &setup_provider(), - block_numbers, - vec![true; len], - Network::Mainnet, - mmr, - mmr_list_len, - vec![merkle_proof; len], - ); - let task = BlockVerifyVsMmrTask { input, arities: schema.arities }; - println!("{}", task.name()); - - scheduler.get_snark(Task::BlockVerifyVsMmr(task)); -} - -const MMR_16525312: &[&str] = &[ - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0xe81cc62bb288e100856ea7d40af72b844e9dcb9ff8ebed659a475e2635cd4e18", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0xb169c87af2d231bc71f910481d6d8315a6fc4edfab212ee003d206b9643339c0", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x43062e083f5f40510f30723d9cebb51c4ae67c35d86c5b791a043bae317350e3", - "0x6cddc980f4c3b403d99080c32b3f0a6205e39560b9021d5f233c04d96c23381e", - "0x6a42052cabd8d66a584b8823c6aadf64dd2755321210c66a2b7acd1da5bdeacf", - "0xebf08ca711cbab09109bb51277c545ee43073d8fa8b46c0cbbedd46ce85e73be", - "0x477c055e69de14e3bbfe2af0389e6c3ac28ffb5b0cc8fa26b543ac47857fd646", - "0xf47e6584af17667881494720b50dd063d0900b288438df7a37e2e91440aedd23", -]; - -#[test] -fn test_scheduler_final_assembly_old() { - let block_number = 14_985_438; - let merkle_proof = [ - "0x4ee507551ceeb4e7cd160e1d6a546f78d7dc5ea29be99e79476a52781e5422a2", - "0xdfba2ede28a828481f99a0277f6062b3f409370c75dbbdb1f7513fdf43e114c5", - "0xe315409409b8ad6c0502debcf846d8f4f4d648c7c7598e12306caebb6879cf4d", - "0x319f29f57fd20fcbd67cf16d1547f2c7206fc402d11640759834574413c7c073", - "0xd7283aa12b799f869ddb6805d6ee0a6ac70bae8af5eda1fa83d910605902d31a", - "0xb6ababeeff584afc2ffd2a6239a8421cd36617a07fdfbaaf78e0d98ee5cdd2b2", - "0x7738be39d3d440a968245f93f2659da6c3955306b70de635caf704a2cd248012", - "0x9c5f767b3e6bf3e6e3716642d4beaeeb0705e4bce45411b4b130739050b85e3b", - "0xde3605c75c7c1e971b9615d608112661d407af3ef24945e226b7f0b3694ba102", - "0x24b2de47bd4094e61e07ee06d40f5a4f4d66999eea97037866eb06c535c70c5d", - "0x7c8e45373e748b8ec69371841bccbc438e4308d345729e5684c1264ac243dd9d", - "0x82779b69d134f2dac8998450a341591d53eb19d7b51d57645d6a67fafa0e4cfd", - "0x6b47cb78db4428a19df5344391d54e1f695576595eadedc82aef81156e3f85a6", - "0xd9e8dbccb0368b6a0b9d2886a0b1c30776684018474174d7666c08068fba49a9", - "0xda256132c245db47f729f5d9b8742a5074ec38dd376a5b01496e179863b8e6ef", - "0xc2cea502d15df8518ddb1834aa172cd4460c5b52d37ac0f38c3a232c1d8d19fb", - "0x282d4ca5df280766756c3f34dbd986145b41b3ebb1bca021c6cceb9ce7214aba", - "0xee9f461f804095981853ed2af093769936c30b686f242f67cb8b65d6e59746dd", - "0x03bb87fd41000fc97ba8639b2439aac2a80389bcb973086d8334930e100f5765", - "0x3267b39f880cdc657d6639cc25b454e4bf72099572682e3e95dc080c4bd1aa59", - ] - .into_iter() - .map(|s| H256::from_str(s).unwrap()) - .collect_vec(); - let mmr_num_blocks = 16_525_312; - let mmr = vec![H256::zero(); BLOCK_BATCH_DEPTH] - .into_iter() - .chain(MMR_16525312.iter().map(|s| H256::from_str(s).unwrap())) - .collect(); - - let circuit_type: FinalAssemblyCircuitType = - serde_json::from_reader(File::open("configs/tests/batch_query/schema.final.json").unwrap()) - .unwrap(); - assert_eq!(circuit_type.network, Network::Mainnet); - let total_arity = circuit_type.account_schema.total_arity; - let len = 1 << total_arity; - - let queries = get_full_storage_queries_nouns_single_block(len, block_number); - let input = get_full_storage_queries( - &setup_provider(), - queries, - ACCOUNT_PROOF_MAX_DEPTH, - STORAGE_PROOF_MAX_DEPTH, - ) - .unwrap(); - let task = Task::Final(FinalAssemblyTask { - circuit_type, - input, - mmr, - mmr_num_blocks, - mmr_proofs: vec![merkle_proof; len], - }); - println!("{}", task.name()); - - let scheduler = test_scheduler(Network::Mainnet); - scheduler.get_calldata(task, true); -} - -#[test] -fn test_scheduler_final_assembly_recent() { - let provider = setup_provider(); - - let mmr_num_blocks = 16_525_312; - let mut side = 777usize; - let list_len = 888usize; - let block_number = mmr_num_blocks + side; - let leaves = get_blocks(&provider, (0..list_len).map(|i| (mmr_num_blocks + i) as u64)) - .unwrap() - .into_iter() - .map(|block| block.unwrap().hash.unwrap()) - .collect_vec(); - let mut mmr = get_merkle_mountain_range(&leaves, BLOCK_BATCH_DEPTH - 1); - mmr.reverse(); - assert_eq!(mmr.len(), BLOCK_BATCH_DEPTH); - mmr.extend(MMR_16525312.iter().map(|s| H256::from_str(s).unwrap())); - - let mut peak_id = bit_length(list_len as u64) - 1; - let mut start = 0; - while (list_len >> peak_id) & 1 == (side >> peak_id) & 1 { - if (list_len >> peak_id) & 1 == 1 { - start += 1 << peak_id; - side -= 1 << peak_id; - } - peak_id -= 1; - } - let mut current_hashes = leaves[start..start + (1 << peak_id)].to_vec(); - let mut merkle_proof = vec![]; - for i in (1..=peak_id).rev() { - merkle_proof.push(current_hashes[side ^ 1]); - for i in 0..(1 << (i - 1)) { - current_hashes[i] = H256(keccak256( - [current_hashes[i * 2].as_bytes(), current_hashes[i * 2 + 1].as_bytes()].concat(), - )); - } - side >>= 1; - } - - let circuit_type: FinalAssemblyCircuitType = - serde_json::from_reader(File::open("configs/tests/batch_query/schema.final.json").unwrap()) - .unwrap(); - assert_eq!(circuit_type.network, Network::Mainnet); - let total_arity = circuit_type.account_schema.total_arity; - let len = 1 << total_arity; - - let queries = get_full_storage_queries_nouns_single_block(len, block_number as u64); - let input = get_full_storage_queries( - &setup_provider(), - queries, - ACCOUNT_PROOF_MAX_DEPTH, - STORAGE_PROOF_MAX_DEPTH, - ) - .unwrap(); - let task = Task::Final(FinalAssemblyTask { - circuit_type, - input, - mmr, - mmr_num_blocks: mmr_num_blocks + list_len, - mmr_proofs: vec![merkle_proof; len], - }); - println!("{}", task.name()); - - let scheduler = test_scheduler(Network::Mainnet); - let str = scheduler.get_calldata(task, true); - println!("{str:?}"); -} - -#[test] -fn test_scheduler_final_assembly_empty_slots() { - let block_number = 14_985_438; - let merkle_proof = [ - "0x4ee507551ceeb4e7cd160e1d6a546f78d7dc5ea29be99e79476a52781e5422a2", - "0xdfba2ede28a828481f99a0277f6062b3f409370c75dbbdb1f7513fdf43e114c5", - "0xe315409409b8ad6c0502debcf846d8f4f4d648c7c7598e12306caebb6879cf4d", - "0x319f29f57fd20fcbd67cf16d1547f2c7206fc402d11640759834574413c7c073", - "0xd7283aa12b799f869ddb6805d6ee0a6ac70bae8af5eda1fa83d910605902d31a", - "0xb6ababeeff584afc2ffd2a6239a8421cd36617a07fdfbaaf78e0d98ee5cdd2b2", - "0x7738be39d3d440a968245f93f2659da6c3955306b70de635caf704a2cd248012", - "0x9c5f767b3e6bf3e6e3716642d4beaeeb0705e4bce45411b4b130739050b85e3b", - "0xde3605c75c7c1e971b9615d608112661d407af3ef24945e226b7f0b3694ba102", - "0x24b2de47bd4094e61e07ee06d40f5a4f4d66999eea97037866eb06c535c70c5d", - "0x7c8e45373e748b8ec69371841bccbc438e4308d345729e5684c1264ac243dd9d", - "0x82779b69d134f2dac8998450a341591d53eb19d7b51d57645d6a67fafa0e4cfd", - "0x6b47cb78db4428a19df5344391d54e1f695576595eadedc82aef81156e3f85a6", - "0xd9e8dbccb0368b6a0b9d2886a0b1c30776684018474174d7666c08068fba49a9", - "0xda256132c245db47f729f5d9b8742a5074ec38dd376a5b01496e179863b8e6ef", - "0xc2cea502d15df8518ddb1834aa172cd4460c5b52d37ac0f38c3a232c1d8d19fb", - "0x282d4ca5df280766756c3f34dbd986145b41b3ebb1bca021c6cceb9ce7214aba", - "0xee9f461f804095981853ed2af093769936c30b686f242f67cb8b65d6e59746dd", - "0x03bb87fd41000fc97ba8639b2439aac2a80389bcb973086d8334930e100f5765", - "0x3267b39f880cdc657d6639cc25b454e4bf72099572682e3e95dc080c4bd1aa59", - ] - .into_iter() - .map(|s| H256::from_str(s).unwrap()) - .collect_vec(); - let mmr_num_blocks = 16_525_312; - let mmr = vec![H256::zero(); BLOCK_BATCH_DEPTH] - .into_iter() - .chain(MMR_16525312.iter().map(|s| H256::from_str(s).unwrap())) - .collect(); - - let circuit_type: FinalAssemblyCircuitType = - serde_json::from_reader(File::open("configs/tests/batch_query/schema.final.json").unwrap()) - .unwrap(); - assert_eq!(circuit_type.network, Network::Mainnet); - let total_arity = circuit_type.account_schema.total_arity; - let len = 1 << total_arity; - - let address = Address::from_str("0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03").unwrap(); // NounsToken - let queries = (0..len) - .map(|_| FullStorageQuery { block_number, addr_slots: Some((address, vec![])) }) - .collect_vec(); - let input = get_full_storage_queries( - &setup_provider(), - queries, - ACCOUNT_PROOF_MAX_DEPTH, - STORAGE_PROOF_MAX_DEPTH, - ) - .unwrap(); - let task = Task::Final(FinalAssemblyTask { - circuit_type, - input, - mmr, - mmr_num_blocks, - mmr_proofs: vec![merkle_proof; len], - }); - println!("{}", task.name()); - - let scheduler = test_scheduler(Network::Mainnet); - scheduler.get_calldata(task, true); -} - -#[test] -fn test_scheduler_final_assembly_empty_accounts() { - let block_number = 14_985_438; - let merkle_proof = [ - "0x4ee507551ceeb4e7cd160e1d6a546f78d7dc5ea29be99e79476a52781e5422a2", - "0xdfba2ede28a828481f99a0277f6062b3f409370c75dbbdb1f7513fdf43e114c5", - "0xe315409409b8ad6c0502debcf846d8f4f4d648c7c7598e12306caebb6879cf4d", - "0x319f29f57fd20fcbd67cf16d1547f2c7206fc402d11640759834574413c7c073", - "0xd7283aa12b799f869ddb6805d6ee0a6ac70bae8af5eda1fa83d910605902d31a", - "0xb6ababeeff584afc2ffd2a6239a8421cd36617a07fdfbaaf78e0d98ee5cdd2b2", - "0x7738be39d3d440a968245f93f2659da6c3955306b70de635caf704a2cd248012", - "0x9c5f767b3e6bf3e6e3716642d4beaeeb0705e4bce45411b4b130739050b85e3b", - "0xde3605c75c7c1e971b9615d608112661d407af3ef24945e226b7f0b3694ba102", - "0x24b2de47bd4094e61e07ee06d40f5a4f4d66999eea97037866eb06c535c70c5d", - "0x7c8e45373e748b8ec69371841bccbc438e4308d345729e5684c1264ac243dd9d", - "0x82779b69d134f2dac8998450a341591d53eb19d7b51d57645d6a67fafa0e4cfd", - "0x6b47cb78db4428a19df5344391d54e1f695576595eadedc82aef81156e3f85a6", - "0xd9e8dbccb0368b6a0b9d2886a0b1c30776684018474174d7666c08068fba49a9", - "0xda256132c245db47f729f5d9b8742a5074ec38dd376a5b01496e179863b8e6ef", - "0xc2cea502d15df8518ddb1834aa172cd4460c5b52d37ac0f38c3a232c1d8d19fb", - "0x282d4ca5df280766756c3f34dbd986145b41b3ebb1bca021c6cceb9ce7214aba", - "0xee9f461f804095981853ed2af093769936c30b686f242f67cb8b65d6e59746dd", - "0x03bb87fd41000fc97ba8639b2439aac2a80389bcb973086d8334930e100f5765", - "0x3267b39f880cdc657d6639cc25b454e4bf72099572682e3e95dc080c4bd1aa59", - ] - .into_iter() - .map(|s| H256::from_str(s).unwrap()) - .collect_vec(); - let mmr_num_blocks = 16_525_312; - let mmr = vec![H256::zero(); BLOCK_BATCH_DEPTH] - .into_iter() - .chain(MMR_16525312.iter().map(|s| H256::from_str(s).unwrap())) - .collect(); - - let circuit_type: FinalAssemblyCircuitType = - serde_json::from_reader(File::open("configs/tests/batch_query/schema.final.json").unwrap()) - .unwrap(); - assert_eq!(circuit_type.network, Network::Mainnet); - let total_arity = circuit_type.account_schema.total_arity; - let len = 1 << total_arity; - - let queries = - (0..len).map(|_| FullStorageQuery { block_number, addr_slots: None }).collect_vec(); - let input = get_full_storage_queries( - &setup_provider(), - queries, - ACCOUNT_PROOF_MAX_DEPTH, - STORAGE_PROOF_MAX_DEPTH, - ) - .unwrap(); - let task = Task::Final(FinalAssemblyTask { - circuit_type, - input, - mmr, - mmr_num_blocks, - mmr_proofs: vec![merkle_proof; len], - }); - println!("{}", task.name()); - - let scheduler = test_scheduler(Network::Mainnet); - scheduler.get_calldata(task, true); -} - -#[test] -fn test_scheduler_final_assembly_resize() { - let block_number = 14_985_438; - let merkle_proof = [ - "0x4ee507551ceeb4e7cd160e1d6a546f78d7dc5ea29be99e79476a52781e5422a2", - "0xdfba2ede28a828481f99a0277f6062b3f409370c75dbbdb1f7513fdf43e114c5", - "0xe315409409b8ad6c0502debcf846d8f4f4d648c7c7598e12306caebb6879cf4d", - "0x319f29f57fd20fcbd67cf16d1547f2c7206fc402d11640759834574413c7c073", - "0xd7283aa12b799f869ddb6805d6ee0a6ac70bae8af5eda1fa83d910605902d31a", - "0xb6ababeeff584afc2ffd2a6239a8421cd36617a07fdfbaaf78e0d98ee5cdd2b2", - "0x7738be39d3d440a968245f93f2659da6c3955306b70de635caf704a2cd248012", - "0x9c5f767b3e6bf3e6e3716642d4beaeeb0705e4bce45411b4b130739050b85e3b", - "0xde3605c75c7c1e971b9615d608112661d407af3ef24945e226b7f0b3694ba102", - "0x24b2de47bd4094e61e07ee06d40f5a4f4d66999eea97037866eb06c535c70c5d", - "0x7c8e45373e748b8ec69371841bccbc438e4308d345729e5684c1264ac243dd9d", - "0x82779b69d134f2dac8998450a341591d53eb19d7b51d57645d6a67fafa0e4cfd", - "0x6b47cb78db4428a19df5344391d54e1f695576595eadedc82aef81156e3f85a6", - "0xd9e8dbccb0368b6a0b9d2886a0b1c30776684018474174d7666c08068fba49a9", - "0xda256132c245db47f729f5d9b8742a5074ec38dd376a5b01496e179863b8e6ef", - "0xc2cea502d15df8518ddb1834aa172cd4460c5b52d37ac0f38c3a232c1d8d19fb", - "0x282d4ca5df280766756c3f34dbd986145b41b3ebb1bca021c6cceb9ce7214aba", - "0xee9f461f804095981853ed2af093769936c30b686f242f67cb8b65d6e59746dd", - "0x03bb87fd41000fc97ba8639b2439aac2a80389bcb973086d8334930e100f5765", - "0x3267b39f880cdc657d6639cc25b454e4bf72099572682e3e95dc080c4bd1aa59", - ] - .into_iter() - .map(|s| H256::from_str(s).unwrap()) - .collect_vec(); - let mmr_num_blocks = 16_525_312; - let mmr = vec![H256::zero(); BLOCK_BATCH_DEPTH] - .into_iter() - .chain(MMR_16525312.iter().map(|s| H256::from_str(s).unwrap())) - .collect(); - - let circuit_type: FinalAssemblyCircuitType = - serde_json::from_reader(File::open("configs/tests/batch_query/schema.final.json").unwrap()) - .unwrap(); - assert_eq!(circuit_type.network, Network::Mainnet); - let len = 5; - - let address = Address::from_str("0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03").unwrap(); // NounsToken - let queries = (0..len) - .map(|_| FullStorageQuery { block_number, addr_slots: Some((address, vec![])) }) - .collect_vec(); - let input = get_full_storage_queries( - &setup_provider(), - queries, - ACCOUNT_PROOF_MAX_DEPTH, - STORAGE_PROOF_MAX_DEPTH, - ) - .unwrap(); - let task = Task::Final(FinalAssemblyTask { - circuit_type, - input, - mmr, - mmr_num_blocks, - mmr_proofs: vec![merkle_proof; len], - }); - println!("{}", task.name()); - - let scheduler = test_scheduler(Network::Mainnet); - scheduler.get_calldata(task, true); -} diff --git a/axiom-eth/src/batch_query/tests/storage.rs b/axiom-eth/src/batch_query/tests/storage.rs deleted file mode 100644 index bc78de66..00000000 --- a/axiom-eth/src/batch_query/tests/storage.rs +++ /dev/null @@ -1,270 +0,0 @@ -use crate::{ - batch_query::{ - hash::{poseidon_tree_root, PoseidonWords}, - response::{ - native::{get_full_storage_response, get_storage_response, FullStorageQuery}, - storage::{MultiStorageCircuit, DEFAULT_STORAGE_QUERY}, - }, - tests::get_latest_block_number, - }, - providers::get_full_storage_queries, - rlp::builder::RlcThreadBuilder, - storage::{ - EthBlockStorageInput, {ACCOUNT_PROOF_MAX_DEPTH, STORAGE_PROOF_MAX_DEPTH}, - }, - util::{encode_addr_to_field, encode_h256_to_field, h256_tree_root, EthConfigParams}, - EthPreCircuit, -}; -use ethers_core::{ - types::{Address, H256}, - utils::keccak256, -}; -use ff::Field; -use halo2_base::halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr}; -use itertools::Itertools; -use rand::{thread_rng, Rng}; -use rand_chacha::ChaChaRng; -use rand_core::{OsRng, SeedableRng}; -use snark_verifier::util::hash::Poseidon; -use snark_verifier_sdk::{halo2::POSEIDON_SPEC, NativeLoader}; -use std::{env::set_var, str::FromStr}; -use test_log::test; - -use super::setup_provider; - -fn test_mock_storage_queries( - block_responses: Vec<(Fr, u32)>, - acct_responses: Vec<(Fr, Address)>, - queries: Vec<(u64, &str, H256)>, - not_empty: Vec, -) { - let params = EthConfigParams::from_path("configs/tests/storage_query.json"); - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(¶ms).unwrap()); - let k = params.degree; - - let queries = queries - .into_iter() - .map(|(block_number, address, slot)| { - let address = address.parse().unwrap(); - (block_number, address, slot) - }) - .collect(); - - let input = MultiStorageCircuit::from_provider( - &setup_provider(), - block_responses, - acct_responses, - queries, - not_empty.clone(), - ); - // instance calculation natively for test validation - let mut poseidon = Poseidon::from_spec(&NativeLoader, POSEIDON_SPEC.clone()); - let block_num_root = poseidon_tree_root( - &mut poseidon, - input - .block_responses - .iter() - .map(|(_, num)| PoseidonWords(vec![Fr::from(*num as u64)])) - .collect(), - &[], - ); - let addr_root = poseidon_tree_root( - &mut poseidon, - input - .account_responses - .iter() - .map(|(_, addr)| PoseidonWords(vec![encode_addr_to_field::(addr)])) - .collect_vec(), - &[], - ); - let (res_p, res_k): (Vec<_>, Vec<_>) = input - .block_responses - .iter() - .zip_eq(input.account_responses.iter()) - .zip_eq(input.queries.iter()) - .zip_eq(not_empty.iter()) - .map(|(((block_response, acct_response), query), not_empty)| { - let (storage_response, _) = get_storage_response(&mut poseidon, query); - let (mut pos, mut kec) = get_full_storage_response( - &mut poseidon, - *block_response, - *acct_response, - storage_response, - ); - if !not_empty { - pos = Fr::zero(); - kec = H256([0u8; 32]); - } - (pos, kec) - }) - .unzip(); - let root_p = poseidon_tree_root(&mut poseidon, res_p, &[]); - let root_k = encode_h256_to_field(&h256_tree_root(&res_k)); - let instance = vec![root_k[0], root_k[1], root_p, block_num_root, addr_root]; - - let circuit = input.create_circuit(RlcThreadBuilder::mock(), None); - - MockProver::run(k, &circuit, vec![instance]).unwrap().assert_satisfied(); -} - -#[test] -fn test_mock_storage_queries_slot0() { - let queries = vec![ - (17143006, "0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B", H256::zero()), - (17143000, "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", H256::zero()), - (16356350, "0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB", H256::zero()), - (15411056, "0x1c479675ad559DC151F6Ec7ed3FbF8ceE79582B6", H256::zero()), - ]; - let mut rng = thread_rng(); - // note that block response is not checked in any way in the circuit, in particular the poseidon and keccak parts don't even need to be consistent! - let block_responses = queries.iter().map(|_| (Fr::random(OsRng), rng.gen())).collect(); - let acct_responses = queries.iter().map(|_| (Fr::random(OsRng), Address::random())).collect(); - let not_empty = vec![true; queries.len()]; - test_mock_storage_queries(block_responses, acct_responses, queries, not_empty); -} - -#[test] -fn test_mock_storage_queries_uni_v3() { - let address = "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"; // uniswap v3 eth-usdc 5bps pool - let mut rng = thread_rng(); - let latest = get_latest_block_number(); - let queries = [0, 1, 2, 8] - .map(|x| (rng.gen_range(12376729..latest), address, H256::from_low_u64_be(x))) - .to_vec(); - let block_responses = queries.iter().map(|_| (Fr::random(OsRng), rng.gen())).collect(); - let acct_responses = queries.iter().map(|_| (Fr::random(OsRng), Address::random())).collect(); - let not_empty = vec![true; queries.len()]; - test_mock_storage_queries(block_responses, acct_responses, queries, not_empty); -} - -#[test] -fn test_mock_storage_queries_mapping() { - let address = "0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB"; // cryptopunks - let mut rng = thread_rng(); - let slots = (0..4).map(|x| { - let mut bytes = [0u8; 64]; - bytes[31] = x; - bytes[63] = 10; - H256::from_slice(&keccak256(bytes)) - }); - let latest = get_latest_block_number(); - let queries: Vec<_> = - slots.map(|slot| (rng.gen_range(3914495..latest), address, slot)).collect(); - let block_responses = queries.iter().map(|_| (Fr::random(OsRng), rng.gen())).collect(); - let acct_responses = queries.iter().map(|_| (Fr::random(OsRng), Address::random())).collect(); - let not_empty = vec![true; queries.len()]; - test_mock_storage_queries(block_responses, acct_responses, queries, not_empty); -} - -#[test] -fn test_mock_storage_queries_empty() { - let address = "0xbb9bc244d798123fde783fcc1c72d3bb8c189413"; // TheDAO Token - let mut rng = thread_rng(); - let latest = get_latest_block_number(); - let mut queries: Vec<_> = (0..8) - .map(|_| (rng.gen_range(1428757..latest), address, H256::from_low_u64_be(3))) - .collect(); - let block_responses = queries.iter().map(|_| (Fr::random(OsRng), rng.gen())).collect(); - let acct_responses = queries.iter().map(|_| (Fr::random(OsRng), Address::random())).collect(); - let mut not_empty = vec![true; queries.len()]; - for (ne, q) in not_empty.iter_mut().zip(queries.iter_mut()).take(4) { - *ne = false; - q.2 = H256::random(); - } - test_mock_storage_queries(block_responses, acct_responses, queries, not_empty); -} - -// some of the slots will be empty, we test that the value returned is 0 -#[test] -fn test_mock_storage_queries_random() { - let address = "0xbb9bc244d798123fde783fcc1c72d3bb8c189413"; // TheDAO Token - let mut rng = ChaChaRng::from_seed([0u8; 32]); - let latest = get_latest_block_number(); - let queries: Vec<_> = - (0..8).map(|_| (rng.gen_range(1428757..latest), address, H256::random())).collect(); - let block_responses = queries.iter().map(|_| (Fr::random(OsRng), rng.gen())).collect(); - let acct_responses = queries.iter().map(|_| (Fr::random(OsRng), Address::random())).collect(); - let not_empty = vec![true; queries.len()]; - test_mock_storage_queries(block_responses, acct_responses, queries, not_empty); -} - -pub fn get_full_storage_queries_nouns_single_block( - len: usize, - block_number: u64, -) -> Vec { - let address = "0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03"; // NounsToken - let address: Address = address.parse().unwrap(); - let mut queries = vec![]; - for i in 0..3 { - queries.push(FullStorageQuery { - block_number, - addr_slots: Some((address, vec![H256::from_low_u64_be(i)])), - }); - } - if len <= 3 { - queries.truncate(len); - } else { - for i in 0..len - 3 { - let mut bytes = [0u8; 64]; - bytes[31] = i as u8; - bytes[63] = 3; // slot 3 is _owners mapping(uint256 => address) - let slot = H256::from_slice(&keccak256(bytes)); - queries - .push(FullStorageQuery { block_number, addr_slots: Some((address, vec![slot])) }); - } - } - queries -} - -pub fn get_full_storage_queries_nouns(len: usize) -> Vec { - let creation_block = 12985438; - let latest = get_latest_block_number(); - let mut rng = rand::thread_rng(); - let mut queries = vec![]; - - let mut remaining_len = len; - - while remaining_len > 0 { - let block_number: u64 = rng.gen_range(creation_block..latest); - let current_len = rng.gen_range(1..=remaining_len); - let mut current_queries = - get_full_storage_queries_nouns_single_block(current_len, block_number); - queries.append(&mut current_queries); - remaining_len -= current_len; - } - queries -} - -pub fn get_full_storage_inputs_nouns(len: usize) -> Vec { - let queries = get_full_storage_queries_nouns(len); - let responses = get_full_storage_queries( - &setup_provider(), - queries, - ACCOUNT_PROOF_MAX_DEPTH, - STORAGE_PROOF_MAX_DEPTH, - ) - .unwrap(); - responses.into_iter().map(|response| response.try_into().unwrap()).collect() -} - -#[test] -fn test_default_storage_query() { - let address = Address::from_str("0x01d5b501C1fc0121e1411970fb79c322737025c2").unwrap(); // AxiomV0 - let provider = setup_provider(); - let query: EthBlockStorageInput = get_full_storage_queries( - &provider, - vec![FullStorageQuery { - block_number: 16504035, - addr_slots: Some((address, vec![H256::zero()])), - }], - ACCOUNT_PROOF_MAX_DEPTH, - STORAGE_PROOF_MAX_DEPTH, - ) - .unwrap() - .pop() - .unwrap() - .try_into() - .unwrap(); - - assert_eq!(format!("{:?}", query.storage), format!("{:?}", DEFAULT_STORAGE_QUERY.clone())) -} diff --git a/axiom-eth/src/bin/AxiomV1.md b/axiom-eth/src/bin/AxiomV1.md deleted file mode 100644 index 973b6265..00000000 --- a/axiom-eth/src/bin/AxiomV1.md +++ /dev/null @@ -1,160 +0,0 @@ -# AxiomV1 SNARK Verifier Circuits - -# Public Instance Formats - -Any `Snark` has an associated `Vec` of public instances. We describe the format for the ones relevant to the core AxiomV1 circuits below. - -## `EthBlockHeaderChainCircuit` - -```rust -pub struct EthBlockHeaderChainCircuit { - header_rlp_encodings: Vec>, - num_blocks: u32, // num_blocks in [0, 2 ** max_depth) - max_depth: usize, - network: Network, - _marker: PhantomData, -} -``` - -This depends on a `max_depth` parameter. The public instances are: - -- `prev_hash`: `H256` as two `Fr` elements in hi-lo format -- `end_hash`: `H256` as two `Fr` elements in hi-lo format -- `start_block_number . end_block_number`: we assume both numbers are `u32` and encode them to a single `Fr` element as `start_block_number * 2^32 + end_block_number` -- `merkle_mountain_range`: a sequence of `max_depth + 1` `H256` elements, each encoded as two `Fr` elements in hi-lo format - -Notes: - -- `prev_hash` is the parent hash of block number `start_block_number` -- `end_hash` is the block hash of block number `end_block_number` -- `end_block_number - start_block_number` is constrained to be `<= 2^max_depth` - - This was previously assumed in `axiom-eth` `v0.1.1` but not enforced because the block numbers are public instances, but we now enforce it for safety -- `merkle_mountain_range` is ordered from largest peak (depth `max_depth`) first to smallest peak (depth `0`) last - -## `EthBlockHeaderChainAggregationCircuit` - -```rust -pub struct EthBlockHeaderChainAggregationCircuit { - num_blocks: u32, - snarks: Vec, - pub max_depth: usize, - pub initial_depth: usize, -} -``` - -This circuit takes two [`EthBlockHeaderChainCircuit`s](#ethblockheaderchaincircuit) and aggregates them. The public instances are: - -- `4 * LIMBS = 12` `Fr` elements for the two BN254 `G1` points representing the _accumulator_, used by the verifier for a pairing check -- `prev_hash`: `H256` as two `Fr` elements in hi-lo format -- `end_hash`: `H256` as two `Fr` elements in hi-lo format -- `start_block_number . end_block_number`: we assume both numbers are `u32` and encode them to a single `Fr` element as `start_block_number * 2^32 + end_block_number` -- `merkle_mountain_range`: a sequence of `2^{max_depth - initial_depth} + initial_depth` `H256` elements, each encoded as two `Fr` elements in hi-lo format - -Notes: - -- Same notes as [`EthBlockHeaderChainCircuit`](#ethblockheaderchaincircuit) **except** that `merkle_mountain_range` is not actually a Merkle mountain range: we recover a Merkle mountain range of length `max_depth + 1` by forming a Merkle mountain range from leaves `merkle_mountain_range[..2^{max_depth - initial_depth}]` and then appending `merkle_mountain_range[2^{max_depth - initial_depth}..]` to the end of it. - - The reason is that we want to delay Keccaks - -## `EthBlockHeaderChainFinalAggregationCircuit` - -```rust -pub struct EthBlockHeaderChainFinalAggregationCircuit(pub EthBlockHeaderChainAggregationCircuit); -``` - -This circuit takes two [`EthBlockHeaderChainAggregationCircuit`s](#ethblockheaderchainaggregationcircuit) and aggregates them. The public instances are: - -- `4 * LIMBS = 12` `Fr` elements for the two BN254 `G1` points representing the _accumulator_, used by the verifier for a pairing check -- `prev_hash`: `H256` as two `Fr` elements in hi-lo format -- `end_hash`: `H256` as two `Fr` elements in hi-lo format -- `start_block_number . end_block_number`: we assume both numbers are `u32` and encode them to a single `Fr` element as `start_block_number * 2^32 + end_block_number` -- `merkle_mountain_range`: a sequence of `max_depth + 1` `H256` elements, each encoded as two `Fr` elements in hi-lo format - -Notes: - -- Same notes as [`EthBlockHeaderChainCircuit`](#ethblockheaderchaincircuit) -- This circuit is the same as [`EthBlockHeaderChainAggregationCircuit`](#ethblockheaderchainaggregationcircuit) except that it does do the final Keccaks to form the full Merkle mountain range - -## `PublicAggregationCircuit` - -```rust -pub struct PublicAggregationCircuit { - pub snarks: Vec, - pub has_prev_accumulators: bool, -} -``` - -This circuit aggregates snarks and re-exposes previous public inputs. The public instances are: - -- `4 * LIMBS = 12` `Fr` elements for the two BN254 `G1` points representing the _accumulator_, used by the verifier for a pairing check -- Sequentially appends the public instances from each `Snark` in `snarks` - - If `has_prev_accumulators` is true, then it assumes all previous snarks are already aggregation circuits and does not re-expose the old accumulators (the first `4 * LIMBS` elements) as public inputs. - -## `EthBlockStorageCircuit` - -```rust -pub struct EthBlockStorageCircuit { - pub inputs: EthBlockStorageInput, - pub network: Network, -} -``` - -The public instances are: - -- `block_hash`: `H256` as two `Fr` elements in hi-lo format -- `block_number`: `u32` as a single `Fr` element -- `address`: `H160` as a single `Fr` element -- Sequence of `inputs.slots.len()` pairs of `(slot, value)` where - - `slot`: `H256` as two `Fr` elements in hi-lo format - - `value`: `U256` as two `Fr` elements in hi-lo format (big endian) - -# `AxiomV1Core` SNARK Verifier - -This snark is created by calling - -```bash -cargo run --bin header_chain --release -- --start 0 --end 1023 --max-depth 10 --initial-depth 7 --final evm --extra-rounds 1 --calldata --create-contract -``` - -This recursively creates the following snarks in a tree: - -``` -PublicAggregationCircuit (10) -> PublicAggregationCircuit (10) -> EthBlockHeaderChainFinalAggregationCircuit (10) -> EthBlockHeaderChainAggregationCircuit (9) -> ... -> EthBlockHeaderChainAggregationCircuit (8) -> EthBlockHeaderChainCircuit (7) -``` - -where the number in parenthesis is a tracker of the `max_depth` for the circuit. We do two rounds of `PublicAggregationCircuit` to minimize final verification gas cost. - -The public instances are the same as for [`EthBlockHeaderChainFinalAggregationCircuit`](#ethblockheaderchainfinalaggregationcircuit). - -# `AxiomV1Core` Historical SNARK Verifier - -This snark is created by calling - -```bash -cargo run --bin header_chain --release -- --start 0 --end 1023 --max-depth 17 --initial-depth 7 --final evm --extra-rounds 1 --calldata --create-contract -``` - -This recursively creates the following snarks in a tree: - -``` -PublicAggregationCircuit (17) -> PublicAggregationCircuit (17) -> EthBlockHeaderChainFinalAggregationCircuit (17) -> EthBlockHeaderChainAggregationCircuit (16) -> ... -> EthBlockHeaderChainAggregationCircuit (8) -> EthBlockHeaderChainCircuit (7) -``` - -where the number in parenthesis is a tracker of the `max_depth` for the circuit. - -The public instances are the same as for [`EthBlockHeaderChainFinalAggregationCircuit`](#ethblockheaderchainfinalaggregationcircuit). - -# `AxiomV1StoragePf` SNARK Verifier - -This snark is created by calling - -```bash -cargo run --bin storage_proof --release -- --path data/storage/task.t.json --create-contract -``` - -with this [`task.t.json`](../../data/storage/task.t.json) file. In particular `inputs.slots.len() = 10`. - -This recursively creates the following snarks: - -``` -PublicAggregationCircuit -> EthBlockStorageCircuit -``` diff --git a/axiom-eth/src/bin/AxiomV1Query.md b/axiom-eth/src/bin/AxiomV1Query.md deleted file mode 100644 index 88368b5d..00000000 --- a/axiom-eth/src/bin/AxiomV1Query.md +++ /dev/null @@ -1,23 +0,0 @@ -# AxiomV1Query SNARK Verifier Circuits - -# `AxiomV1Query` SNARK Verifier - -This snark is created by calling - -```bash -cargo test --release -- --nocapture test_scheduler_final_assembly_old -``` - -This will create several proving key files and a Yul file at `data/tests/batch_query/final_2.yul`. -It will also create a `final_*.calldata` file in the `data/tests/batch_query/` directory. - -> **NOTE**: This command requires a machine with `400GB` of RAM to fully run. We recommend using a machine such as an AWS EC2 `r5.metal` or setting up [swap](https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-22-04). The memory requirement is largely because this command keeps all intermediate proving keys in memory. On a machine with at least `100GB` combined RAM and swap, you can try re-running the above command after it is killed due to memory limits until the final proof calldata is created: the intermediate SNARKs are stored locally on disk, so re-running the command will not need to regenerate them from scratch. - -Other tests that will generate different `final_*.calldata` proof calldata files, but create the same proving keys and Yul code, are: - -```bash -cargo test --release -- --nocapture test_scheduler_final_assembly_recent -cargo test --release -- --nocapture test_scheduler_final_assembly_empty_slots -cargo test --release -- --nocapture test_scheduler_final_assembly_empty_accounts -cargo test --release -- --nocapture test_scheduler_final_assembly_resize -``` diff --git a/axiom-eth/src/bin/README.md b/axiom-eth/src/bin/README.md deleted file mode 100644 index aacc8c99..00000000 --- a/axiom-eth/src/bin/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Binaries - -## Setup - -```bash -cd axiom-eth -# symlink existing params folder if you have one, otherwise a new one will automatically be created -mkdir data - -# update Cargo -eval $(ssh-agent) -ssh-add -cargo update -``` - -Copy [`.env.example`](../../.env.example) to `.env` and add your INFURA ID. Then run - -```bash -source .env -``` - -## Header chain - -To create a single snark without aggregation for `2^7` block headers on mainnet: - -``` -cargo run --bin header_chain --release -- --start 0x765fb3 --end 0x766031 --max-depth 7 -``` - -This will randomly generate trusted setup files (in `./params/*.srs`) and proving key, JSON-API call, snark (in `data/headers/`). - -## Header chain with aggregation for EVM - -To create a snark with TWO additional rounds of aggregation for `2^10` block headers on mainnet, to be submitted to EVM: - -``` -cargo run --bin header_chain --release -- --start 0 --end 1023 --max-depth 10 --initial-depth 7 --final evm --extra-rounds 1 --calldata --create-contract -``` - -The `--calldata` flag tells the binary to print out calldata instead of a binary snark. -The `--create-contract` flag generates the bytecode for the EVM verifier and submits the proof, printing the gas cost -if the transaction doesn't revert. - -For full commandline usage, type - -``` -cargo run --bin header_chain --release -- --help -``` - -## Storage Proof - -To prove a pre-specified number of storage slots for a given account at a given block number, fill in [`task.t.json`](../../data/storage/task.t.json) with the required information and run - -```bash -cargo run --bin storage_proof --release -- --path data/storage/task.t.json --create-contract -``` - -Currently we only provide [configuration files](../../configs/storage/) for `10` storage slots. diff --git a/axiom-eth/src/bin/header_chain.rs b/axiom-eth/src/bin/header_chain.rs deleted file mode 100644 index 6ef37f6a..00000000 --- a/axiom-eth/src/bin/header_chain.rs +++ /dev/null @@ -1,109 +0,0 @@ -#[cfg(feature = "display")] -use ark_std::{end_timer, start_timer}; -use axiom_eth::{ - block_header::helpers::{BlockHeaderScheduler, CircuitType, Finality, Task}, - util::scheduler::Scheduler, - Network, -}; -use clap::{Parser, ValueEnum}; -use clap_num::maybe_hex; -use std::{cmp::min, fmt::Display, path::PathBuf}; - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] // Read from `Cargo.toml` -/// Generates multiple SNARKS for chains of block header hashes. -/// Optionally does final processing to get merkle mountain range and/or produce EVM verifier contract code and calldata. -struct Cli { - #[arg(long, default_value_t = Network::Mainnet)] - network: Network, - #[arg(short, long = "start", value_parser=maybe_hex::)] - start_block_number: u32, - #[arg(short, long = "end", value_parser=maybe_hex::)] - end_block_number: u32, - #[arg(long = "max-depth")] - max_depth: usize, - #[arg(long = "initial-depth")] - initial_depth: Option, - #[arg(long = "final", default_value_t = CliFinality::None)] - finality: CliFinality, - #[arg(long = "extra-rounds")] - rounds: Option, - #[arg(long = "calldata")] - calldata: bool, - #[cfg_attr(feature = "evm", arg(long = "create-contract"))] - create_contract: bool, - #[arg(long = "readonly")] - readonly: bool, - #[arg(long = "srs-readonly")] - srs_readonly: bool, -} - -#[derive(Clone, Debug, ValueEnum)] -enum CliFinality { - /// Produces as many snarks as needed to fit the entire block number range, without any final processing. - None, - /// The block number range must fit within the specified max depth. - /// Produces a single final snark with the starting & ending block numbers, previous and last block hashes, - /// and merkle mountain range as output. - Merkle, - /// The block number range must fit within the specified max depth. Produces the final verifier circuit to verifier all - /// the previous snarks in EVM. Writes the calldata to disk. - Evm, -} - -impl Display for CliFinality { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - CliFinality::None => write!(f, "none"), - CliFinality::Merkle => write!(f, "merkle"), - CliFinality::Evm => write!(f, "evm"), - } - } -} - -fn main() { - let args = Cli::parse(); - let initial_depth = args.initial_depth.unwrap_or(args.max_depth); - #[cfg(feature = "production")] - let srs_readonly = true; - #[cfg(not(feature = "production"))] - let srs_readonly = args.srs_readonly; - - let scheduler = BlockHeaderScheduler::new( - args.network, - srs_readonly, - args.readonly, - PathBuf::from("configs/headers"), - PathBuf::from("data/headers"), - ); - - #[cfg(feature = "display")] - let start = start_timer!(|| format!( - "Generating SNARKs for blocks {} to {}, max depth {}, initial depth {}, finality {}", - args.start_block_number, - args.end_block_number, - args.max_depth, - initial_depth, - args.finality - )); - - let finality = match args.finality { - CliFinality::None => Finality::None, - CliFinality::Merkle => Finality::Merkle, - CliFinality::Evm => Finality::Evm(args.rounds.unwrap_or(0)), - }; - let circuit_type = CircuitType::new(args.max_depth, initial_depth, finality, args.network); - for start in (args.start_block_number..=args.end_block_number).step_by(1 << args.max_depth) { - let end = min(start + (1 << args.max_depth) - 1, args.end_block_number); - let task = Task::new(start, end, circuit_type); - if args.calldata { - #[cfg(feature = "evm")] - scheduler.get_calldata(task, args.create_contract); - } else { - scheduler.get_snark(task); - } - } - - #[cfg(feature = "display")] - end_timer!(start); -} diff --git a/axiom-eth/src/block_header/aggregation/final_merkle.rs b/axiom-eth/src/block_header/aggregation/final_merkle.rs deleted file mode 100644 index 84df3068..00000000 --- a/axiom-eth/src/block_header/aggregation/final_merkle.rs +++ /dev/null @@ -1,160 +0,0 @@ -#[cfg(not(feature = "production"))] -use crate::util::EthConfigParams; -use crate::{ - block_header::aggregation::join_previous_instances, - keccak::{FixedLenRLCs, FnSynthesize, KeccakChip, VarLenRLCs}, - rlp::{ - builder::{RlcThreadBreakPoints, RlcThreadBuilder}, - RlpChip, - }, - util::{bytes_be_to_u128, num_to_bytes_be, NUM_BYTES_IN_U128}, - EthCircuitBuilder, -}; -#[cfg(feature = "display")] -use ark_std::{end_timer, start_timer}; -use halo2_base::{ - gates::{builder::CircuitBuilderStage, GateInstructions, RangeChip, RangeInstructions}, - halo2_proofs::{ - halo2curves::bn256::{Bn256, Fr}, - poly::kzg::commitment::ParamsKZG, - }, - QuantumCell::Constant, -}; -use snark_verifier_sdk::{halo2::aggregation::AggregationCircuit, Snark, SHPLONK}; -use std::cell::RefCell; -#[cfg(not(feature = "production"))] -use std::env::var; - -use super::EthBlockHeaderChainAggregationCircuit; - -/// Same as `EthBlockHeaderChainAggregationCircuit` but uses Keccak chip to compute the final merkle mountain root. Specifically, it aggregates two snarks at `max_depth - 1` and then computes the keccaks to get the final merkle mountain root. -#[derive(Clone, Debug)] -pub struct EthBlockHeaderChainFinalAggregationCircuit(pub EthBlockHeaderChainAggregationCircuit); - -impl EthBlockHeaderChainFinalAggregationCircuit { - pub fn new( - snarks: Vec, - num_blocks: u32, - max_depth: usize, - initial_depth: usize, - ) -> Self { - let inner = EthBlockHeaderChainAggregationCircuit::new( - snarks, - num_blocks, - max_depth, - initial_depth, - ); - /* // Only for testing - let leaves = - &inner.chain_instance.merkle_mountain_range[..num_blocks as usize >> initial_depth]; - let mut new_mmr = get_merkle_mountain_range(leaves, max_depth - initial_depth); - new_mmr.extend_from_slice( - &inner.chain_instance.merkle_mountain_range[1 << (max_depth - initial_depth)..], - ); - inner.chain_instance.merkle_mountain_range = new_mmr; - */ - Self(inner) - } - - /// `snarks` should be exactly two snarks of either - /// - `EthBlockHeaderChainCircuit` if `max_depth == initial_depth + 1` or - /// - `EthBlockHeaderChainAggregationCircuit` otherwise - pub fn create_circuit( - self, - stage: CircuitBuilderStage, - break_points: Option, - lookup_bits: usize, - params: &ParamsKZG, - ) -> EthCircuitBuilder> { - let num_blocks = self.0.num_blocks; - let max_depth = self.0.max_depth; - let initial_depth = self.0.initial_depth; - #[cfg(feature = "display")] - let timer = start_timer!(|| { - format!("New EthBlockHeaderChainFinalAggregationCircuit | num_blocks: {num_blocks} | max_depth: {max_depth} | initial_depth: {initial_depth}") - }); - let aggregation = AggregationCircuit::new::( - stage, - Some(Vec::new()), // break points aren't actually used, since we will just take the builder from this circuit - lookup_bits, - params, - self.0.snarks, - ); - // All computations are contained in the `aggregations`'s builder, so we take that to create a new RlcThreadBuilder - let mut builder = RlcThreadBuilder { - threads_rlc: Vec::new(), - gate_builder: aggregation.inner.circuit.0.builder.take(), - }; - // TODO: should reuse RangeChip from aggregation circuit, but can't refactor right now - let range = RangeChip::::default(lookup_bits); - let ctx = builder.gate_builder.main(0); - let num_blocks_minus_one = - ctx.load_witness(range.gate().get_field_element(num_blocks as u64 - 1)); - let new_instances = join_previous_instances( - ctx, - &range, - aggregation.previous_instances.try_into().unwrap(), - num_blocks_minus_one, - max_depth, - initial_depth, - ); - let num_blocks = range.gate().add(ctx, num_blocks_minus_one, Constant(Fr::one())); - - // compute the keccaks that were delayed, to get the `max_depth - initial_depth + 1` biggest merkle mountain ranges - let mut keccak = KeccakChip::default(); - let bits = range.gate().num_to_bits(ctx, num_blocks, max_depth + 1); - // bits is in little endian, we take the top `max_depth - initial_depth + 1` bits - let num_leaves = 1 << (max_depth - initial_depth); - let num_leaves_bits = &bits[initial_depth..]; - let mmr_instances = &new_instances[5..]; - // convert from u128 to bytes - let leaves: Vec<_> = mmr_instances - .chunks(2) - .take(num_leaves) - .map(|hash| -> Vec<_> { - hash.iter() - .flat_map(|hash_u128| { - num_to_bytes_be(ctx, &range, hash_u128, NUM_BYTES_IN_U128) - }) - .collect() - }) - .collect(); - let new_mmr = keccak.merkle_mountain_range(ctx, range.gate(), &leaves, num_leaves_bits); - let new_mmr_len = new_mmr.len(); - debug_assert_eq!(new_mmr_len, max_depth - initial_depth + 1); - // convert from bytes to u128 - let new_mmr = - new_mmr.into_iter().zip(num_leaves_bits.iter().rev()).flat_map(|(hash_bytes, bit)| { - let hash_u128s: [_; 2] = - bytes_be_to_u128(ctx, range.gate(), &hash_bytes[..]).try_into().unwrap(); - hash_u128s.map(|hash_u128| range.gate().mul(ctx, hash_u128, *bit)) - }); - let mut assigned_instances = aggregation.inner.assigned_instances; - assigned_instances.extend_from_slice(&new_instances[..5]); - assigned_instances.extend(new_mmr); - assigned_instances.extend_from_slice(&mmr_instances[2 * num_leaves..]); - - let _prover = builder.witness_gen_only(); - let circuit = EthCircuitBuilder::new( - assigned_instances, - builder, - RefCell::new(keccak), - range, - break_points, - |_: &mut RlcThreadBuilder, - _: RlpChip, - _: (FixedLenRLCs, VarLenRLCs)| {}, - ); - #[cfg(feature = "display")] - end_timer!(timer); - #[cfg(not(feature = "production"))] - if !_prover { - let config_params: EthConfigParams = serde_json::from_str( - var("ETH_CONFIG_PARAMS").expect("ETH_CONFIG_PARAMS is not set").as_str(), - ) - .unwrap(); - circuit.config(config_params.degree as usize, Some(config_params.unusable_rows)); - } - circuit - } -} diff --git a/axiom-eth/src/block_header/helpers.rs b/axiom-eth/src/block_header/helpers.rs deleted file mode 100644 index 6b938c4a..00000000 --- a/axiom-eth/src/block_header/helpers.rs +++ /dev/null @@ -1,273 +0,0 @@ -use super::{ - aggregation::{ - EthBlockHeaderChainAggregationCircuit, EthBlockHeaderChainFinalAggregationCircuit, - }, - EthBlockHeaderChainCircuit, -}; -use crate::{ - util::{ - circuit::{AnyCircuit, PinnableCircuit}, - circuit::{PreCircuit, PublicAggregationCircuit}, - scheduler::{self, EthScheduler, Scheduler}, - AggregationConfigPinning, EthConfigPinning, Halo2ConfigPinning, - }, - AggregationPreCircuit, Network, -}; -use any_circuit_derive::AnyCircuit; -use core::cmp::min; -use halo2_base::{ - gates::builder::CircuitBuilderStage, - halo2_proofs::{ - halo2curves::bn256::{Bn256, Fr, G1Affine}, - plonk::ProvingKey, - poly::kzg::commitment::ParamsKZG, - }, -}; -use snark_verifier_sdk::Snark; -use std::{env::var, path::Path}; - -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -pub enum Finality { - /// Produces as many snarks as needed to fit the entire block number range, without any final processing. - None, - /// The block number range must fit within the specified max depth. - /// Produces a single final snark with the starting & ending block numbers, previous and last block hashes, - /// and merkle mountain range as output. - Merkle, - /// The block number range must fit within the specified max depth. `Evm(round)` performs `round + 1` - /// rounds of SNARK verification on the final `Merkle` circuit - Evm(usize), -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct CircuitType { - pub network: Network, - pub depth: usize, - pub initial_depth: usize, - pub finality: Finality, -} - -impl CircuitType { - pub fn new(depth: usize, initial_depth: usize, finality: Finality, network: Network) -> Self { - Self { depth, initial_depth, finality, network } - } - - pub fn prev(&self) -> Self { - assert!(self.depth != self.initial_depth, "Trying to call prev on initial circuit"); - match self.finality { - Finality::None | Finality::Merkle => { - Self::new(self.depth - 1, self.initial_depth, Finality::None, self.network) - } - Finality::Evm(round) => { - if round == 0 { - Self::new(self.depth, self.initial_depth, Finality::Merkle, self.network) - } else { - Self::new( - self.depth, - self.initial_depth, - Finality::Evm(round - 1), - self.network, - ) - } - } - } - } - - pub fn fname_prefix(&self) -> String { - if self.depth == self.initial_depth { - format!("{}_{}", self.network, self.depth) - } else { - format!("{}_{}_{}", self.network, self.depth, self.initial_depth) - } - } - - pub fn fname_suffix(&self) -> String { - match self.finality { - Finality::None => "".to_string(), - Finality::Merkle => "_final".to_string(), - Finality::Evm(round) => format!("_for_evm_{round}"), - } - } -} - -impl scheduler::CircuitType for CircuitType { - fn name(&self) -> String { - format!("{}{}", self.fname_prefix(), self.fname_suffix()) - } - - fn get_degree_from_pinning(&self, path: impl AsRef) -> u32 { - let CircuitType { network: _, depth, initial_depth, finality } = self; - if depth == initial_depth { - EthConfigPinning::from_path(path).degree() - } else { - match finality { - Finality::None | Finality::Evm(_) => { - AggregationConfigPinning::from_path(path).degree() - } - Finality::Merkle => EthConfigPinning::from_path(path).degree(), - } - } - } -} - -#[derive(Clone, Copy, Debug)] -pub struct Task { - pub start: u32, - pub end: u32, - pub circuit_type: CircuitType, -} - -impl Task { - pub fn new(start: u32, end: u32, circuit_type: CircuitType) -> Self { - Self { start, end, circuit_type } - } -} - -impl scheduler::Task for Task { - type CircuitType = CircuitType; - - fn circuit_type(&self) -> Self::CircuitType { - self.circuit_type - } - - fn name(&self) -> String { - format!( - "{}_{:06x}_{:06x}{}", - self.circuit_type.fname_prefix(), - self.start, - self.end, - self.circuit_type.fname_suffix() - ) - } - fn dependencies(&self) -> Vec { - let Task { start, end, circuit_type } = *self; - let CircuitType { network: _, depth, initial_depth, finality: _ } = circuit_type; - assert!(end - start < 1 << depth); - if depth == initial_depth { - vec![] - } else { - let prev_type = circuit_type.prev(); - let prev_depth = prev_type.depth; - (start..=end) - .step_by(1 << prev_depth) - .map(|i| Task::new(i, min(end, i + (1 << prev_depth) - 1), prev_type)) - .collect() - } - } -} - -/// The public/private inputs for various circuits. -// This is an enum of `PreCircuit`s. -// We implement `AnyCircuit` for `CircuitRouter` by passing through the implementations from each enum variant using a procedural macro. -// This is because Rust traits do not allow a single type to output different kinds of `PreCircuit`s. -#[derive(Clone, Debug, AnyCircuit)] -pub enum CircuitRouter { - Initial(EthBlockHeaderChainCircuit), - Intermediate(EthBlockHeaderChainAggregationCircuit), - Final(EthBlockHeaderChainFinalAggregationCircuit), - ForEvm(PublicAggregationCircuit), -} - -pub type BlockHeaderScheduler = EthScheduler; - -impl Scheduler for BlockHeaderScheduler { - type Task = Task; - type CircuitRouter = CircuitRouter; - - fn get_circuit(&self, task: Task, mut snarks: Vec) -> CircuitRouter { - let Task { start, end, circuit_type } = task; - let CircuitType { network, depth, initial_depth, finality } = circuit_type; - assert_eq!(network, self.network); - assert!(end - start < 1 << depth); - if depth == initial_depth { - let circuit = EthBlockHeaderChainCircuit::from_provider( - &self.provider, - network, - start, - end - start + 1, - depth, - ); - CircuitRouter::Initial(circuit) - } else { - assert!(!snarks.is_empty()); - match finality { - Finality::None => { - if snarks.len() != 2 { - snarks.resize(2, snarks[0].clone()); // dummy snark - } - let circuit = EthBlockHeaderChainAggregationCircuit::new( - snarks, - end - start + 1, - depth, - initial_depth, - ); - CircuitRouter::Intermediate(circuit) - } - Finality::Merkle => { - if snarks.len() != 2 { - snarks.resize(2, snarks[0].clone()); // dummy snark - } - let circuit = EthBlockHeaderChainFinalAggregationCircuit::new( - snarks, - end - start + 1, - depth, - initial_depth, - ); - CircuitRouter::Final(circuit) - } - Finality::Evm(_) => { - assert_eq!(snarks.len(), 1); // currently just passthrough - CircuitRouter::ForEvm(PublicAggregationCircuit::new( - snarks.into_iter().map(|snark| (snark, true)).collect(), - )) - } - } - } - } -} - -impl PreCircuit for EthBlockHeaderChainAggregationCircuit { - type Pinning = AggregationConfigPinning; - - fn create_circuit( - self, - stage: CircuitBuilderStage, - pinning: Option, - params: &ParamsKZG, - ) -> impl PinnableCircuit { - // look for lookup_bits either from pinning, if available, or from env var - let lookup_bits = pinning - .as_ref() - .map(|p| p.params.lookup_bits) - .or_else(|| var("LOOKUP_BITS").map(|v| v.parse().unwrap()).ok()) - .expect("LOOKUP_BITS is not set"); - let break_points = pinning.map(|p| p.break_points()); - AggregationPreCircuit::create_circuit(self, stage, break_points, lookup_bits, params) - } -} - -impl PreCircuit for EthBlockHeaderChainFinalAggregationCircuit { - type Pinning = EthConfigPinning; - - fn create_circuit( - self, - stage: CircuitBuilderStage, - pinning: Option, - params: &ParamsKZG, - ) -> impl PinnableCircuit { - // look for lookup_bits either from pinning, if available, or from env var - let lookup_bits = pinning - .as_ref() - .map(|p| p.params.lookup_bits.unwrap()) - .or_else(|| var("LOOKUP_BITS").map(|v| v.parse().unwrap()).ok()) - .expect("LOOKUP_BITS is not set"); - let break_points = pinning.map(|p| p.break_points()); - EthBlockHeaderChainFinalAggregationCircuit::create_circuit( - self, - stage, - break_points, - lookup_bits, - params, - ) - } -} diff --git a/axiom-eth/src/block_header/mod.rs b/axiom-eth/src/block_header/mod.rs index e8908dd1..3693da6e 100644 --- a/axiom-eth/src/block_header/mod.rs +++ b/axiom-eth/src/block_header/mod.rs @@ -1,71 +1,78 @@ -use super::{util::bytes_be_to_u128, Field, Network}; +use crate::Field; use crate::{ - keccak::{ - parallelize_keccak_phase0, ContainsParallelizableKeccakQueries, FixedLenRLCs, FnSynthesize, - KeccakChip, VarLenRLCs, + keccak::{types::KeccakVarLenQuery, KeccakChip}, + rlc::{ + chip::RlcChip, + circuit::builder::{RlcCircuitBuilder, RlcContextPair}, + types::RlcTrace, }, rlp::{ - builder::{parallelize_phase1, RlcThreadBreakPoints, RlcThreadBuilder}, - rlc::{RlcContextPair, RlcFixedTrace, RlcTrace, FIRST_PHASE, RLC_PHASE}, - RlpArrayTraceWitness, RlpChip, RlpFieldTrace, RlpFieldWitness, + evaluate_byte_array, max_rlp_encoding_len, + types::{RlpArrayWitness, RlpFieldTrace, RlpFieldWitness}, + RlpChip, }, - util::bytes_be_var_to_fixed, - EthChip, EthCircuitBuilder, EthPreCircuit, ETH_LOOKUP_BITS, + utils::{bytes_be_to_u128, AssignedH256}, }; -use core::{ - iter::{self, once}, - marker::PhantomData, -}; -#[cfg(feature = "providers")] -use ethers_providers::{JsonRpcClient, Provider}; +use core::iter::once; +use ethers_core::types::Chain; use halo2_base::{ - gates::{builder::GateThreadBuilder, GateInstructions, RangeChip, RangeInstructions}, - halo2_proofs::halo2curves::bn256::Fr, - utils::bit_length, + gates::{ + flex_gate::threads::parallelize_core, GateChip, GateInstructions, RangeChip, + RangeInstructions, + }, + safe_types::{left_pad_var_array_to_fixed, FixLenBytes, SafeTypeChip}, AssignedValue, Context, QuantumCell::{Constant, Existing}, }; use itertools::Itertools; -use std::cell::RefCell; -#[cfg(feature = "aggregation")] -pub mod aggregation; -#[cfg(all(feature = "aggregation", feature = "providers"))] -pub mod helpers; #[cfg(test)] mod tests; // extra data max byte length is different for different networks pub const MAINNET_EXTRA_DATA_MAX_BYTES: usize = 32; -pub const MAINNET_EXTRA_DATA_RLP_MAX_BYTES: usize = MAINNET_EXTRA_DATA_MAX_BYTES + 1; -pub const GOERLI_EXTRA_DATA_MAX_BYTES: usize = 97; -pub const GOERLI_EXTRA_DATA_RLP_MAX_BYTES: usize = GOERLI_EXTRA_DATA_MAX_BYTES + 1; +pub const GOERLI_EXTRA_DATA_MAX_BYTES: usize = 300; /// This is the minimum possible RLP byte length of a block header *at any block* (including pre EIPs) pub const BLOCK_HEADER_RLP_MIN_BYTES: usize = 479; -/// The maximum possible RLP byte length of a block header *at any block* (including all EIPs). -/// -/// Provided that the total length is < 256^2, this will be 1 + 2 + sum(max RLP byte length of each field) -pub const MAINNET_BLOCK_HEADER_RLP_MAX_BYTES: usize = - 1 + 2 + (521 + MAINNET_EXTRA_DATA_RLP_MAX_BYTES + 33); // 33 is for withdrawals_root -pub const GOERLI_BLOCK_HEADER_RLP_MAX_BYTES: usize = - 1 + 2 + (521 + GOERLI_EXTRA_DATA_RLP_MAX_BYTES + 33); pub const MIN_NUM_BLOCK_HEADER_FIELDS: usize = 15; -pub const NUM_BLOCK_HEADER_FIELDS: usize = 17; -pub const MAINNET_HEADER_FIELDS_MAX_BYTES: [usize; NUM_BLOCK_HEADER_FIELDS] = - [32, 32, 20, 32, 32, 32, 256, 7, 4, 4, 4, 4, MAINNET_EXTRA_DATA_MAX_BYTES, 32, 8, 6, 32]; -pub const GOERLI_HEADER_FIELDS_MAX_BYTES: [usize; NUM_BLOCK_HEADER_FIELDS] = - [32, 32, 20, 32, 32, 32, 256, 7, 4, 4, 4, 4, GOERLI_EXTRA_DATA_MAX_BYTES, 32, 8, 6, 32]; +pub const NUM_BLOCK_HEADER_FIELDS: usize = 20; +pub const MAINNET_HEADER_FIELDS_MAX_BYTES: [usize; NUM_BLOCK_HEADER_FIELDS] = [ + 32, // parent_hash + 32, // ommers_hash + 20, // coinbase [beneficiary] + 32, // state_root + 32, // txs_root + 32, // receipts_root + 256, // logs_bloom + 7, // difficulty + 4, // number + 4, // gas_limit + 4, // gas_used + 4, // timestamp + MAINNET_EXTRA_DATA_MAX_BYTES, // extradata + 32, // mix_hash or prev_randao + 8, // nonce + 32, // base_fee_per_gas + 32, // withdrawals_root + 8, // data_gas_used + 8, // excess_data_gas + 32, // parent_beacon_block_root +]; pub const BLOCK_HEADER_FIELD_IS_VAR_LEN: [bool; NUM_BLOCK_HEADER_FIELDS] = [ false, false, false, false, false, false, false, true, true, true, true, true, true, false, - false, true, false, + false, true, false, true, true, false, ]; /// The maximum number of bytes it takes to represent a block number, without any RLP encoding. pub const BLOCK_NUMBER_MAX_BYTES: usize = MAINNET_HEADER_FIELDS_MAX_BYTES[BLOCK_NUMBER_INDEX]; -pub(crate) const STATE_ROOT_INDEX: usize = 3; -pub(crate) const BLOCK_NUMBER_INDEX: usize = 8; -pub(crate) const EXTRA_DATA_INDEX: usize = 12; +pub const STATE_ROOT_INDEX: usize = 3; +pub const TX_ROOT_INDEX: usize = 4; +pub const RECEIPT_ROOT_INDEX: usize = 5; +pub const LOGS_BLOOM_INDEX: usize = 6; +pub const BLOCK_NUMBER_INDEX: usize = 8; +pub const EXTRA_DATA_INDEX: usize = 12; +pub const WITHDRAWALS_ROOT_INDEX: usize = 16; /** | Field | Type | Size (bytes) | RLP size (bytes) | RLP size (bits) | @@ -85,8 +92,11 @@ pub(crate) const EXTRA_DATA_INDEX: usize = 12; | extraData (Mainnet) | up to 256 bits | variable, <= 32 | <= 33 | <= 264 | | mixHash | 256 bits | 32 | 33 | 264 | | nonce | 64 bits | 8 | 9 | 72 | -| basefee (post-1559) | big int scalar | variable | <= 6 | <= 48 | -| withdrawalsRoot (post-4895) | 256 bits | 32 | 33 | 264 | +| basefee (post-1559) | big int scalar | variable, <=32 | <= 33 | <= 264 | +| withdrawalsRoot (post-4895) | 256 bits | 32 | 33 | 264 | +| blobGasUsed (post-4844) | 64 bits | <= 8 | <= 9 | <= 72 | +| excessBlobGas (post-4844) | 64 bits | <= 8 | <= 9 | <= 72 | +| parentBeaconBlockRoot (post-4788) | 256 bits | 32 | 33 | 264 | */ #[allow(dead_code)] #[derive(Clone, Debug)] @@ -110,19 +120,52 @@ pub struct EthBlockHeaderTrace { pub basefee: RlpFieldTrace, // this is 0 (or undefined) for pre-EIP1559 (London) blocks pub withdrawals_root: RlpFieldTrace, // this is 0 (or undefined) for pre-EIP4895 (Shapella) blocks (before block number 1681338455) // the user will have to separately determine whether the block is EIP1559 or not - pub block_hash: RlcFixedTrace, + pub block_hash: KeccakVarLenQuery, // pub prefix: AssignedValue, pub len_trace: RlcTrace, } + #[derive(Clone, Debug)] -pub struct EthBlockHeaderTraceWitness { - pub rlp_witness: RlpArrayTraceWitness, - pub block_hash: Vec>, - pub block_hash_query_idx: usize, +pub struct EthBlockHeaderWitness { + pub rlp_witness: RlpArrayWitness, + pub block_hash: KeccakVarLenQuery, } -impl EthBlockHeaderTraceWitness { +impl EthBlockHeaderWitness { + /// Returns block number as bytes4 (left padded with zeros, big endian) + pub fn get_number_fixed( + &self, + ctx: &mut Context, + gate: &impl GateInstructions, + ) -> FixLenBytes { + let block_num_bytes = &self.get_number().field_cells; + let block_num_len = self.get_number().field_len; + SafeTypeChip::unsafe_to_fix_len_bytes( + left_pad_var_array_to_fixed( + ctx, + gate, + block_num_bytes, + block_num_len, + BLOCK_NUMBER_MAX_BYTES, + ) + .try_into() + .unwrap(), + ) + } + /// Returns block number as a field element + pub fn get_number_value( + &self, + ctx: &mut Context, + gate: &impl GateInstructions, + ) -> AssignedValue { + let block_num_bytes = &self.get_number().field_cells; + let block_num_len = self.get_number().field_len; + evaluate_byte_array(ctx, gate, block_num_bytes, block_num_len) + } + pub fn get_block_hash_hi_lo(&self) -> AssignedH256 { + self.block_hash.hi_lo() + } pub fn get_parent_hash(&self) -> &RlpFieldWitness { &self.rlp_witness.field_witness[0] } @@ -177,156 +220,99 @@ impl EthBlockHeaderTraceWitness { pub fn get_index(&self, idx: usize) -> Option<&RlpFieldWitness> { self.rlp_witness.field_witness.get(idx) } + /// Returns the number of fields in the block header + pub fn get_list_len(&self) -> AssignedValue { + self.rlp_witness.list_len.unwrap() + } } -impl ContainsParallelizableKeccakQueries for EthBlockHeaderTraceWitness { - // Currently all indices are with respect to `keccak.var_len_queries` - fn shift_query_indices(&mut self, _: usize, var_shift: usize) { - self.block_hash_query_idx += var_shift; - } +pub struct EthBlockHeaderChip<'chip, F: Field> { + pub rlp: RlpChip<'chip, F>, + pub max_extra_data_bytes: usize, } -pub trait EthBlockHeaderChip { - /// Takes the variable length RLP encoded block header, padded with 0s to the maximum possible block header RLP length, and outputs the decomposition into block header fields. - /// - /// In addition, the keccak block hash of the block is calculated. - /// - /// Assumes `block_header` and `block_header_assigned` have the same values as bytes. The former is only used for faster witness generation. - /// - /// This is the preparation step that computes the witnesses. This MUST be done in `FirstPhase`. - /// The accompanying `decompose_block_header_finalize` must be called in `SecondPhase` to constrain the RLCs associated to the RLP decoding. - fn decompose_block_header_phase0( - &self, - ctx: &mut Context, - keccak: &mut KeccakChip, - block_header_rlp: &[u8], - network: Network, - ) -> EthBlockHeaderTraceWitness; +impl<'chip, F: Field> EthBlockHeaderChip<'chip, F> { + pub fn new(rlp: RlpChip<'chip, F>, max_extra_data_bytes: usize) -> Self { + Self { rlp, max_extra_data_bytes } + } - /// Takes the variable length RLP encoded block header, padded with 0s to the maximum possible block header RLP length, and outputs the decomposition into block header fields. - /// - /// In addition, the keccak block hash of the block is calculated. - /// - /// Assumes `block_header` and `block_header_assigned` have the same values as bytes. The former is only used for faster witness generation. - /// - /// This is the finalization step that constrains RLC concatenations. - /// This should be called after `decompose_block_header_phase0`. - /// This MUST be done in `SecondPhase`. - /// - /// WARNING: This function is not thread-safe unless you call `load_rlc_cache` ahead of time. - fn decompose_block_header_phase1( - &self, - ctx: RlcContextPair, - witness: EthBlockHeaderTraceWitness, - ) -> EthBlockHeaderTrace; + pub fn new_from_network(rlp: RlpChip<'chip, F>, chain: Chain) -> Self { + let max_extra_data_bytes = get_block_header_extra_bytes(chain); + Self { rlp, max_extra_data_bytes } + } - /// Makes multiple calls to `decompose_block_header_phase0` in parallel threads. Should be called in FirstPhase. - fn decompose_block_headers_phase0( - &self, - thread_pool: &mut GateThreadBuilder, - keccak: &mut KeccakChip, - block_headers: Vec>, - network: Network, - ) -> Vec> - where - Self: Sync, - { - parallelize_keccak_phase0( - thread_pool, - keccak, - block_headers, - |ctx, keccak, block_header| { - self.decompose_block_header_phase0(ctx, keccak, &block_header, network) - }, - ) + pub fn gate(&self) -> &GateChip { + self.rlp.gate() } - /// Makes multiple calls to `decompose_block_header_phase1` in parallel threads. Should be called in SecondPhase. - fn decompose_block_headers_phase1( - &self, - thread_pool: &mut RlcThreadBuilder, - witnesses: Vec>, - ) -> Vec>; + pub fn range(&self) -> &RangeChip { + self.rlp.range() + } - /// Takes a list of (purported) RLP encoded block headers and - /// decomposes each header into it's fields. - /// `headers[0]` is the earliest block. - /// - /// This is the preparation step that computes the witnesses. This MUST be done in `FirstPhase`. - /// The accompanying `decompose_block_header_chain_phase1` must be called in `SecondPhase` to constrain the RLCs associated to the RLP decoding. - fn decompose_block_header_chain_phase0( - &self, - thread_pool: &mut GateThreadBuilder, - keccak: &mut KeccakChip, - block_headers: Vec>, - network: Network, - ) -> Vec> - where - Self: Sync, - { - self.decompose_block_headers_phase0(thread_pool, keccak, block_headers, network) + pub fn rlc(&self) -> &RlcChip { + self.rlp.rlc() } - /// Takes a list of `2^max_depth` (purported) RLP encoded block headers. - /// Decomposes each header into it's fields. - /// `headers[0]` is the earliest block + pub fn rlp(&self) -> &RlpChip { + &self.rlp + } + + /// Takes the variable length RLP encoded block header, padded with 0s to the maximum possible block header RLP length, and outputs the decomposition into block header fields. /// - /// - If `num_blocks_minus_one = (num_blocks_minus_one, indicator)` is not None, then the circuit checks that the first `num_blocks := num_blocks_minus_one + 1` block headers form a chain: meaning that the parent hash of block i + 1 equals the hash of block i. - /// - `indicator` is a vector with index `i` equal to `i == num_blocks - 1 ? 1 : 0`. - /// - Otherwise if `num_blocks` is None, the circuit checks that all `headers` form a hash chain. + /// This function _will_ range check that `block_header` consists of bytes (8 bits). /// - /// Assumes that `0 <= num_blocks_minus_one < 2^max_depth`. + /// In addition, the keccak block hash of the block is calculated. /// - /// This is the finalization step that constrains RLC concatenations. In this step the hash chain is actually constrained. - /// This should be called after `decompose_block_header_chain_phase0`. - /// This MUST be done in `SecondPhase`. - fn decompose_block_header_chain_phase1( - &self, - thread_pool: &mut RlcThreadBuilder, - witnesses: Vec>, - num_blocks_minus_one: Option<(AssignedValue, Vec>)>, - ) -> Vec>; -} - -impl<'chip, F: Field> EthBlockHeaderChip for EthChip<'chip, F> { - fn decompose_block_header_phase0( + /// This is the preparation step that computes the witnesses. This MUST be done in `FirstPhase`. + /// The accompanying `decompose_block_header_phase1` must be called in `SecondPhase` to constrain the RLCs associated to the RLP decoding. + pub fn decompose_block_header_phase0( &self, ctx: &mut Context, // ctx_gate in FirstPhase - keccak: &mut KeccakChip, - block_header: &[u8], - network: Network, - ) -> EthBlockHeaderTraceWitness { - let (max_len, max_field_lens) = get_block_header_rlp_max_lens(network); + keccak: &KeccakChip, + block_header: &[AssignedValue], + ) -> EthBlockHeaderWitness { + let (max_len, max_field_lens) = + get_block_header_rlp_max_lens_from_extra(self.max_extra_data_bytes); assert_eq!(block_header.len(), max_len); - let block_header_assigned = - ctx.assign_witnesses(block_header.iter().map(|byte| F::from(*byte as u64))); - let rlp_witness = - self.rlp().decompose_rlp_array_phase0(ctx, block_header_assigned, max_field_lens, true); // `is_variable_len = true` because RLP can have between 15 to 17 fields, depending on which EIPs are active at that block + // range check that block_header consists of bytes + for b in block_header { + self.range().range_check(ctx, *b, 8); + } + + let rlp_witness = self.rlp().decompose_rlp_array_phase0( + ctx, + block_header.to_vec(), + &max_field_lens, + true, + ); // `is_variable_len = true` because RLP can have >=15 fields, depending on which EIPs are active at that block - let block_hash_query_idx = keccak.keccak_var_len( + let block_hash = keccak.keccak_var_len( ctx, - self.range(), rlp_witness.rlp_array.clone(), // this is `block_header_assigned` - Some(block_header.to_vec()), rlp_witness.rlp_len, BLOCK_HEADER_RLP_MIN_BYTES, ); - let block_hash = keccak.var_len_queries[block_hash_query_idx].output_assigned.clone(); - EthBlockHeaderTraceWitness { rlp_witness, block_hash, block_hash_query_idx } + EthBlockHeaderWitness { rlp_witness, block_hash } } - fn decompose_block_header_phase1( + /// Takes the variable length RLP encoded block header, padded with 0s to the maximum possible block header RLP length, and outputs the decomposition into block header fields. + /// + /// In addition, the keccak block hash of the block is calculated. + /// + /// This is the finalization step that constrains RLC concatenations. + /// This should be called after `decompose_block_header_phase0`. + /// This MUST be done in `SecondPhase`. + pub fn decompose_block_header_phase1( &self, ctx: RlcContextPair, - witness: EthBlockHeaderTraceWitness, + witness: EthBlockHeaderWitness, ) -> EthBlockHeaderTrace { let trace = self.rlp().decompose_rlp_array_phase1(ctx, witness.rlp_witness, true); - let block_hash = self.keccak_var_len_rlcs()[witness.block_hash_query_idx].1; + let block_hash = witness.block_hash; // Base fee per unit gas only after London - let [parent_hash, ommers_hash, beneficiary, state_root, transactions_root, receipts_root, logs_bloom, difficulty, number, gas_limit, gas_used, timestamp, extra_data, mix_hash, nonce, basefee, withdrawals_root]: [RlpFieldTrace; NUM_BLOCK_HEADER_FIELDS] = + let [parent_hash, ommers_hash, beneficiary, state_root, transactions_root, receipts_root, logs_bloom, difficulty, number, gas_limit, gas_used, timestamp, extra_data, mix_hash, nonce, basefee, withdrawals_root, ..]: [RlpFieldTrace; NUM_BLOCK_HEADER_FIELDS] = trace.field_trace.try_into().unwrap(); - EthBlockHeaderTrace { parent_hash, ommers_hash, @@ -350,40 +336,78 @@ impl<'chip, F: Field> EthBlockHeaderChip for EthChip<'chip, F> { } } - fn decompose_block_headers_phase1( + /// Makes multiple calls to `decompose_block_header_phase0` in parallel threads. Should be called in FirstPhase. + pub fn decompose_block_headers_phase0( + &self, + builder: &mut RlcCircuitBuilder, + keccak: &KeccakChip, + block_headers: Vec>>, + ) -> Vec> { + parallelize_core(builder.base.pool(0), block_headers, |ctx, block_header| { + self.decompose_block_header_phase0(ctx, keccak, &block_header) + }) + } + + /// Makes multiple calls to `decompose_block_header_phase1` in parallel threads. Should be called in SecondPhase. + pub fn decompose_block_headers_phase1( &self, - thread_pool: &mut RlcThreadBuilder, - witnesses: Vec>, + builder: &mut RlcCircuitBuilder, + witnesses: Vec>, ) -> Vec> { assert!(!witnesses.is_empty()); - let ctx = thread_pool.rlc_ctx_pair(); - // to ensure thread-safety of the later calls, we load rlc_cache to the max length first. - // assuming this is called after `decompose_block_header_chain_phase0`, all headers should be same length = max_len - let cache_bits = bit_length(witnesses[0].rlp_witness.rlp_array.len() as u64); - self.rlc().load_rlc_cache(ctx, self.gate(), cache_bits); - // now multi-threading: - parallelize_phase1(thread_pool, witnesses, |(ctx_gate, ctx_rlc), witness| { + // `load_rlc_cache` no longer called here: it should be called globally when `RlcCircuitBuilder` is constructed + builder.parallelize_phase1(witnesses, |(ctx_gate, ctx_rlc), witness| { self.decompose_block_header_phase1((ctx_gate, ctx_rlc), witness) }) } - fn decompose_block_header_chain_phase1( + /// Takes a list of (purported) RLP encoded block headers and + /// decomposes each header into it's fields. + /// `headers[0]` is the earliest block. + /// + /// This is the preparation step that computes the witnesses. This MUST be done in `FirstPhase`. + /// The accompanying `decompose_block_header_chain_phase1` must be called in `SecondPhase` to constrain the RLCs associated to the RLP decoding. + pub fn decompose_block_header_chain_phase0( + &self, + builder: &mut RlcCircuitBuilder, + keccak: &KeccakChip, + block_headers: Vec>>, + ) -> Vec> { + self.decompose_block_headers_phase0(builder, keccak, block_headers) + } + + /// Takes a list of `2^max_depth` (purported) RLP encoded block headers. + /// Decomposes each header into it's fields. + /// `headers[0]` is the earliest block + /// + /// - If `num_blocks_minus_one = (num_blocks_minus_one, indicator)` is not None, then the circuit checks that the first `num_blocks := num_blocks_minus_one + 1` block headers form a chain: meaning that the parent hash of block i + 1 equals the hash of block i. + /// - `indicator` is a vector with index `i` equal to `i == num_blocks - 1 ? 1 : 0`. + /// - Otherwise if `num_blocks` is None, the circuit checks that all `headers` form a hash chain. + /// + /// Assumes that `0 <= num_blocks_minus_one < 2^max_depth`. + /// + /// This is the finalization step that constrains RLC concatenations. In this step the hash chain is actually constrained. + /// This should be called after `decompose_block_header_chain_phase0`. + /// This MUST be done in `SecondPhase`. + pub fn decompose_block_header_chain_phase1( &self, - thread_pool: &mut RlcThreadBuilder, - witnesses: Vec>, + builder: &mut RlcCircuitBuilder, + witnesses: Vec>, num_blocks_minus_one: Option<(AssignedValue, Vec>)>, ) -> Vec> { assert!(!witnesses.is_empty()); - let traces = self.decompose_block_headers_phase1(thread_pool, witnesses); - let ctx_gate = thread_pool.gate_builder.main(RLC_PHASE); - let thirty_two = self.gate().get_field_element(32); + let traces = self.decompose_block_headers_phase1(builder, witnesses); + let (ctx_gate, ctx_rlc) = builder.rlc_ctx_pair(); + let thirty_two = F::from(32); // record for each idx whether hash of headers[idx] is in headers[idx + 1] if let Some((num_blocks_minus_one, indicator)) = num_blocks_minus_one { let mut hash_checks = Vec::with_capacity(traces.len() - 1); for idx in 0..traces.len() - 1 { + let block_hash_bytes = traces[idx].block_hash.output_bytes.as_ref().iter().copied(); + let block_hash = self.rlc().compute_rlc_fixed_len(ctx_rlc, block_hash_bytes); let hash_check = self.gate().is_equal( ctx_gate, - traces[idx].block_hash.rlc_val, + block_hash.rlc_val, traces[idx + 1].parent_hash.field_trace.rlc_val, ); hash_checks.push(hash_check); @@ -397,14 +421,16 @@ impl<'chip, F: Field> EthBlockHeaderChip for EthChip<'chip, F> { self.gate().partial_sums(ctx_gate, hash_checks.iter().copied()).collect_vec(); let hash_check_sum = self.gate().select_by_indicator( ctx_gate, - once(Constant(F::zero())).chain(hash_check_sums.into_iter().map(Existing)), + once(Constant(F::ZERO)).chain(hash_check_sums.into_iter().map(Existing)), indicator, ); ctx_gate.constrain_equal(&hash_check_sum, &num_blocks_minus_one); } else { for idx in 0..traces.len() - 1 { + let block_hash_bytes = traces[idx].block_hash.output_bytes.as_ref().iter().copied(); + let block_hash = self.rlc().compute_rlc_fixed_len(ctx_rlc, block_hash_bytes); ctx_gate.constrain_equal( - &traces[idx].block_hash.rlc_val, + &block_hash.rlc_val, &traces[idx + 1].parent_hash.field_trace.rlc_val, ); self.gate().assert_is_const( @@ -441,57 +467,46 @@ impl<'chip, F: Field> EthBlockHeaderChip for EthChip<'chip, F> { pub fn get_boundary_block_data( ctx: &mut Context, // ctx_gate in FirstPhase gate: &impl GateInstructions, - chain: &[EthBlockHeaderTraceWitness], + chain: &[EthBlockHeaderWitness], indicator: &[AssignedValue], ) -> ([AssignedValue; 2], [AssignedValue; 2], AssignedValue) { + let parent_hash_bytes = SafeTypeChip::unsafe_to_fix_len_bytes_vec( + chain[0].get_parent_hash().field_cells.clone(), + 32, + ); let prev_block_hash: [_; 2] = - bytes_be_to_u128(ctx, gate, &chain[0].get_parent_hash().field_cells).try_into().unwrap(); - let end_block_hash: [_; 2] = { - let end_block_hash_bytes = (0..32) - .map(|idx| { - gate.select_by_indicator( - ctx, - chain.iter().map(|header| header.block_hash[idx]), - indicator.iter().copied(), - ) - }) - .collect_vec(); - bytes_be_to_u128(ctx, gate, &end_block_hash_bytes).try_into().unwrap() - }; + bytes_be_to_u128(ctx, gate, parent_hash_bytes.bytes()).try_into().unwrap(); + let end_block_hash: [_; 2] = [0, 1].map(|idx| { + gate.select_by_indicator( + ctx, + chain.iter().map(|header| header.block_hash.hi_lo()[idx]), + indicator.iter().copied(), + ) + }); // start_block_number || end_block_number let block_numbers = { debug_assert_eq!(chain[0].get_number().max_field_len, BLOCK_NUMBER_MAX_BYTES); - let start_block_number_bytes = bytes_be_var_to_fixed( - ctx, - gate, - &chain[0].get_number().field_cells, - chain[0].get_number().field_len, - BLOCK_NUMBER_MAX_BYTES, - ); - // TODO: is there a way to do this without so many selects - let end_block_number_bytes: [_; BLOCK_NUMBER_MAX_BYTES] = - core::array::from_fn(|i| i).map(|idx| { + let start_block_number_bytes = chain[0].get_number_fixed(ctx, gate); + let end_block_number_bytes = { + // TODO: is there a way to do this without so many selects + let bytes: [_; BLOCK_NUMBER_MAX_BYTES] = core::array::from_fn(|i| i).map(|idx| { gate.select_by_indicator( ctx, chain.iter().map(|header| header.get_number().field_cells[idx]), indicator.iter().copied(), ) }); - let end_block_number_len = gate.select_by_indicator( - ctx, - chain.iter().map(|header| header.get_number().field_len), - indicator.iter().copied(), - ); - let mut end_block_number_bytes = bytes_be_var_to_fixed( - ctx, - gate, - &end_block_number_bytes, - end_block_number_len, - BLOCK_NUMBER_MAX_BYTES, - ); - let mut block_numbers_bytes = start_block_number_bytes; - block_numbers_bytes.append(&mut end_block_number_bytes); + let len = gate.select_by_indicator( + ctx, + chain.iter().map(|header| header.get_number().field_len), + indicator.iter().copied(), + ); + let var_bytes = SafeTypeChip::unsafe_to_var_len_bytes(bytes, len); + var_bytes.left_pad_to_fixed(ctx, gate) + }; + let block_numbers_bytes = + [start_block_number_bytes.into_bytes(), end_block_number_bytes.into_bytes()].concat(); let [block_numbers]: [_; 1] = bytes_be_to_u128(ctx, gate, &block_numbers_bytes).try_into().unwrap(); block_numbers @@ -500,141 +515,64 @@ pub fn get_boundary_block_data( (prev_block_hash, end_block_hash, block_numbers) } -#[derive(Clone, Debug)] -/// The input datum for the block header chain circuit. It is used to generate a circuit. -pub struct EthBlockHeaderChainCircuit { - /// The private inputs, which are the RLP encodings of the block headers - header_rlp_encodings: Vec>, - num_blocks: u32, // num_blocks in [0, 2 ** max_depth) - // The public inputs: - // (prev_hash, end_hash, start_block_number, end_block_number, merkle_roots: [H256; max_depth + 1]) - // pub instance: EthBlockHeaderChainInstance, - max_depth: usize, - network: Network, - _marker: PhantomData, +pub fn get_block_header_rlp_max_lens(chain: Chain) -> (usize, [usize; NUM_BLOCK_HEADER_FIELDS]) { + get_block_header_rlp_max_lens_from_chain_id(chain as u64) } -impl EthBlockHeaderChainCircuit { - #[cfg(feature = "providers")] - pub fn from_provider( - provider: &Provider

, - network: Network, - start_block_number: u32, - num_blocks: u32, - max_depth: usize, - ) -> Self { - let (header_rlp_max_bytes, _) = get_block_header_rlp_max_lens(network); - let mut block_rlps = - crate::providers::get_blocks_input(provider, start_block_number, num_blocks, max_depth); - for block_rlp in block_rlps.iter_mut() { - block_rlp.resize(header_rlp_max_bytes, 0u8); - } - - Self { - header_rlp_encodings: block_rlps, - num_blocks, - max_depth, - network, - _marker: PhantomData, - } +pub fn get_block_header_rlp_max_lens_from_extra( + max_extra_data_bytes: usize, +) -> (usize, [usize; NUM_BLOCK_HEADER_FIELDS]) { + let mut field_lens = [0usize; NUM_BLOCK_HEADER_FIELDS]; + for (a, b) in field_lens.iter_mut().zip_eq(MAINNET_HEADER_FIELDS_MAX_BYTES.iter()) { + *a = *b; } - - pub fn create( - self, - mut builder: RlcThreadBuilder, - break_points: Option, - ) -> EthCircuitBuilder> { - let range = RangeChip::default(ETH_LOOKUP_BITS); - // KECCAK_ROWS should be set if prover = true - let chip = EthChip::new(RlpChip::new(&range, None), None); - let mut keccak = KeccakChip::default(); - - let ctx = builder.gate_builder.main(FIRST_PHASE); - // ======== FIRST PHASE =========== - // ==== Load private inputs ===== - let num_blocks = ctx.load_witness(F::from(self.num_blocks as u64)); - let num_blocks_minus_one = chip.gate().sub(ctx, num_blocks, Constant(F::one())); - // `num_blocks_minus_one` should be < 2^max_depth. - // We check this for safety, although it is not technically necessary because `num_blocks_minus_one` will equal the difference of the start, end block numbers, which are public inputs - chip.range().range_check(ctx, num_blocks_minus_one, self.max_depth); - - // ==== Load RLP encoding and decode ==== - // The block header RLPs are assigned as witnesses in this function - let block_chain_witness = chip.decompose_block_header_chain_phase0( - &mut builder.gate_builder, - &mut keccak, - self.header_rlp_encodings, - self.network, - ); - // All keccaks must be done in FirstPhase, so we compute the merkle mountain range from the RLP decoded witnesses now - let ctx = builder.gate_builder.main(FIRST_PHASE); - let num_leaves_bits = chip.gate().num_to_bits(ctx, num_blocks, self.max_depth + 1); - let block_hashes = block_chain_witness - .iter() - .map(|witness| { - keccak.var_len_queries[witness.block_hash_query_idx].output_assigned.clone() - }) - .collect_vec(); - // mountain range in bytes - let mountain_range = - keccak.merkle_mountain_range(ctx, chip.gate(), &block_hashes, &num_leaves_bits); - let mountain_range = mountain_range - .into_iter() - .zip(num_leaves_bits.into_iter().rev()) - .flat_map(|(hash_bytes, bit)| { - // convert bytes32 to two u128 - let hash_u128s: [_; 2] = - bytes_be_to_u128(ctx, chip.gate(), &hash_bytes).try_into().unwrap(); - // if the bit is 0, then we set the hash root to 0 - hash_u128s.map(|hash_u128| chip.gate().mul(ctx, hash_u128, bit)) - }) - .collect_vec(); - - let indicator = - chip.gate().idx_to_indicator(ctx, num_blocks_minus_one, block_chain_witness.len()); - let (prev_block_hash, end_block_hash, block_numbers) = - get_boundary_block_data(ctx, chip.gate(), &block_chain_witness, &indicator); - let assigned_instances = iter::empty() - .chain(prev_block_hash) - .chain(end_block_hash) - .chain(iter::once(block_numbers)) - .chain(mountain_range) - .collect_vec(); - - EthCircuitBuilder::new( - assigned_instances, - builder, - RefCell::new(keccak), - range, - break_points, - move |builder: &mut RlcThreadBuilder, - rlp: RlpChip, - keccak_rlcs: (FixedLenRLCs, VarLenRLCs)| { - // ======== SECOND PHASE =========== - let chip = EthChip::new(rlp, Some(keccak_rlcs)); - let _block_chain_trace = chip.decompose_block_header_chain_phase1( - builder, - block_chain_witness, - Some((num_blocks_minus_one, indicator)), - ); - }, - ) + field_lens[EXTRA_DATA_INDEX] = max_extra_data_bytes; + let mut list_payload_len = 0; + for &field_len in &field_lens { + list_payload_len += max_rlp_encoding_len(field_len); } + let rlp_len = max_rlp_encoding_len(list_payload_len); + (rlp_len, field_lens) } -impl EthPreCircuit for EthBlockHeaderChainCircuit { - fn create( - self, - builder: RlcThreadBuilder, - break_points: Option, - ) -> EthCircuitBuilder> { - self.create(builder, break_points) - } +pub fn get_block_header_extra_bytes(chain: Chain) -> usize { + get_block_header_extra_bytes_from_chain_id(chain as u64) } -pub fn get_block_header_rlp_max_lens(network: Network) -> (usize, &'static [usize]) { - match network { - Network::Mainnet => (MAINNET_BLOCK_HEADER_RLP_MAX_BYTES, &MAINNET_HEADER_FIELDS_MAX_BYTES), - Network::Goerli => (GOERLI_BLOCK_HEADER_RLP_MAX_BYTES, &GOERLI_HEADER_FIELDS_MAX_BYTES), +pub fn get_block_header_rlp_max_lens_from_chain_id( + chain_id: u64, +) -> (usize, [usize; NUM_BLOCK_HEADER_FIELDS]) { + let max_extra_data_bytes = get_block_header_extra_bytes_from_chain_id(chain_id); + get_block_header_rlp_max_lens_from_extra(max_extra_data_bytes) +} + +pub fn get_block_header_extra_bytes_from_chain_id(chain_id: u64) -> usize { + match chain_id { + 5 => GOERLI_EXTRA_DATA_MAX_BYTES, + _ => MAINNET_EXTRA_DATA_MAX_BYTES, // for now everything besides Goerli assumed to be mainnet equivalent } } + +/// RLP of block number 0 on mainnet +pub const GENESIS_BLOCK_RLP: &[u8] = &[ + 249, 2, 20, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 160, 29, 204, 77, 232, 222, 199, 93, 122, 171, 133, 181, 103, 182, 204, 212, + 26, 211, 18, 69, 27, 148, 138, 116, 19, 240, 161, 66, 253, 64, 212, 147, 71, 148, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 160, 215, 248, 151, 79, 181, 172, 120, 217, + 172, 9, 155, 154, 213, 1, 139, 237, 194, 206, 10, 114, 218, 209, 130, 122, 23, 9, 218, 48, 88, + 15, 5, 68, 160, 86, 232, 31, 23, 27, 204, 85, 166, 255, 131, 69, 230, 146, 192, 248, 110, 91, + 72, 224, 27, 153, 108, 173, 192, 1, 98, 47, 181, 227, 99, 180, 33, 160, 86, 232, 31, 23, 27, + 204, 85, 166, 255, 131, 69, 230, 146, 192, 248, 110, 91, 72, 224, 27, 153, 108, 173, 192, 1, + 98, 47, 181, 227, 99, 180, 33, 185, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 133, 4, 0, 0, 0, 0, 128, 130, 19, 136, 128, 128, 160, + 17, 187, 232, 219, 78, 52, 123, 78, 140, 147, 124, 28, 131, 112, 228, 181, 237, 51, 173, 179, + 219, 105, 203, 219, 122, 56, 225, 229, 11, 27, 130, 250, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136, 0, 0, 0, 0, 0, 0, 0, 66, +]; diff --git a/axiom-eth/src/block_header/tests.rs b/axiom-eth/src/block_header/tests.rs new file mode 100644 index 00000000..3b837dfc --- /dev/null +++ b/axiom-eth/src/block_header/tests.rs @@ -0,0 +1,192 @@ +use crate::{ + mpt::MPTChip, + rlc::{circuit::RlcCircuitParams, tests::get_rlc_params}, + utils::{ + assign_vec, + eth_circuit::{create_circuit, EthCircuitImpl, EthCircuitInstructions}, + }, +}; + +use super::*; +use ark_std::{end_timer, start_timer}; +use halo2_base::{ + gates::circuit::CircuitBuilderStage, + halo2_proofs::{ + dev::MockProver, + halo2curves::bn256::{Bn256, Fr}, + plonk::*, + poly::kzg::commitment::ParamsKZG, + }, + utils::testing::{check_proof_with_instances, gen_proof_with_instances}, +}; +use hex::FromHex; +use rand_core::OsRng; +use std::marker::PhantomData; +use test_log::test; + +/// The maximum possible RLP byte length of a block header *at any block* (including all EIPs). +/// +/// Provided that the total length is < 256^2, this will be 1 + 2 + sum(max RLP byte length of each field) +const MAINNET_EXTRA_DATA_RLP_MAX_BYTES: usize = max_rlp_encoding_len(MAINNET_EXTRA_DATA_MAX_BYTES); +const MAINNET_BLOCK_HEADER_RLP_MAX_BYTES: usize = + 1 + 2 + (515 + MAINNET_EXTRA_DATA_RLP_MAX_BYTES + 33 + 33 + 9 * 2 + 33); // 33 + 33 is for basefee, withdrawals_root, 9*2 for eip-4844, 33 for eip-4788 + +#[test] +fn test_block_header_rlp_max_lens() { + let from_chain = get_block_header_rlp_max_lens_from_chain_id(1); + assert_eq!(from_chain.0, MAINNET_BLOCK_HEADER_RLP_MAX_BYTES); + assert_eq!(from_chain.1, MAINNET_HEADER_FIELDS_MAX_BYTES); +} + +#[derive(Clone)] +struct HeaderTest { + headers: Vec>, + _marker: PhantomData, +} + +struct HeaderWitness { + chain: Vec>, +} + +impl EthCircuitInstructions for HeaderTest { + type FirstPhasePayload = HeaderWitness; + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + ) -> Self::FirstPhasePayload { + let chip = EthBlockHeaderChip::new_from_network(mpt.rlp, Chain::Mainnet); + let ctx = builder.base.main(0); + let inputs = self + .headers + .iter() + .map(|header| assign_vec(ctx, header.clone(), MAINNET_BLOCK_HEADER_RLP_MAX_BYTES)) + .collect_vec(); + let chain = chip.decompose_block_header_chain_phase0(builder, mpt.keccak, inputs); + HeaderWitness { chain } + } + + fn virtual_assign_phase1( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + witness: Self::FirstPhasePayload, + ) { + let chip = EthBlockHeaderChip::new_from_network(mpt.rlp, Chain::Mainnet); + chip.decompose_block_header_chain_phase1(builder, witness.chain, None); + } +} + +fn block_header_test_circuit( + stage: CircuitBuilderStage, + inputs: Vec>, + params: RlcCircuitParams, +) -> EthCircuitImpl> { + let test = HeaderTest { headers: inputs, _marker: PhantomData }; + let mut circuit = create_circuit(stage, params, test); + circuit.mock_fulfill_keccak_promises(None); + if !stage.witness_gen_only() { + circuit.calculate_params(); + } + circuit +} + +#[test] +pub fn test_one_mainnet_header_mock() { + let params = get_rlc_params("configs/tests/one_block.json"); + let k = params.base.k as u32; + let input_hex = "f90201a0d7519abd494a823b2c9c28908eaf250fe4a6287d747f1cc53a5a193b6533a549a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347944675c7e5baafbffbca748158becba61ef3b0a263a025000d51f040ee5c473fed74eda9ace87d55a35187b11bcde6f5176025c395bfa0a5800a6de6d28d7425ff72714af2af769b9f8f9e1baf56fb42f793fbb40fde07a056e1062a3dc63791e8a8496837606b14062da70ee69178cea97d6eeb5047550cb9010000236420014dc00423903000840002280080282100004704018340c0241c20011211400426000f900001d8088000011006020002ce98bc00c0000020c9a02040000688040200348c3a0082b81402002814922008085d008008200802802c4000130000101703124801400400018008a6108002020420144011200070020bc0202681810804221304004800088600300000040463614a000e200201c00611c0008e800b014081608010a0218a0b410010082000428209080200f50260a00840006700100f40a000000400000448301008c4a00341040e343500800d06250020010215200c008018002c88350404000bc5000a8000210c00724a0d0a4010210a448083eee2468401c9c3808343107884633899e780a07980d8d1f15474c9185e4d1cef5f207167735009daad2eb6af6da37ffba213c28800000000000000008501e08469e60000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + let mut input_bytes: Vec = Vec::from_hex(input_hex).unwrap(); + input_bytes.resize(MAINNET_BLOCK_HEADER_RLP_MAX_BYTES, 0); + + let circuit = + block_header_test_circuit::(CircuitBuilderStage::Mock, vec![input_bytes], params); + let instances = circuit.instances(); + MockProver::run(k, &circuit, instances).unwrap().assert_satisfied(); +} + +#[test] +pub fn test_one_mainnet_header_before_london_mock() { + let params = get_rlc_params("configs/tests/one_block.json"); + let k = params.base.k as u32; + let input_hex = "f90221a0b8b861952bca93c10bc7c38f9ef5c4e047beae539cfe46fa456c78893d916927a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940501b62d81a3f072f1d393d2f74013bab8d36d5ca01fd1d6a626d5d72d433b776c0c348f0cab03d13c68ba39ca4a6d6f109032de34a0418c7fdf567a5989a727ea0fe6054008ecf4953aaf56c28f7f197f6e443f05c0a05f79bcb9839eb480350b541377d04c5088fc4bab6952ed27cb94c70dd6736d73b9010081029040054830208119a218064a503c384490dc2014a414e3148820851856c05008e643a88a4a0002242e1a702d8a516244220a18cd0121a13a20882930000e471369c142ad4323475013088accb068824a002cc35021640860a448405a904001094c200a6081d0420feb02802c2e090a121403213d2640c100503510300364e43020f55943142815080595b145040045890021412545119b9002891cfe41011a704100ca97641210002a3b22c10f24853849048420100465c361880421593000021022c90800008800750e546464068cc40290108c48741899114af9c52801403da6800c02000c6ea270992068b45618c46f1254d7601d4411104e41d00a0787074abe0f14de3383765fdd837a121d8379cbd7845cda8ef39fde830203088f5061726974792d457468657265756d86312e33332e30826c69a09d41f9f64af4ebd672dec132507a12a4c85c1a514f47969dbd9c2b5e9d7d214e882b8a10229542325400000000000000000000"; + let mut input_bytes: Vec = Vec::from_hex(input_hex).unwrap(); + input_bytes.resize(MAINNET_BLOCK_HEADER_RLP_MAX_BYTES, 0); + + let circuit = + block_header_test_circuit::(CircuitBuilderStage::Mock, vec![input_bytes], params); + let instances = circuit.instances(); + MockProver::run(k, &circuit, instances).unwrap().assert_satisfied(); +} + +#[test] +pub fn test_one_mainnet_header_withdrawals_mock() { + let params = get_rlc_params("configs/tests/one_block.json"); + let k = params.base.k as u32; + let input_hex = "f90222a0d7519abd494a823b2c9c28908eaf250fe4a6287d747f1cc53a5a193b6533a549a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347944675c7e5baafbffbca748158becba61ef3b0a263a025000d51f040ee5c473fed74eda9ace87d55a35187b11bcde6f5176025c395bfa0a5800a6de6d28d7425ff72714af2af769b9f8f9e1baf56fb42f793fbb40fde07a056e1062a3dc63791e8a8496837606b14062da70ee69178cea97d6eeb5047550cb9010000236420014dc00423903000840002280080282100004704018340c0241c20011211400426000f900001d8088000011006020002ce98bc00c0000020c9a02040000688040200348c3a0082b81402002814922008085d008008200802802c4000130000101703124801400400018008a6108002020420144011200070020bc0202681810804221304004800088600300000040463614a000e200201c00611c0008e800b014081608010a0218a0b410010082000428209080200f50260a00840006700100f40a000000400000448301008c4a00341040e343500800d06250020010215200c008018002c88350404000bc5000a8000210c00724a0d0a4010210a448083eee2468401c9c3808343107884633899e780a07980d8d1f15474c9185e4d1cef5f207167735009daad2eb6af6da37ffba213c28800000000000000008501e08469e6a0f7519abd494a823b2c9c28908eaf250fe4a6287d747f1cc53a5a193b6533a549"; + let mut input_bytes: Vec = Vec::from_hex(input_hex).unwrap(); + input_bytes.resize(MAINNET_BLOCK_HEADER_RLP_MAX_BYTES, 0); + + let circuit = + block_header_test_circuit::(CircuitBuilderStage::Mock, vec![input_bytes], params); + let instances = circuit.instances(); + MockProver::run(k, &circuit, instances).unwrap().assert_satisfied(); +} + +#[test] +pub fn test_one_mainnet_header_fake_cancun_mock() { + let params = get_rlc_params("configs/tests/one_block.json"); + let k = params.base.k as u32; + let input_hex = "f90249a0d7519abd494a823b2c9c28908eaf250fe4a6287d747f1cc53a5a193b6533a549a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347944675c7e5baafbffbca748158becba61ef3b0a263a025000d51f040ee5c473fed74eda9ace87d55a35187b11bcde6f5176025c395bfa0a5800a6de6d28d7425ff72714af2af769b9f8f9e1baf56fb42f793fbb40fde07a056e1062a3dc63791e8a8496837606b14062da70ee69178cea97d6eeb5047550cb9010000236420014dc00423903000840002280080282100004704018340c0241c20011211400426000f900001d8088000011006020002ce98bc00c0000020c9a02040000688040200348c3a0082b81402002814922008085d008008200802802c4000130000101703124801400400018008a6108002020420144011200070020bc0202681810804221304004800088600300000040463614a000e200201c00611c0008e800b014081608010a0218a0b410010082000428209080200f50260a00840006700100f40a000000400000448301008c4a00341040e343500800d06250020010215200c008018002c88350404000bc5000a8000210c00724a0d0a4010210a448083eee2468401c9c3808343107884633899e780a07980d8d1f15474c9185e4d1cef5f207167735009daad2eb6af6da37ffba213c28800000000000000008501e08469e6a0f7519abd494a823b2c9c28908eaf250fe4a6287d747f1cc53a5a193b6533a549820123820456a07980d8d1f15474c9185e4d1cef5f207167735009daad2eb6af6da37ffba213c2"; + let mut input_bytes: Vec = Vec::from_hex(input_hex).unwrap(); + input_bytes.resize(MAINNET_BLOCK_HEADER_RLP_MAX_BYTES, 0); + + let circuit = + block_header_test_circuit::(CircuitBuilderStage::Mock, vec![input_bytes], params); + let instances = circuit.instances(); + MockProver::run(k, &circuit, instances).unwrap().assert_satisfied(); +} + +#[test] +pub fn test_one_mainnet_header_prover() -> Result<(), Box> { + let config_params = get_rlc_params("configs/tests/one_block.json"); + let k = config_params.base.k as u32; + let input_hex = "f90222a0d7519abd494a823b2c9c28908eaf250fe4a6287d747f1cc53a5a193b6533a549a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347944675c7e5baafbffbca748158becba61ef3b0a263a025000d51f040ee5c473fed74eda9ace87d55a35187b11bcde6f5176025c395bfa0a5800a6de6d28d7425ff72714af2af769b9f8f9e1baf56fb42f793fbb40fde07a056e1062a3dc63791e8a8496837606b14062da70ee69178cea97d6eeb5047550cb9010000236420014dc00423903000840002280080282100004704018340c0241c20011211400426000f900001d8088000011006020002ce98bc00c0000020c9a02040000688040200348c3a0082b81402002814922008085d008008200802802c4000130000101703124801400400018008a6108002020420144011200070020bc0202681810804221304004800088600300000040463614a000e200201c00611c0008e800b014081608010a0218a0b410010082000428209080200f50260a00840006700100f40a000000400000448301008c4a00341040e343500800d06250020010215200c008018002c88350404000bc5000a8000210c00724a0d0a4010210a448083eee2468401c9c3808343107884633899e780a07980d8d1f15474c9185e4d1cef5f207167735009daad2eb6af6da37ffba213c28800000000000000008501e08469e6a0f7519abd494a823b2c9c28908eaf250fe4a6287d747f1cc53a5a193b6533a549"; + let mut input_bytes: Vec = Vec::from_hex(input_hex).unwrap(); + input_bytes.resize(MAINNET_BLOCK_HEADER_RLP_MAX_BYTES, 0); + + let mut rng = OsRng; + let params = ParamsKZG::::setup(k, &mut rng); + let circuit = block_header_test_circuit::( + CircuitBuilderStage::Keygen, + vec![input_bytes.clone()], + config_params, + ); + //circuit.config(k as usize, Some(109)); + let vk_time = start_timer!(|| "vk gen"); + let vk = keygen_vk(¶ms, &circuit).unwrap(); + end_timer!(vk_time); + let pk_time = start_timer!(|| "pk gen"); + let pk = keygen_pk(¶ms, vk, &circuit).unwrap(); + end_timer!(pk_time); + let break_points = circuit.break_points(); + let config_params = circuit.params().rlc; + + let pf_time = start_timer!(|| "proof gen"); + let circuit = block_header_test_circuit::( + CircuitBuilderStage::Prover, + vec![input_bytes], + config_params, + ) + .use_break_points(break_points); + let instances = circuit.instances(); + assert_eq!(instances.len(), 1); + let proof = gen_proof_with_instances(¶ms, &pk, circuit, &[&instances[0]]); + end_timer!(pf_time); + + let verify_time = start_timer!(|| "verify"); + check_proof_with_instances(¶ms, pk.get_vk(), &proof, &[&instances[0]], true); + end_timer!(verify_time); + + Ok(()) +} diff --git a/axiom-eth/src/block_header/tests/chain_instance.rs b/axiom-eth/src/block_header/tests/chain_instance.rs deleted file mode 100644 index 0b00dc7b..00000000 --- a/axiom-eth/src/block_header/tests/chain_instance.rs +++ /dev/null @@ -1,83 +0,0 @@ -use super::{ - util::{bytes_be_to_u128, encode_h256_to_field, EthConfigParams}, - Field, Network, -}; -use crate::{ - keccak::{FixedLenRLCs, FnSynthesize, KeccakChip, VarLenRLCs}, - rlp::{ - builder::{RlcThreadBreakPoints, RlcThreadBuilder}, - rlc::{RlcContextPair, RlcFixedTrace, RlcTrace, FIRST_PHASE, RLC_PHASE}, - RlpArrayTraceWitness, RlpChip, RlpFieldTrace, RlpFieldWitness, - }, - util::{bytes_be_var_to_fixed, decode_field_to_h256}, - EthChip, EthCircuitBuilder, ETH_LOOKUP_BITS, -}; -use core::{ - iter::{self, once}, - marker::PhantomData, -}; -use ethers_core::types::H256; -#[cfg(feature = "providers")] -use ethers_providers::{Http, Provider}; -use halo2_base::{ - gates::{builder::GateThreadBuilder, GateInstructions, RangeChip}, - utils::bit_length, - AssignedValue, Context, - QuantumCell::{Constant, Existing}, -}; -use itertools::Itertools; -use rayon::prelude::*; -use serde::{Deserialize, Serialize}; -use std::{cell::RefCell, env::var}; - -/* // No longer used, For testing only -#[derive(Serialize, Deserialize, Debug, Clone)] -struct EthBlockHeaderChainInstance { - pub prev_hash: H256, - pub end_hash: H256, - pub start_block_number: u32, - pub end_block_number: u32, - merkle_mountain_range: Vec, -} - -impl EthBlockHeaderChainInstance { - pub fn new( - prev_hash: H256, - end_hash: H256, - start_block_number: u32, - end_block_number: u32, - merkle_mountain_range: Vec, - ) -> Self { - Self { prev_hash, end_hash, start_block_number, end_block_number, merkle_mountain_range } - } - fn to_instance(&self) -> Vec { - // * prevHash: uint256 represented as 2 uint128s - // * endHash: uint256 represented as 2 uint128s - // * startBlockNumber || endBlockNumber: 0..0 || uint32 || 0..0 || uint32 as u64 (exactly 64 bits) - // * merkleRoots: Vec, each represented as 2 uint128s - let [prev_hash, end_hash] = - [&self.prev_hash, &self.end_hash].map(|hash| encode_h256_to_field::(hash)); - let block_numbers = - F::from(((self.start_block_number as u64) << 32) + (self.end_block_number as u64)); - let merkle_mountain_range = self - .merkle_mountain_range - .iter() - .flat_map(|hash| encode_h256_to_field::(hash)) - .collect_vec(); - - [&prev_hash[..], &end_hash[..], &[block_numbers], &merkle_mountain_range].concat() - } - - fn from_instance(instance: &[F]) -> Self { - let prev_hash = decode_field_to_h256(&instance[0..2]); - let end_hash = decode_field_to_h256(&instance[2..4]); - let block_numbers = instance[4].to_repr(); // little endian - let start_block_number = u32::from_le_bytes(block_numbers[4..8].try_into().unwrap()); - let end_block_number = u32::from_le_bytes(block_numbers[..4].try_into().unwrap()); - let merkle_mountain_range = - instance[5..].chunks(2).map(|chunk| decode_field_to_h256(chunk)).collect_vec(); - - Self::new(prev_hash, end_hash, start_block_number, end_block_number, merkle_mountain_range) - } -} -*/ diff --git a/axiom-eth/src/block_header/tests/mod.rs b/axiom-eth/src/block_header/tests/mod.rs deleted file mode 100644 index be09d16a..00000000 --- a/axiom-eth/src/block_header/tests/mod.rs +++ /dev/null @@ -1,356 +0,0 @@ -use crate::{ - keccak::SharedKeccakChip, - util::{EthConfigParams, EthConfigPinning, Halo2ConfigPinning}, -}; - -use super::*; -use ark_std::{end_timer, start_timer}; -use halo2_base::{ - halo2_proofs::{ - dev::MockProver, - halo2curves::bn256::{Bn256, Fr, G1Affine}, - plonk::*, - poly::commitment::ParamsProver, - poly::kzg::{ - commitment::{KZGCommitmentScheme, ParamsKZG}, - multiopen::{ProverSHPLONK, VerifierSHPLONK}, - strategy::SingleStrategy, - }, - transcript::{ - Blake2bRead, Blake2bWrite, Challenge255, TranscriptReadBuffer, TranscriptWriterBuffer, - }, - }, - utils::fs::gen_srs, -}; -use hex::FromHex; -use rand_core::OsRng; -use std::{ - env::{set_var, var}, - fs::File, - marker::PhantomData, -}; -use test_log::test; - -fn block_header_test_circuit( - mut builder: RlcThreadBuilder, - inputs: Vec>, - network: Network, - break_points: Option, -) -> EthCircuitBuilder> { - let prover = builder.witness_gen_only(); - let range = RangeChip::default(ETH_LOOKUP_BITS); - let keccak = SharedKeccakChip::default(); - let chip = EthChip::new(RlpChip::new(&range, None), None); - let chain_witness = chip.decompose_block_header_chain_phase0( - &mut builder.gate_builder, - &mut keccak.borrow_mut(), - inputs, - network, - ); - - let circuit = EthCircuitBuilder::new( - vec![], - builder, - keccak, - range, - break_points, - move |builder: &mut RlcThreadBuilder, - rlp: RlpChip, - keccak_rlcs: (FixedLenRLCs, VarLenRLCs)| { - let chip = EthChip::new(rlp, Some(keccak_rlcs)); - let _block_chain_trace = - chip.decompose_block_header_chain_phase1(builder, chain_witness, None); - }, - ); - if !prover { - let config_params: EthConfigParams = serde_json::from_str( - var("ETH_CONFIG_PARAMS").expect("ETH_CONFIG_PARAMS is not set").as_str(), - ) - .unwrap(); - circuit.config(config_params.degree as usize, Some(config_params.unusable_rows)); - } - circuit -} - -#[test] -pub fn test_one_mainnet_header_mock() { - let params = EthConfigPinning::from_path("configs/tests/one_block.json").params; - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(¶ms).unwrap()); - let k = params.degree; - let input_hex = "f90201a0d7519abd494a823b2c9c28908eaf250fe4a6287d747f1cc53a5a193b6533a549a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347944675c7e5baafbffbca748158becba61ef3b0a263a025000d51f040ee5c473fed74eda9ace87d55a35187b11bcde6f5176025c395bfa0a5800a6de6d28d7425ff72714af2af769b9f8f9e1baf56fb42f793fbb40fde07a056e1062a3dc63791e8a8496837606b14062da70ee69178cea97d6eeb5047550cb9010000236420014dc00423903000840002280080282100004704018340c0241c20011211400426000f900001d8088000011006020002ce98bc00c0000020c9a02040000688040200348c3a0082b81402002814922008085d008008200802802c4000130000101703124801400400018008a6108002020420144011200070020bc0202681810804221304004800088600300000040463614a000e200201c00611c0008e800b014081608010a0218a0b410010082000428209080200f50260a00840006700100f40a000000400000448301008c4a00341040e343500800d06250020010215200c008018002c88350404000bc5000a8000210c00724a0d0a4010210a448083eee2468401c9c3808343107884633899e780a07980d8d1f15474c9185e4d1cef5f207167735009daad2eb6af6da37ffba213c28800000000000000008501e08469e60000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - let mut input_bytes: Vec = Vec::from_hex(input_hex).unwrap(); - input_bytes.resize(MAINNET_BLOCK_HEADER_RLP_MAX_BYTES, 0); - - let circuit = block_header_test_circuit::( - RlcThreadBuilder::mock(), - vec![input_bytes], - Network::Mainnet, - None, - ); - MockProver::run(k, &circuit, vec![vec![]]).unwrap().assert_satisfied(); -} - -#[test] -pub fn test_one_mainnet_header_before_london_mock() { - let params = EthConfigPinning::from_path("configs/tests/one_block.json").params; - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(¶ms).unwrap()); - let k = params.degree; - let input_hex = "f90221a0b8b861952bca93c10bc7c38f9ef5c4e047beae539cfe46fa456c78893d916927a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940501b62d81a3f072f1d393d2f74013bab8d36d5ca01fd1d6a626d5d72d433b776c0c348f0cab03d13c68ba39ca4a6d6f109032de34a0418c7fdf567a5989a727ea0fe6054008ecf4953aaf56c28f7f197f6e443f05c0a05f79bcb9839eb480350b541377d04c5088fc4bab6952ed27cb94c70dd6736d73b9010081029040054830208119a218064a503c384490dc2014a414e3148820851856c05008e643a88a4a0002242e1a702d8a516244220a18cd0121a13a20882930000e471369c142ad4323475013088accb068824a002cc35021640860a448405a904001094c200a6081d0420feb02802c2e090a121403213d2640c100503510300364e43020f55943142815080595b145040045890021412545119b9002891cfe41011a704100ca97641210002a3b22c10f24853849048420100465c361880421593000021022c90800008800750e546464068cc40290108c48741899114af9c52801403da6800c02000c6ea270992068b45618c46f1254d7601d4411104e41d00a0787074abe0f14de3383765fdd837a121d8379cbd7845cda8ef39fde830203088f5061726974792d457468657265756d86312e33332e30826c69a09d41f9f64af4ebd672dec132507a12a4c85c1a514f47969dbd9c2b5e9d7d214e882b8a10229542325400000000000000000000"; - let mut input_bytes: Vec = Vec::from_hex(input_hex).unwrap(); - input_bytes.resize(MAINNET_BLOCK_HEADER_RLP_MAX_BYTES, 0); - - let circuit = block_header_test_circuit::( - RlcThreadBuilder::mock(), - vec![input_bytes], - Network::Mainnet, - None, - ); - MockProver::run(k, &circuit, vec![vec![]]).unwrap().assert_satisfied(); -} - -#[test] -pub fn test_one_mainnet_header_withdrawals_mock() { - let params = EthConfigPinning::from_path("configs/tests/one_block.json").params; - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(¶ms).unwrap()); - let k = params.degree; - let input_hex = "f90222a0d7519abd494a823b2c9c28908eaf250fe4a6287d747f1cc53a5a193b6533a549a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347944675c7e5baafbffbca748158becba61ef3b0a263a025000d51f040ee5c473fed74eda9ace87d55a35187b11bcde6f5176025c395bfa0a5800a6de6d28d7425ff72714af2af769b9f8f9e1baf56fb42f793fbb40fde07a056e1062a3dc63791e8a8496837606b14062da70ee69178cea97d6eeb5047550cb9010000236420014dc00423903000840002280080282100004704018340c0241c20011211400426000f900001d8088000011006020002ce98bc00c0000020c9a02040000688040200348c3a0082b81402002814922008085d008008200802802c4000130000101703124801400400018008a6108002020420144011200070020bc0202681810804221304004800088600300000040463614a000e200201c00611c0008e800b014081608010a0218a0b410010082000428209080200f50260a00840006700100f40a000000400000448301008c4a00341040e343500800d06250020010215200c008018002c88350404000bc5000a8000210c00724a0d0a4010210a448083eee2468401c9c3808343107884633899e780a07980d8d1f15474c9185e4d1cef5f207167735009daad2eb6af6da37ffba213c28800000000000000008501e08469e6a0f7519abd494a823b2c9c28908eaf250fe4a6287d747f1cc53a5a193b6533a549"; - let mut input_bytes: Vec = Vec::from_hex(input_hex).unwrap(); - input_bytes.resize(MAINNET_BLOCK_HEADER_RLP_MAX_BYTES, 0); - - let circuit = block_header_test_circuit::( - RlcThreadBuilder::mock(), - vec![input_bytes], - Network::Mainnet, - None, - ); - MockProver::run(k, &circuit, vec![vec![]]).unwrap().assert_satisfied(); -} - -#[test] -pub fn test_one_mainnet_header_prover() -> Result<(), Box> { - let params = EthConfigPinning::from_path("configs/tests/one_block.json").params; - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(¶ms).unwrap()); - let k = params.degree; - let input_hex = "f90222a0d7519abd494a823b2c9c28908eaf250fe4a6287d747f1cc53a5a193b6533a549a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347944675c7e5baafbffbca748158becba61ef3b0a263a025000d51f040ee5c473fed74eda9ace87d55a35187b11bcde6f5176025c395bfa0a5800a6de6d28d7425ff72714af2af769b9f8f9e1baf56fb42f793fbb40fde07a056e1062a3dc63791e8a8496837606b14062da70ee69178cea97d6eeb5047550cb9010000236420014dc00423903000840002280080282100004704018340c0241c20011211400426000f900001d8088000011006020002ce98bc00c0000020c9a02040000688040200348c3a0082b81402002814922008085d008008200802802c4000130000101703124801400400018008a6108002020420144011200070020bc0202681810804221304004800088600300000040463614a000e200201c00611c0008e800b014081608010a0218a0b410010082000428209080200f50260a00840006700100f40a000000400000448301008c4a00341040e343500800d06250020010215200c008018002c88350404000bc5000a8000210c00724a0d0a4010210a448083eee2468401c9c3808343107884633899e780a07980d8d1f15474c9185e4d1cef5f207167735009daad2eb6af6da37ffba213c28800000000000000008501e08469e6a0f7519abd494a823b2c9c28908eaf250fe4a6287d747f1cc53a5a193b6533a549"; - let mut input_bytes: Vec = Vec::from_hex(input_hex).unwrap(); - input_bytes.resize(MAINNET_BLOCK_HEADER_RLP_MAX_BYTES, 0); - - let mut rng = OsRng; - let params = ParamsKZG::::setup(k, &mut rng); - let circuit = block_header_test_circuit::( - RlcThreadBuilder::keygen(), - vec![input_bytes.clone()], - Network::Mainnet, - None, - ); - let vk_time = start_timer!(|| "vk gen"); - let vk = keygen_vk(¶ms, &circuit).unwrap(); - end_timer!(vk_time); - let pk_time = start_timer!(|| "pk gen"); - let pk = keygen_pk(¶ms, vk, &circuit).unwrap(); - end_timer!(pk_time); - let break_points = circuit.circuit.break_points.take(); - let pinning = EthConfigPinning { - params: serde_json::from_str(var("ETH_CONFIG_PARAMS").unwrap().as_str()).unwrap(), - break_points, - }; - serde_json::to_writer(File::create("configs/tests/one_block.json").unwrap(), &pinning)?; - - let mut transcript = Blake2bWrite::<_, _, Challenge255<_>>::init(vec![]); - let pf_time = start_timer!(|| "proof gen"); - let break_points = pinning.break_points(); - let circuit = block_header_test_circuit::( - RlcThreadBuilder::prover(), - vec![input_bytes], - Network::Mainnet, - Some(break_points), - ); - create_proof::< - KZGCommitmentScheme, - ProverSHPLONK<'_, Bn256>, - Challenge255, - _, - Blake2bWrite, G1Affine, Challenge255>, - _, - >(¶ms, &pk, &[circuit], &[&[&[]]], rng, &mut transcript)?; - let proof = transcript.finalize(); - end_timer!(pf_time); - - let verifier_params = params.verifier_params(); - let strategy = SingleStrategy::new(¶ms); - let mut transcript = Blake2bRead::<_, _, Challenge255<_>>::init(&proof[..]); - let verify_time = start_timer!(|| "verify"); - verify_proof::< - KZGCommitmentScheme, - VerifierSHPLONK<'_, Bn256>, - Challenge255, - Blake2bRead<&[u8], G1Affine, Challenge255>, - SingleStrategy<'_, Bn256>, - >(verifier_params, pk.get_vk(), strategy, &[&[&[]]], &mut transcript) - .unwrap(); - end_timer!(verify_time); - - Ok(()) -} - -fn get_default_goerli_header_chain_circuit() -> EthBlockHeaderChainCircuit { - let network = Network::Goerli; - let header_rlp_max_bytes = GOERLI_BLOCK_HEADER_RLP_MAX_BYTES; - let blocks: Vec = - serde_json::from_reader(File::open("data/headers/default_blocks_goerli.json").unwrap()) - .unwrap(); - let mut input_bytes = Vec::new(); - let max_depth = 3; - for block_str in blocks.iter() { - let mut block_vec: Vec = Vec::from_hex(block_str).unwrap(); - block_vec.resize(header_rlp_max_bytes, 0); - input_bytes.push(block_vec); - } - let dummy_header_rlp = input_bytes[0].clone(); - input_bytes.extend(iter::repeat(dummy_header_rlp).take((1 << max_depth) - input_bytes.len())); - - EthBlockHeaderChainCircuit { - header_rlp_encodings: input_bytes, - num_blocks: 7, - max_depth, - network, - _marker: PhantomData, - } -} - -#[test] -pub fn test_multi_goerli_header_mock() { - let config = EthConfigPinning::from_path("configs/tests/multi_block.json").params; - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(&config).unwrap()); - let k = config.degree; - - let input = get_default_goerli_header_chain_circuit(); - let circuit = input.create_circuit(RlcThreadBuilder::mock(), None); - let instance = circuit.instance(); - - MockProver::run(k, &circuit, vec![instance]).unwrap().assert_satisfied(); -} - -#[test] -pub fn test_multi_goerli_header_prover() { - let config = EthConfigPinning::from_path("configs/tests/multi_block.json").params; - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(&config).unwrap()); - let k = config.degree; - let input = get_default_goerli_header_chain_circuit(); - let circuit = input.clone().create_circuit(RlcThreadBuilder::keygen(), None); - - let params = gen_srs(k); - - let vk_time = start_timer!(|| "vk gen"); - let vk = keygen_vk(¶ms, &circuit).unwrap(); - end_timer!(vk_time); - let pk_time = start_timer!(|| "pk gen"); - let pk = keygen_pk(¶ms, vk, &circuit).unwrap(); - end_timer!(pk_time); - let break_points = circuit.circuit.break_points.take(); - let pinning = EthConfigPinning { - params: serde_json::from_str(var("ETH_CONFIG_PARAMS").unwrap().as_str()).unwrap(), - break_points, - }; - serde_json::to_writer(File::create("configs/tests/multi_block.json").unwrap(), &pinning) - .unwrap(); - - let mut transcript = Blake2bWrite::<_, _, Challenge255<_>>::init(vec![]); - let pf_time = start_timer!(|| "proof gen"); - let break_points = pinning.break_points(); - let circuit = input.create_circuit(RlcThreadBuilder::prover(), Some(break_points)); - let instance = circuit.instance(); - create_proof::< - KZGCommitmentScheme, - ProverSHPLONK<'_, Bn256>, - Challenge255, - _, - Blake2bWrite, G1Affine, Challenge255>, - _, - >(¶ms, &pk, &[circuit], &[&[&instance]], OsRng, &mut transcript) - .unwrap(); - let proof = transcript.finalize(); - end_timer!(pf_time); - - let verifier_params = params.verifier_params(); - let strategy = SingleStrategy::new(¶ms); - let mut transcript = Blake2bRead::<_, _, Challenge255<_>>::init(&proof[..]); - let verify_time = start_timer!(|| "verify"); - verify_proof::< - KZGCommitmentScheme, - VerifierSHPLONK<'_, Bn256>, - Challenge255, - Blake2bRead<&[u8], G1Affine, Challenge255>, - SingleStrategy<'_, Bn256>, - >(verifier_params, pk.get_vk(), strategy, &[&[&instance]], &mut transcript) - .unwrap(); - end_timer!(verify_time); -} - -#[cfg(all(feature = "aggregation", feature = "providers"))] -mod aggregation { - use std::path::PathBuf; - - use super::test; - use super::*; - use crate::{ - block_header::helpers::{BlockHeaderScheduler, CircuitType, Finality, Task}, - util::scheduler::Scheduler, - }; - - fn test_scheduler(network: Network) -> BlockHeaderScheduler { - BlockHeaderScheduler::new( - network, - false, - false, - PathBuf::from("configs/headers"), - PathBuf::from("data/headers"), - ) - } - - #[test] - fn test_goerli_header_chain_provider() { - let scheduler = test_scheduler(Network::Goerli); - scheduler.get_snark(Task::new( - 0x765fb3, - 0x765fb3 + 7, - CircuitType::new(3, 3, Finality::None, Network::Goerli), - )); - } - - #[test] - #[ignore = "requires over 32G memory"] - fn test_goerli_header_chain_with_aggregation() { - let scheduler = test_scheduler(Network::Goerli); - scheduler.get_snark(Task::new( - 0x765fb3, - 0x765fb3 + 11, - CircuitType::new(4, 3, Finality::None, Network::Goerli), - )); - } - - #[test] - #[ignore = "requires over 32G memory"] - fn test_goerli_header_chain_final_aggregation() { - let scheduler = test_scheduler(Network::Goerli); - scheduler.get_snark(Task::new( - 0x765fb3, - 0x765fb3 + 9, - CircuitType::new(4, 3, Finality::Merkle, Network::Goerli), - )); - } - - #[cfg(feature = "evm")] - #[test] - fn test_goerli_header_chain_for_evm() { - let scheduler = test_scheduler(Network::Goerli); - scheduler.get_calldata( - Task::new( - 0x765fb3, - 0x765fb3 + 11, - CircuitType::new(4, 3, Finality::Evm(1), Network::Goerli), - ), - true, - ); - } -} diff --git a/axiom-eth/src/keccak/README.md b/axiom-eth/src/keccak/README.md new file mode 100644 index 00000000..0df68831 --- /dev/null +++ b/axiom-eth/src/keccak/README.md @@ -0,0 +1,12 @@ +# Keccak Chip + +This module provides adapters and trait implementations to enable the [`KeccakComponentShardCircuit`](https://github.com/axiom-crypto/halo2-lib/tree/main/hashes/zkevm/src/keccak) to fall under the [Component Framework](../utils/README.md). + +The keccak component circuit outputs a virtual table of `(key, value)` pairs where `key` is of type `Vec` and `value` is of type `H256`. We represent the `H256` in hi-lo form as two field elements. The complication is in the `key`, which is a variable length byte array of possibly arbitrary length. +To handle this in ZK, we must instead encode `(key,value)` as a different virtual table with fixed length keys but where a single logical `(key,value)` can take up multiple rows in the new fixed length key table. + +For keccak, the format of the fixed length key table is specified in [zkEVM hashes](https://github.com/axiom-crypto/halo2-lib/tree/main/hashes/zkevm/src/keccak). +What is provided in the [promise](./promise.rs) submodule is a way to perform promise calls into the keccak component circuit. +Promise calls are done as follows: the caller circuit loaded the fixed length key virtual table as private witnesses and computes the commitment to the table to exactly match the output commitment computation of the keccak component circuit. +Then it creates a raw Halo2 table where it RLCs the entries of the fixed length key table in a way that encodes the variable lengths. +Finally for each keccak promise call, the variable length input bytes are first packed into field elements in a way that matches the packing done in the virtual table. Then the packed field elements are RLCed together with the variable length. This RLC value is dynamically looked up against the raw Halo2 table to verify the promise call. diff --git a/axiom-eth/src/keccak/builder.rs b/axiom-eth/src/keccak/builder.rs deleted file mode 100644 index 3c102977..00000000 --- a/axiom-eth/src/keccak/builder.rs +++ /dev/null @@ -1,299 +0,0 @@ -use halo2_base::gates::builder::GateThreadBuilder; - -use crate::rlp::rlc::FIRST_PHASE; - -use super::*; - -/// We need a more custom synthesize function to work with the outputs of keccak RLCs. -pub trait FnSynthesize = - FnOnce(&mut RlcThreadBuilder, RlpChip, (FixedLenRLCs, VarLenRLCs)) + Clone; - -pub struct KeccakCircuitBuilder -where - FnPhase1: FnSynthesize, -{ - pub builder: RefCell>, - pub break_points: RefCell, - pub synthesize_phase1: RefCell>, - pub keccak: SharedKeccakChip, - pub range: RangeChip, -} - -impl KeccakCircuitBuilder -where - FnPhase1: FnSynthesize, -{ - pub fn new( - builder: RlcThreadBuilder, - keccak: SharedKeccakChip, - range: RangeChip, - break_points: Option, - synthesize_phase1: FnPhase1, - ) -> Self { - Self { - builder: RefCell::new(builder), - break_points: RefCell::new(break_points.unwrap_or_default()), - synthesize_phase1: RefCell::new(Some(synthesize_phase1)), - keccak, - range, - } - } - - /// Does a dry run of multi-phase synthesize to calculate optimal configuration parameters - /// - /// Beware: the `KECCAK_ROWS` is calculated based on the `minimum_rows = UNUSABLE_ROWS`, - /// however at configuration time the `minimum_rows` will depend on `KECCAK_ROWS`. - /// If you then reset `minimum_rows` to this smaller number, it might auto-configure - /// to a higher `KECCAK_ROWS`, which now requires higher `minimum_rows`... - pub fn config(&self, k: usize, minimum_rows: Option) -> EthConfigParams { - // clone everything so we don't alter the circuit in any way for later calls - let mut builder = self.builder.borrow().clone(); - let mut keccak = self.keccak.borrow().clone(); - let optimal_rows_per_round = - rows_per_round((1 << k) - minimum_rows.unwrap_or(0), keccak.capacity()); - // we don't want to actually call `keccak.assign_phase{0,1}` so we fake the output - let table_input_rlcs = vec![ - AssignedValue { - value: Assigned::Trivial(F::zero()), - cell: Some(ContextCell { context_id: KECCAK_CONTEXT_ID, offset: 0 }) - }; - keccak.capacity() - ]; - let table_output_rlcs = table_input_rlcs.clone(); - let rlc_chip = RlcChip::new(F::zero()); - let rlp_chip = RlpChip::new(&self.range, Some(&rlc_chip)); - let keccak_rlcs = keccak.process_phase1( - &mut builder, - &rlc_chip, - &self.range, - table_input_rlcs, - table_output_rlcs, - ); - let f = self.synthesize_phase1.borrow().clone().expect("synthesize_phase1 should exist"); - f(&mut builder, rlp_chip, keccak_rlcs); - let mut params = builder.config(k, minimum_rows); - params.keccak_rows_per_round = std::cmp::min(optimal_rows_per_round, 50); // empirically more than 50 rows per round makes the rotation offsets too large - self.keccak.borrow_mut().num_rows_per_round = params.keccak_rows_per_round; - #[cfg(feature = "display")] - log::info!("KeccakCircuitBuilder auto-calculated config params: {:#?}", params); - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(¶ms).unwrap()); - set_var("KECCAK_DEGREE", k.to_string()); - set_var("KECCAK_ROWS", params.keccak_rows_per_round.to_string()); - - params - } - - // re-usable function for synthesize - pub fn two_phase_synthesize( - &self, - config: &MPTConfig, - layouter: &mut impl Layouter, - ) -> HashMap<(usize, usize), (circuit::Cell, usize)> { - config.rlp.range.load_lookup_table(layouter).expect("load range lookup table"); - config.keccak.load_aux_tables(layouter).expect("load keccak lookup tables"); - - let mut first_pass = SKIP_FIRST_PASS; - let witness_gen_only = self.builder.borrow().witness_gen_only(); - - let mut gamma = None; - if !witness_gen_only { - // in these cases, synthesize is called twice, and challenge can be gotten after the first time, or we use dummy value 0 - layouter.get_challenge(config.rlp.rlc.gamma).map(|gamma_| gamma = Some(gamma_)); - } - let mut assigned_advices = HashMap::new(); - - layouter - .assign_region( - || "KeccakCircuitBuilder generated circuit", - |mut region| { - if first_pass { - first_pass = false; - return Ok(()); - } - if !witness_gen_only { - let start = std::time::Instant::now(); - let mut builder = self.builder.borrow().clone(); - let mut keccak = self.keccak.borrow().clone(); - let f = self - .synthesize_phase1 - .borrow() - .clone() - .expect("synthesize_phase1 should exist"); - // tells zkevm keccak to assign its cells - let squeeze_digests = keccak.assign_phase0(&mut region, &config.keccak); - println!("{:?}", start.elapsed()); - // end of FirstPhase - let rlc_chip = RlcChip::new(gamma.unwrap_or_else(|| F::zero())); - // SecondPhase: tell zkevm keccak to assign RLC cells and upload them into `assigned_advices` - let (keccak_advices, table_input_rlcs, table_output_rlcs) = keccak - .assign_phase1( - &mut region, - &config.keccak.keccak_table, - squeeze_digests, - *rlc_chip.gamma(), - Some(HashMap::new()), - ); - // Constrain RLCs so keccak chip witnesses are actually correct - let keccak_rlcs = keccak.process_phase1( - &mut builder, - &rlc_chip, - &self.range, - table_input_rlcs, - table_output_rlcs, - ); - // Do any custom synthesize functions in SecondPhase - let mut assignments = KeygenAssignments { - assigned_advices: keccak_advices, - ..Default::default() - }; - let rlp_chip = RlpChip::new(&self.range, Some(&rlc_chip)); - f(&mut builder, rlp_chip, keccak_rlcs); - assignments = builder.assign_all( - &config.rlp.range.gate, - &config.rlp.range.lookup_advice, - &config.rlp.range.q_lookup, - &config.rlp.rlc, - &mut region, - assignments, - ); - *self.break_points.borrow_mut() = assignments.break_points; - assigned_advices = assignments.assigned_advices; - } else { - let builder = &mut self.builder.borrow_mut(); - let break_points = &mut self.break_points.borrow_mut(); - assign_prover_phase0( - &mut region, - &config.rlp.range.gate, - &config.rlp.range.lookup_advice, - builder, - break_points, - ); - let squeeze_digests = - self.keccak.borrow().assign_phase0(&mut region, &config.keccak); - // == END OF FIRST PHASE == - // this is a special backend API function (in halo2-axiom only) that computes the KZG commitments for all columns in FirstPhase and performs Fiat-Shamir on them to return the challenge value - #[cfg(feature = "halo2-axiom")] - region.next_phase(); - // == BEGIN SECOND PHASE == - // get challenge value - let mut gamma = None; - #[cfg(feature = "halo2-axiom")] - region.get_challenge(config.rlp.rlc.gamma).map(|gamma_| { - log::info!("gamma: {gamma_:?}"); - gamma = Some(gamma_); - }); - let rlc_chip = - RlcChip::new(gamma.expect("Could not get challenge in second phase")); - let (_, table_input_rlcs, table_output_rlcs) = - self.keccak.borrow().assign_phase1( - &mut region, - &config.keccak.keccak_table, - squeeze_digests, - *rlc_chip.gamma(), - None, - ); - // Constrain RLCs so keccak chip witnesses are actually correct - let keccak_rlcs = self.keccak.borrow_mut().process_phase1( - builder, - &rlc_chip, - &self.range, - table_input_rlcs, - table_output_rlcs, - ); - let f = RefCell::take(&self.synthesize_phase1) - .expect("synthesize_phase1 should exist"); // we `take` the closure during proving to avoid cloning captured variables (the captured variables would be the AssignedValue payload sent from FirstPhase to SecondPhase) - assign_prover_phase1( - &mut region, - &config.rlp.range.gate, - &config.rlp.range.lookup_advice, - &config.rlp.rlc, - &rlc_chip, - builder, - break_points, - |builder: &mut RlcThreadBuilder, rlc_chip: &RlcChip| { - let rlp_chip = RlpChip::new(&self.range, Some(rlc_chip)); - f(builder, rlp_chip, keccak_rlcs); - }, - ); - } - Ok(()) - }, - ) - .unwrap(); - assigned_advices - } -} - -impl> Circuit for KeccakCircuitBuilder { - type Config = MPTConfig; - type FloorPlanner = SimpleFloorPlanner; - - fn without_witnesses(&self) -> Self { - unimplemented!() - } - - fn configure(meta: &mut ConstraintSystem) -> MPTConfig { - let params: EthConfigParams = - serde_json::from_str(&std::env::var("ETH_CONFIG_PARAMS").unwrap()).unwrap(); - MPTConfig::configure(meta, params) - } - - fn synthesize( - &self, - config: Self::Config, - mut layouter: impl Layouter, - ) -> Result<(), Error> { - self.two_phase_synthesize(&config, &mut layouter); - Ok(()) - } -} - -// The following is a hack to allow parallelization of KeccakChip. Should refactor more generally in future. - -/// Trait for structs that contain references to keccak query indices. -/// Due to current [`KeccakChip`] implementation, these indices must be shifted -/// if multiple structs are created in parallel, to avoid race conditions on the circuit. -pub trait ContainsParallelizableKeccakQueries { - /// Shifts all fixed (resp. variable) length keccak query indices by `fixed_shift` (resp. `var_shift`). - /// This is necessary when joining parallel (multi-threaded) keccak chips into one. - fn shift_query_indices(&mut self, fixed_shift: usize, var_shift: usize); -} - -/// Utility function to parallelize an operation involving KeccakChip that outputs something referencing -/// keccak query indices. This should be done in FirstPhase. -/// -/// When `R` stores keccak queries by index, in a parallel setting, we need to shift -/// the indices at the end. -pub fn parallelize_keccak_phase0( - thread_pool: &mut GateThreadBuilder, - keccak: &mut KeccakChip, - input: Vec, - f: FR, -) -> Vec -where - F: Field, - T: Send, - R: ContainsParallelizableKeccakQueries + Send, - FR: Fn(&mut Context, &mut KeccakChip, T) -> R + Send + Sync, -{ - let witness_gen_only = thread_pool.witness_gen_only(); - let ctx_ids = input.iter().map(|_| thread_pool.get_new_thread_id()).collect_vec(); - let (mut trace, ctxs): (Vec<_>, Vec<_>) = input - .into_par_iter() - .zip(ctx_ids.into_par_iter()) - .map(|(input, ctx_id)| { - let mut ctx = Context::new(witness_gen_only, ctx_id); - let mut keccak = KeccakChip::default(); - let trace = f(&mut ctx, &mut keccak, input); - (trace, (ctx, keccak)) - }) - .unzip(); - // join gate contexts and keccak queries; need to shift keccak query indices because of the join - for (trace, (ctx, mut _keccak)) in trace.iter_mut().zip(ctxs.into_iter()) { - thread_pool.threads[FIRST_PHASE].push(ctx); - trace.shift_query_indices(keccak.fixed_len_queries.len(), keccak.var_len_queries.len()); - keccak.fixed_len_queries.append(&mut _keccak.fixed_len_queries); - keccak.var_len_queries.append(&mut _keccak.var_len_queries); - } - trace -} diff --git a/axiom-eth/src/keccak/component_shim.rs b/axiom-eth/src/keccak/component_shim.rs new file mode 100644 index 00000000..7c5121ea --- /dev/null +++ b/axiom-eth/src/keccak/component_shim.rs @@ -0,0 +1,85 @@ +use std::{any::Any, collections::HashMap, iter}; + +use anyhow::{anyhow, bail, Result}; +use ethers_core::{types::H256, utils::keccak256}; +use zkevm_hashes::keccak::{ + component::{ + circuit::shard::KeccakComponentShardCircuit, + output::{calculate_circuit_outputs_commit, multi_inputs_to_circuit_outputs}, + }, + vanilla::keccak_packed_multi::get_num_keccak_f, +}; + +use crate::{ + halo2_proofs::plonk::Circuit, + utils::{ + component::{ + types::ComponentPublicInstances, ComponentCircuit, ComponentPromiseResult, + ComponentPromiseResultsInMerkle, GroupedPromiseCalls, GroupedPromiseResults, + LogicalResult, PromiseShardMetadata, + }, + encode_h256_to_hilo, + }, + Field, +}; + +use super::types::{ComponentTypeKeccak, CoreInputKeccak, KeccakLogicalInput, KeccakVirtualOutput}; + +impl ComponentCircuit for KeccakComponentShardCircuit { + fn clear_witnesses(&self) { + self.base_circuit_builder().borrow_mut().clear(); + self.hasher().borrow_mut().clear(); + } + /// No promise calls + fn compute_promise_calls(&self) -> Result { + Ok(HashMap::new()) + } + /// The `input` should be of type [CoreInputKeccak]. + /// As a special case, we allow the input to have used capacity less than the configured capacity because the Keccak component circuit knows to automatically pad the input. + fn feed_input(&self, input: Box) -> Result<()> { + let typed_input = + input.downcast::().map_err(|_| anyhow!("invalid input type"))?; + let params_cap = self.params().capacity(); + let input_cap = + typed_input.iter().map(|input| get_num_keccak_f(input.len())).sum::(); + if input_cap > params_cap { + bail!("Input capacity {input_cap} > configured capacity {params_cap}"); + } + let mut inputs = *typed_input; + // resize so the capacity of `inputs` is exactly `params_cap` + inputs.extend(iter::repeat(vec![]).take(params_cap - input_cap)); + *self.inputs().borrow_mut() = inputs; + Ok(()) + } + fn compute_outputs(&self) -> Result> { + let capacity = self.params().capacity(); + // This is the same as the `instances()` implementation + let vt = multi_inputs_to_circuit_outputs::(&self.inputs().borrow(), capacity); + let output_commit_val = calculate_circuit_outputs_commit(&vt); + let pr: Vec> = self + .inputs() + .borrow() + .iter() + .map(|bytes| { + let output = H256(keccak256(bytes)); + LogicalResult::>::new( + KeccakLogicalInput::new(bytes.clone()), + KeccakVirtualOutput::new(encode_h256_to_hilo(&output)), + ) + .into() + }) + .collect(); + + Ok(ComponentPromiseResultsInMerkle::::new( + vec![PromiseShardMetadata { commit: output_commit_val, capacity }], + vec![(0, pr)], + )) + } + fn get_public_instances(&self) -> ComponentPublicInstances { + unreachable!("keccak does not follow ComponentPublicInstances") + } + /// Promise results are ignored since this component makes no promise calls. + fn fulfill_promise_results(&self, _: &GroupedPromiseResults) -> anyhow::Result<()> { + Ok(()) + } +} diff --git a/axiom-eth/src/keccak/mod.rs b/axiom-eth/src/keccak/mod.rs index eeed51c8..7a2346e7 100644 --- a/axiom-eth/src/keccak/mod.rs +++ b/axiom-eth/src/keccak/mod.rs @@ -1,201 +1,164 @@ -//! This module integrates the zkEVM keccak circuit with Axiom's circuits and API. +//! Firstly, the structs and functions in this module **DO NOT** constrain the computation of the keccak hash function. +//! Instead, they are meant to constrain the correctness of keccak hashes on a collection of variable length byte arrays +//! when given a commitment to a lookup table of keccak hashes from an external keccak "coprocessor" circuit. //! -//! Recall that the way zkEVM keccak works is that it proceeds in iterations of keccak_f, -//! squeezing out a new value per keccak_f. -//! Selectors are used to tell it in which rounds to absorb new bytes. For each call of keccak on some input bytes, -//! the circuit pads the bytes and adds the results squeezed out after each keccak_f in the process of computing the keccak -//! of the padded input. -//! -//! In Axiom's circuits, we will queue up all Keccaks that need to be computed throughout, with some extra logic to -//! handle variable length inputs. Then we -//! * compute all keccaks at once in zkevm-keccak sub-circuit -//! * calculate the RLCs of the input bytes and the output bytes in our own sub-circuit -//! * constrain the RLCs are equal -use crate::{ - halo2_proofs::circuit::Region, - rlp::{ - builder::{ - assign_prover_phase0, assign_prover_phase1, KeygenAssignments, RlcThreadBreakPoints, - RlcThreadBuilder, - }, - rlc::{RlcChip, RlcFixedTrace, RlcTrace, RLC_PHASE}, - RlpChip, - }, - util::EthConfigParams, - MPTConfig, -}; use core::iter::once; -use ethers_core::utils::keccak256; + +use getset::Getters; use halo2_base::{ gates::{GateChip, GateInstructions, RangeChip, RangeInstructions}, - halo2_proofs::{ - circuit::{self, AssignedCell, Layouter, SimpleFloorPlanner, Value}, - plonk::{Assigned, Circuit, ConstraintSystem, Error}, - }, - utils::{bit_length, value_to_option, ScalarField}, - AssignedValue, Context, ContextCell, + safe_types::{SafeBytes32, SafeTypeChip}, + utils::{bit_length, ScalarField}, + AssignedValue, Context, QuantumCell::Constant, - SKIP_FIRST_PASS, }; use itertools::Itertools; -use rayon::prelude::*; -use std::{cell::RefCell, collections::HashMap, env::set_var, iter, mem}; -pub(crate) use zkevm_keccak::KeccakConfig; -use zkevm_keccak::{ - keccak_packed_multi::{ - get_num_keccak_f, get_num_rows_per_round, keccak_phase0, multi_keccak_phase1, KeccakRow, - KeccakTable, +use zkevm_hashes::keccak::vanilla::param::NUM_BYTES_TO_SQUEEZE; + +use crate::{ + keccak::promise::KeccakVarLenCall, + utils::{ + bytes_be_to_u128, component::promise_collector::PromiseCaller, u128s_to_bytes_be, + AssignedH256, }, - util::{eth_types::Field, NUM_BYTES_TO_SQUEEZE, NUM_ROUNDS, NUM_WORDS_TO_SQUEEZE, RATE}, + Field, }; -#[cfg(feature = "halo2-axiom")] -type KeccakAssignedValue<'v, F> = AssignedCell<&'v Assigned, F>; -#[cfg(not(feature = "halo2-axiom"))] -type KeccakAssignedValue<'v, F> = AssignedCell; +use self::{ + promise::KeccakFixLenCall, + types::{ComponentTypeKeccak, KeccakFixedLenQuery, KeccakVarLenQuery}, +}; -mod builder; +mod component_shim; +/// Keccak Promise Loader +pub mod promise; #[cfg(test)] mod tests; +/// Types +pub mod types; -pub use builder::*; -pub(crate) type FixedLenRLCs = Vec<(RlcFixedTrace, RlcFixedTrace)>; -pub(crate) type VarLenRLCs = Vec<(RlcTrace, RlcFixedTrace)>; - -pub(crate) const KECCAK_CONTEXT_ID: usize = usize::MAX; - -#[derive(Clone, Debug)] -pub struct KeccakFixedLenQuery { - pub input_bytes: Vec, - pub input_assigned: Vec>, - - pub output_bytes: [u8; NUM_BYTES_TO_SQUEEZE], - pub output_assigned: Vec>, -} - -#[derive(Clone, Debug)] -pub struct KeccakVarLenQuery { - pub min_bytes: usize, - pub max_bytes: usize, - pub num_bytes: usize, - // if `length` is `None`, then this is a fixed length keccak query - // and it is assumed `min_bytes = max_bytes` - pub length: AssignedValue, - pub input_bytes: Vec, - pub input_assigned: Vec>, - - pub output_bytes: [u8; NUM_BYTES_TO_SQUEEZE], - pub output_assigned: Vec>, -} - -pub(crate) type SharedKeccakChip = RefCell>; - -/// `KeccakChip` plays the role both of the chip and something like a `KeccakThreadBuilder` in that it keeps a -/// list of the keccak queries that need to be linked with the external zkEVM keccak chip. -#[derive(Clone, Debug)] +/// Thread-safe manager that collects all keccak queries and then constrains that the input, outputs +/// are correct with respect to an externally provided table of | encoding(input) | keccak(input) |. +#[derive(Clone, Debug, Getters)] pub struct KeccakChip { - pub(crate) num_rows_per_round: usize, - // available only in `FirstPhase` - pub var_len_queries: Vec>, - pub fixed_len_queries: Vec>, -} - -impl Default for KeccakChip { - fn default() -> Self { - Self::new(get_num_rows_per_round()) - } + #[getset(get = "pub")] + promise_caller: PromiseCaller, + #[getset(get = "pub")] + range: RangeChip, } impl KeccakChip { - pub fn new(num_rows_per_round: usize) -> Self { - Self { num_rows_per_round, var_len_queries: vec![], fixed_len_queries: vec![] } + pub fn new(range: RangeChip, promise_collector: PromiseCaller) -> Self { + Self::new_with_promise_collector(range, promise_collector) + } + pub fn new_with_promise_collector( + range: RangeChip, + promise_collector: PromiseCaller, + ) -> Self { + Self { range, promise_caller: promise_collector } + } + pub fn gate(&self) -> &GateChip { + &self.range.gate } /// Takes a byte vector of known fixed length and computes the keccak digest of `bytes`. - /// - Returns `(output_assigned, output_bytes)`, where `output_bytes` is provided just for convenience. + /// - Returns `(output_bytes, output_hi, output_lo)`. /// - This function only computes witnesses for output bytes. - /// The guarantee is that in `SecondPhase`, `input_assigned` and `output_assigned` - /// will have their RLCs computed and these RLCs will be constrained to equal the - /// correct ones in the keccak table. /// - /// Assumes that `input_bytes` coincides with the values of `bytes_assigned` as bytes, - /// if provided (`bytes` is used for faster witness generation). - /// - /// Returns the index in `self.fixed_len_queries` of the query. + /// # Assumptions + /// - `input` elements have been range checked to be bytes + /// - This assumption is rather **unsafe** and assumes the user is careful. + // TODO: use SafeByte pub fn keccak_fixed_len( - &mut self, + &self, ctx: &mut Context, - gate: &impl GateInstructions, - input_assigned: Vec>, - input_bytes: Option>, - ) -> usize { - let bytes = input_bytes.unwrap_or_else(|| get_bytes(&input_assigned[..])); - debug_assert_eq!(bytes.len(), input_assigned.len()); + input: Vec>, + ) -> KeccakFixedLenQuery { + let [output_hi, output_lo] = { + let len = input.len(); + let output = self + .promise_caller + .call::, ComponentTypeKeccak>( + ctx, + KeccakFixLenCall::new(SafeTypeChip::unsafe_to_fix_len_bytes_vec( + input.clone(), + len, + )), + ) + .unwrap(); + output.hash.hi_lo() + }; - let output_bytes = keccak256(&bytes); - let output_assigned = - ctx.assign_witnesses(output_bytes.iter().map(|b| gate.get_field_element(*b as u64))); + // Decompose hi-lo into bytes (with range check). Right now we always provide the bytes for backwards compatibility. + // In the future we may create them on demand. + let output_bytes = u128s_to_bytes_be(ctx, self.range(), &[output_hi, output_lo]); + // no good way to transmute from Vec to SafeBytes32 + let output_raw: Vec> = + output_bytes.into_iter().map(|b| b.into()).collect(); + let output_bytes = SafeTypeChip::unsafe_to_safe_type(output_raw); - self.fixed_len_queries.push(KeccakFixedLenQuery { - input_bytes: bytes, - input_assigned, - output_bytes, - output_assigned, - }); - self.fixed_len_queries.len() - 1 + KeccakFixedLenQuery { input_assigned: input, output_bytes, output_hi, output_lo } } /// Takes a fixed length byte vector and computes the keccak digest of `bytes[..len]`. - /// - Returns `(output_assigned, output_bytes)`, where `output_bytes` is provided just for convenience. + /// - Returns `(output_bytes, output_hi, output_lo)`. /// - This function only computes witnesses for output bytes. - /// The guarantee is that in `SecondPhase`, `input_assigned` and `output_assigned` - /// will have their RLCs computed and these RLCs will be constrained to equal the - /// correct ones in the keccak table. - /// - /// Assumes that `input_bytes[..len]` coincides with the values of `input_assigned[..len]` as bytes, if provided (`bytes` is used for faster witness generation). /// /// Constrains `min_len <= len <= bytes.len()`. /// - /// Returns the index in `self.var_len_queries` of the query. + /// # Assumptions + /// - `input` elements have been range checked to be bytes + /// - This assumption is rather **unsafe** and assumes the user is careful. + // TODO: use SafeByte pub fn keccak_var_len( - &mut self, + &self, ctx: &mut Context, - range: &impl RangeInstructions, - input_assigned: Vec>, - input_bytes: Option>, + input: Vec>, len: AssignedValue, min_len: usize, - ) -> usize { - let bytes = input_bytes.unwrap_or_else(|| get_bytes(&input_assigned[..])); - let max_len = input_assigned.len(); + ) -> KeccakVarLenQuery { + let bytes = get_bytes(&input); + let max_len = input.len(); + let range = self.range(); range.check_less_than_safe(ctx, len, (max_len + 1) as u64); if min_len != 0 { range.check_less_than( ctx, - Constant(range.gate().get_field_element((min_len - 1) as u64)), + Constant(F::from((min_len - 1) as u64)), len, bit_length((max_len + 1) as u64), ); } - let num_bytes = len.value().get_lower_32() as usize; + let num_bytes = len.value().get_lower_64() as usize; debug_assert!(bytes.len() >= num_bytes); - let output_bytes = keccak256(&bytes[..num_bytes]); - let output_assigned = ctx.assign_witnesses( - output_bytes.iter().map(|b| range.gate().get_field_element(*b as u64)), - ); - self.var_len_queries.push(KeccakVarLenQuery { + let [output_hi, output_lo] = { + let output = self + .promise_caller + .call::, ComponentTypeKeccak>( + ctx, + KeccakVarLenCall::new( + SafeTypeChip::unsafe_to_var_len_bytes_vec(input.clone(), len, max_len), + min_len, + ), + ) + .unwrap(); + output.hash.hi_lo() + }; + + // Decompose hi-lo into bytes (with range check). Right now we always provide the bytes for backwards compatibility. + // In the future we may create them on demand. + let output_bytes = u128s_to_bytes_be(ctx, self.range(), &[output_hi, output_lo]); + + KeccakVarLenQuery { min_bytes: min_len, - max_bytes: max_len, - num_bytes, length: len, - input_bytes: bytes, - input_assigned, - output_bytes, - output_assigned, - }); - self.var_len_queries.len() - 1 + input_assigned: input, + output_bytes: output_bytes.try_into().unwrap(), + output_hi, + output_lo, + } } /// Computes the keccak merkle root of a tree with leaves `leaves`. @@ -204,45 +167,37 @@ impl KeccakChip { /// /// # Assumptions /// - `leaves.len()` is a power of two. - /// - Each element of `leaves` is a slice of assigned byte values. + /// - Each element of `leaves` is a slice of assigned **byte** values. /// - The byte length of each element of `leaves` is known and fixed, i.e., we use `keccak_fixed_len` to perform the hashes. /// /// # Warning /// - This implementation currently has no domain separation between hashing leaves versus hashing inner nodes pub fn merkle_tree_root( - &mut self, + &self, ctx: &mut Context, - gate: &impl GateInstructions, leaves: &[impl AsRef<[AssignedValue]>], - ) -> Vec> { + ) -> (SafeBytes32, AssignedH256) { let depth = leaves.len().ilog2() as usize; debug_assert_eq!(1 << depth, leaves.len()); - if depth == 0 { - return leaves[0].as_ref().to_vec(); - } + assert_ne!(depth, 0, "Merkle root of a single leaf is ill-defined"); // bottom layer hashes let mut hashes = leaves .chunks(2) - .into_iter() .map(|pair| { let leaves_concat = [pair[0].as_ref(), pair[1].as_ref()].concat(); - self.keccak_fixed_len(ctx, gate, leaves_concat, None) + self.keccak_fixed_len(ctx, leaves_concat) }) .collect_vec(); debug_assert_eq!(hashes.len(), 1 << (depth - 1)); for d in (0..depth - 1).rev() { for i in 0..(1 << d) { - let bytes_concat = [2 * i, 2 * i + 1] - .map(|idx| &self.fixed_len_queries[hashes[idx]].output_bytes[..]) - .concat(); - let leaves_concat = [2 * i, 2 * i + 1] - .map(|idx| &self.fixed_len_queries[hashes[idx]].output_assigned[..]) - .concat(); - hashes[i] = self.keccak_fixed_len(ctx, gate, leaves_concat, Some(bytes_concat)); + let leaves_concat = + [2 * i, 2 * i + 1].map(|idx| hashes[idx].output_bytes.as_ref()).concat(); + hashes[i] = self.keccak_fixed_len(ctx, leaves_concat); } } - self.fixed_len_queries[hashes[0]].output_assigned.clone() + (hashes[0].output_bytes.clone(), [hashes[0].output_hi, hashes[0].output_lo]) } /// Computes a keccak merkle mountain range of a tree with leaves `leaves`. @@ -261,12 +216,11 @@ impl KeccakChip { /// The merkle root of the tree with leaves `leaves[..num_leaves]` can be recovered by successively hashing the elements in the merkle mountain range, in reverse order, corresponding to indices /// where `num_leaves` has a 1 bit. pub fn merkle_mountain_range( - &mut self, + &self, ctx: &mut Context, - gate: &impl GateInstructions, leaves: &[Vec>], num_leaves_bits: &[AssignedValue], - ) -> Vec>> { + ) -> Vec<(SafeBytes32, AssignedH256)> { let max_depth = leaves.len().ilog2() as usize; assert_eq!(leaves.len(), 1 << max_depth); assert_eq!(num_leaves_bits.len(), max_depth + 1); @@ -276,16 +230,16 @@ impl KeccakChip { // we do this with a barrel-shifter, by shifting `leaves` left by 2^i or 0 depending on the bit in `num_leaves_bits` // we skip the first shift by 2^max_depth because if num_leaves == 2^max_depth then all these subsequent peaks are undefined let mut shift_leaves = leaves.to_vec(); - once(self.merkle_tree_root(ctx, gate, leaves)) + once(self.merkle_tree_root(ctx, leaves)) .chain(num_leaves_bits.iter().enumerate().rev().skip(1).map(|(depth, &sel)| { - let peak = self.merkle_tree_root(ctx, gate, &shift_leaves[..(1usize << depth)]); // no need to shift if we're at the end if depth != 0 { + let peak = self.merkle_tree_root(ctx, &shift_leaves[..(1usize << depth)]); // shift left by sel == 1 ? 2^depth : 0 for i in 0..1 << depth { debug_assert_eq!(shift_leaves[i].len(), NUM_BYTES_TO_SQUEEZE); for j in 0..shift_leaves[i].len() { - shift_leaves[i][j] = gate.select( + shift_leaves[i][j] = self.gate().select( ctx, shift_leaves[i + (1 << depth)][j], shift_leaves[i][j], @@ -293,347 +247,23 @@ impl KeccakChip { ); } } + peak + } else { + let leaf_bytes = + SafeTypeChip::unsafe_to_fix_len_bytes_vec(shift_leaves[0].clone(), 32) + .into_bytes(); + let hi_lo: [_; 2] = + bytes_be_to_u128(ctx, self.gate(), &leaf_bytes).try_into().unwrap(); + let bytes = SafeBytes32::try_from(leaf_bytes).unwrap(); + (bytes, hi_lo) } - - peak })) .collect() } - - fn capacity(&self) -> usize { - self.fixed_len_queries - .iter() - .map(|q| q.input_assigned.len()) - .chain(self.var_len_queries.iter().map(|q| q.max_bytes)) - .map(get_num_keccak_f) - .sum() - } - - /// Wrapper that calls zkEVM Keccak chip for `FirstPhase` region assignments. - /// - /// Do this at the end of `FirstPhase` and then call `assign_phase1` in `SecondPhase`. - pub fn assign_phase0( - &self, - region: &mut Region, - zkevm_keccak: &KeccakConfig, - ) -> Vec<[F; NUM_WORDS_TO_SQUEEZE]> { - let mut squeeze_digests = Vec::with_capacity(self.capacity()); - let mut num_rows_used = 0; - - // Dummy first rows so that the initial data is absorbed - // The initial data doesn't really matter, `is_final` just needs to be disabled. - for (idx, row) in KeccakRow::dummy_rows(self.num_rows_per_round).iter().enumerate() { - zkevm_keccak.set_row(region, idx, row); - } - num_rows_used += self.num_rows_per_round; - - // Generate witnesses for the fixed length queries first since there's no issue of selection - let artifacts_fixed = self - .fixed_len_queries - .par_iter() - .map(|query| { - let num_keccak_f = get_num_keccak_f(query.input_bytes.len()); - let mut squeeze_digests = Vec::with_capacity(num_keccak_f); - let mut rows = - Vec::with_capacity(num_keccak_f * (NUM_ROUNDS + 1) * self.num_rows_per_round); - keccak_phase0(&mut rows, &mut squeeze_digests, &query.input_bytes); - (rows, squeeze_digests) - }) - .collect::>(); - // Generate witnesses for the variable length queries - let artifacts_var = self - .var_len_queries - .par_iter() - .map(|query| { - let num_keccak_f = get_num_keccak_f(query.num_bytes); - let max_keccak_f = get_num_keccak_f(query.max_bytes); - let mut squeeze_digests = Vec::with_capacity(max_keccak_f); - let mut rows = - Vec::with_capacity(max_keccak_f * (NUM_ROUNDS + 1) * self.num_rows_per_round); - - keccak_phase0( - &mut rows, - &mut squeeze_digests, - &query.input_bytes[..query.num_bytes], - ); - // we generate extra keccaks so the number of keccak_f performed equals max_keccak_f - // due to padding, each keccak([]) will do exactly one keccak_f and produce one squeeze digest - let mut filler: Vec<_> = (num_keccak_f..max_keccak_f) - .into_par_iter() - .map(|_| { - let mut squeeze_digests = Vec::with_capacity(1); - let mut rows = - Vec::with_capacity((NUM_ROUNDS + 1) * self.num_rows_per_round); - keccak_phase0(&mut rows, &mut squeeze_digests, &[]); - (rows, squeeze_digests) - }) - .collect(); - for filler in filler.iter_mut() { - rows.append(&mut filler.0); - squeeze_digests.append(&mut filler.1); - } - debug_assert_eq!(squeeze_digests.len(), max_keccak_f); - (rows, squeeze_digests) - }) - .collect::>(); - - for (rows, squeezes) in artifacts_fixed.into_iter().chain(artifacts_var.into_iter()) { - for row in rows { - zkevm_keccak.set_row(region, num_rows_used, &row); - num_rows_used += 1; - } - squeeze_digests.extend(squeezes); - } - squeeze_digests - } - - /// Counterpart of `assign_phase1`. Wrapper that calls zkEVM Keccak chip for `SecondPhase` region assignments. - /// - /// This loads the `KeccakTable` and produces the table of input and output RLCs squeezed out per keccak_f. - /// We also translate these tables, which are of type Halo2 [`AssignedCell`] into our internal [`AssignedValue`] type. - /// - /// We assume this function is the first time Context ID [`KECCAK_CONTEXT_ID`] is used in the circuit. - #[allow(clippy::type_complexity)] - pub fn assign_phase1( - &self, - region: &mut Region, - keccak_table: &KeccakTable, - squeeze_digests: Vec<[F; NUM_WORDS_TO_SQUEEZE]>, - challenge_value: F, - assigned_advices: Option>, - ) -> ( - HashMap<(usize, usize), (circuit::Cell, usize)>, - Vec>, - Vec>, - ) { - let witness_gen_only = assigned_advices.is_none(); - let mut assigned_advices = assigned_advices.unwrap_or_default(); - // the input and output rlcs in the keccak table - let empty: &[u8] = &[]; - let (table_input_rlcs, table_output_rlcs) = multi_keccak_phase1( - region, - keccak_table, - self.fixed_len_queries.iter().map(|query| &query.input_bytes[..]).chain( - self.var_len_queries.iter().flat_map(|query| { - let num_keccak_f = get_num_keccak_f(query.num_bytes); - let max_keccak_f = get_num_keccak_f(query.max_bytes); - iter::once(&query.input_bytes[..query.num_bytes]) - .chain(iter::repeat(empty).take(max_keccak_f - num_keccak_f)) - }), - ), - Value::known(challenge_value), - squeeze_digests, - ); - // the above are external `AssignedCell`s, we need to convert them to internal `AssignedValue`s - let mut upload_cells = |acells: Vec>, - offset| - -> Vec> { - acells - .into_iter() - .enumerate() - .map(|(i, acell)| { - let value = value_to_option(acell.value()) - .map(|v| { - #[cfg(feature = "halo2-axiom")] - { - **v - } - #[cfg(not(feature = "halo2-axiom"))] - { - Assigned::Trivial(*v) - } - }) - .unwrap_or_else(|| Assigned::Trivial(F::zero())); // for keygen - let aval = AssignedValue { - value, - cell: (!witness_gen_only).then_some(ContextCell { - context_id: KECCAK_CONTEXT_ID, - offset: offset + i, - }), - }; - if !witness_gen_only { - // we set row_offset = usize::MAX because you should never be directly using lookup on such a cell - #[cfg(feature = "halo2-axiom")] - assigned_advices - .insert((KECCAK_CONTEXT_ID, offset + i), (*acell.cell(), usize::MAX)); - #[cfg(not(feature = "halo2-axiom"))] - assigned_advices - .insert((KECCAK_CONTEXT_ID, offset + i), (acell.cell(), usize::MAX)); - } - aval - }) - .collect::>() - }; - - let table_input_rlcs = upload_cells(table_input_rlcs, 0); - let table_output_rlcs = upload_cells(table_output_rlcs, table_input_rlcs.len()); - - (assigned_advices, table_input_rlcs, table_output_rlcs) - } - - /// Takes the translated `KeccakTable` of input and output RLCs, produced by the external zkEVM Keccak, - /// and constrains that the RLCs equal the internally computed RLCs from the byte string witnesses. - /// This is what guarantees that the internal byte string witnesses we use as keccak input/outputs actually - /// correspond to correctly computed keccak hashes. - pub fn process_phase1( - &mut self, - thread_pool: &mut RlcThreadBuilder, - rlc: &RlcChip, - range: &RangeChip, - table_input_rlcs: Vec>, - table_output_rlcs: Vec>, - ) -> (FixedLenRLCs, VarLenRLCs) { - let witness_gen_only = thread_pool.witness_gen_only(); - let gate = range.gate(); - let (fixed_len_rlcs, var_len_rlcs) = self.compute_all_rlcs(thread_pool, rlc, gate); - - let mut keccak_f_index = 0; - let ctx = thread_pool.gate_builder.main(0); // this ctx only used for equality constraints, so doesn't matter which phase. If we used phase 1 and it was the only empty ctx, currently it causes a panic in `assign_all` - // we know exactly where the fixed length squeezed RLCs are in the table, so it's straightforward to fetch - for (input_rlc, output_rlc) in &fixed_len_rlcs { - keccak_f_index += get_num_keccak_f(input_rlc.len); - - ctx.constrain_equal(&table_input_rlcs[keccak_f_index - 1], &input_rlc.rlc_val); - ctx.constrain_equal(&table_output_rlcs[keccak_f_index - 1], &output_rlc.rlc_val); - } - // ctx dropped now - - assert_eq!(self.var_len_queries.len(), var_len_rlcs.len()); - // some additional selection logic is needed for fetching the correct hash for variable length - // for each variable length keccak query, we select the correct keccak_f squeeze digest from a possible range of keccak_f's - let var_selects = self - .var_len_queries - .iter() - .map(|query| { - let min_keccak_f = get_num_keccak_f(query.min_bytes); - let max_keccak_f = get_num_keccak_f(query.max_bytes); - // query.length is variable - let select_range = keccak_f_index + min_keccak_f - 1..keccak_f_index + max_keccak_f; - // we want to select index `keccak_f_index + get_num_keccak_f(query.length) - 1` where `query.length` is variable - // this is the same as selecting `idx = get_num_keccak_f(query.length) - min_keccak_f` from `select_range` - - // num_keccak_f = length / RATE + 1 - let idx = (select_range.len() > 1).then(|| { - let ctx = thread_pool.gate_builder.main(RLC_PHASE); - let (len_div_rate, _) = - range.div_mod(ctx, query.length, RATE, bit_length(query.max_bytes as u64)); - gate.sub( - ctx, - len_div_rate, - Constant(gate.get_field_element(min_keccak_f as u64 - 1)), - ) - }); - keccak_f_index += max_keccak_f; - (select_range, idx) - }) - .collect_vec(); - - // multiple-thread selecting of correct rlc from keccak table - let ctx_ids = (0..var_selects.len()).map(|_| thread_pool.get_new_thread_id()).collect_vec(); - let mut ctxs = ctx_ids - .into_par_iter() - .zip(var_selects.into_par_iter()) - .zip(var_len_rlcs.par_iter()) - .map(|((ctx_id, (select_range, idx)), (input_rlc, output_rlc))| { - let mut ctx = Context::new(witness_gen_only, ctx_id); - let (table_input_rlc, table_output_rlc) = if let Some(idx) = idx { - let indicator = gate.idx_to_indicator(&mut ctx, idx, select_range.len()); - let input_rlc = gate.select_by_indicator( - &mut ctx, - table_input_rlcs[select_range.clone()].iter().copied(), - indicator.iter().copied(), - ); - let output_rlc = gate.select_by_indicator( - &mut ctx, - table_output_rlcs[select_range].iter().copied(), - indicator, - ); - (input_rlc, output_rlc) - } else { - debug_assert_eq!(select_range.len(), 1); - (table_input_rlcs[select_range.start], table_output_rlcs[select_range.start]) - }; - // Define the dynamic RLC: RLC(a, l) = \sum_{i = 0}^{l - 1} a_i r^{l - 1 - i} - // For a variable length RLC, we only have a1 = a2 if RLC(a1, l1) = RLC(a2, l2) AND l1 = l2. - // In general, the length constraint is necessary because a1, a2 can have leading zeros. - // However, I think it is not necessary in this case because if a1, a2 have the same RLC, - // then they only differ in leading zeros. Since the zkevm-keccak pads the input with trailing bits, - // this leads to the padded input being different in the two cases, which would lead to different outputs. - ctx.constrain_equal(&input_rlc.rlc_val, &table_input_rlc); - ctx.constrain_equal(&output_rlc.rlc_val, &table_output_rlc); - - ctx - }) - .collect::>(); - thread_pool.gate_builder.threads[RLC_PHASE].append(&mut ctxs); - - (fixed_len_rlcs, var_len_rlcs) - } - - pub(crate) fn compute_all_rlcs( - &mut self, - thread_pool: &mut RlcThreadBuilder, - rlc: &RlcChip, - gate: &GateChip, - ) -> (FixedLenRLCs, VarLenRLCs) { - let witness_gen_only = thread_pool.witness_gen_only(); - // multi-threaded computation of fixed-length RLCs - let ctx_ids = (0..self.fixed_len_queries.len()) - .map(|_| thread_pool.get_new_thread_id()) - .collect_vec(); - let (mut ctxs, fixed_len_rlcs): (Vec<_>, Vec<_>) = self - .fixed_len_queries - .par_iter_mut() - .zip(ctx_ids.into_par_iter()) - .map(|(query, rlc_id)| { - let mut ctx_rlc = Context::new(witness_gen_only, rlc_id); - let input = mem::take(&mut query.input_assigned); - let output = mem::take(&mut query.output_assigned); - let input_rlc = rlc.compute_rlc_fixed_len(&mut ctx_rlc, input); - let output_rlc = rlc.compute_rlc_fixed_len(&mut ctx_rlc, output); - (ctx_rlc, (input_rlc, output_rlc)) - }) - .unzip(); - thread_pool.threads_rlc.append(&mut ctxs); - - // multi-threaded computation of variable-length RLCs - let ctx_ids = (0..self.var_len_queries.len()) - .map(|_| (thread_pool.get_new_thread_id(), thread_pool.get_new_thread_id())) - .collect_vec(); - let (ctxs, var_len_rlcs): (Vec<_>, Vec<_>) = self - .var_len_queries - .par_iter_mut() - .zip(ctx_ids.into_par_iter()) - .map(|(query, (gate_id, rlc_id))| { - let mut ctx_gate = Context::new(witness_gen_only, gate_id); - let mut ctx_rlc = Context::new(witness_gen_only, rlc_id); - let input = mem::take(&mut query.input_assigned); - let output = mem::take(&mut query.output_assigned); - let input_rlc = - rlc.compute_rlc((&mut ctx_gate, &mut ctx_rlc), gate, input, query.length); - let output_rlc = rlc.compute_rlc_fixed_len(&mut ctx_rlc, output); - ((ctx_gate, ctx_rlc), (input_rlc, output_rlc)) - }) - .unzip(); - let (mut ctxs_gate, mut ctxs_rlc): (Vec<_>, Vec<_>) = ctxs.into_iter().unzip(); - thread_pool.gate_builder.threads[RLC_PHASE].append(&mut ctxs_gate); - thread_pool.threads_rlc.append(&mut ctxs_rlc); - - (fixed_len_rlcs, var_len_rlcs) - } } // convert field values to u8: -pub fn get_bytes(bytes_assigned: &[AssignedValue]) -> Vec { +pub fn get_bytes(bytes: &[impl AsRef>]) -> Vec { // TODO: if we really wanted to optimize, we can pre-compute a HashMap containing just `F::from(byte as u64)` for each byte. I think the cost of hashing is still cheaper than performing the Montgomery reduction - bytes_assigned.iter().map(|abyte| abyte.value().get_lower_32() as u8).collect_vec() -} - -pub(crate) fn rows_per_round(max_rows: usize, num_keccak_f: usize) -> usize { - use zkevm_keccak::util::NUM_WORDS_TO_ABSORB; - - log::info!("Number of keccak_f permutations: {num_keccak_f}"); - let rows_per_round = max_rows / (num_keccak_f * (NUM_ROUNDS + 1) + 1 + NUM_WORDS_TO_ABSORB); - log::info!("Optimal keccak rows per round: {rows_per_round}"); - rows_per_round + bytes.iter().map(|b| b.as_ref().value().get_lower_64() as u8).collect_vec() } diff --git a/axiom-eth/src/keccak/promise.rs b/axiom-eth/src/keccak/promise.rs new file mode 100644 index 00000000..4bc8646d --- /dev/null +++ b/axiom-eth/src/keccak/promise.rs @@ -0,0 +1,343 @@ +use std::{any::Any, marker::PhantomData}; + +use getset::Getters; +use halo2_base::{ + gates::{circuit::builder::BaseCircuitBuilder, GateInstructions, RangeChip, RangeInstructions}, + safe_types::{FixLenBytesVec, VarLenBytesVec}, + utils::bit_length, + AssignedValue, Context, + QuantumCell::Constant, +}; +use itertools::Itertools; +use num_bigint::BigUint; +use snark_verifier::util::hash::Poseidon; +use snark_verifier_sdk::NativeLoader; +use zkevm_hashes::keccak::{ + component::{ + encode::{format_input, num_word_per_witness, pack_native_input}, + param::{POSEIDON_RATE, POSEIDON_T}, + }, + vanilla::{ + keccak_packed_multi::get_num_keccak_f, + param::{NUM_BITS_PER_WORD, NUM_BYTES_TO_ABSORB}, + }, +}; + +use crate::{ + rlc::chip::RlcChip, + utils::component::{ + promise_loader::comp_loader::ComponentCommiter, + types::Flatten, + utils::{create_hasher, into_key, native_poseidon_hasher, try_from_key}, + ComponentCircuit, ComponentType, ComponentTypeId, LogicalInputValue, PromiseCallWitness, + TypelessLogicalInput, + }, + Field, +}; + +use super::types::{ + ComponentTypeKeccak, KeccakLogicalInput, KeccakVirtualInput, KeccakVirtualOutput, + OutputKeccakShard, NUM_WITNESS_PER_KECCAK_F, +}; + +/// Keccak promise call for fixed-length inputs. +#[derive(Clone, Debug, Getters)] +pub struct KeccakFixLenCall { + #[getset(get = "pub")] + bytes: FixLenBytesVec, +} +impl KeccakFixLenCall { + pub fn new(bytes: FixLenBytesVec) -> Self { + Self { bytes } + } + pub fn to_logical_input(&self) -> KeccakLogicalInput { + let bytes_vec = self + .bytes + .bytes() + .iter() + .map(|b| b.as_ref().value().get_lower_64() as u8) + .collect_vec(); + KeccakLogicalInput::new(bytes_vec) + } +} + +impl PromiseCallWitness for KeccakFixLenCall { + fn get_component_type_id(&self) -> ComponentTypeId { + ComponentTypeKeccak::::get_type_id() + } + fn get_capacity(&self) -> usize { + get_num_keccak_f(self.bytes.len()) + } + fn to_rlc( + &self, + (gate_ctx, rlc_ctx): (&mut Context, &mut Context), + range_chip: &RangeChip, + rlc_chip: &RlcChip, + ) -> AssignedValue { + let len = self.bytes.len(); + // NOTE: we pack with len + 1 instead of len for domain separation so the RLC can distinguish an empty input with is_final = 1 vs 0 + let len_p1 = gate_ctx.load_constant(F::from((len + 1) as u64)); + let packed_input = + format_input(gate_ctx, &range_chip.gate, self.bytes.bytes(), len_p1).concat().concat(); + let rlc_fixed_trace = rlc_chip.compute_rlc_fixed_len(rlc_ctx, packed_input); + rlc_fixed_trace.rlc_val + } + fn to_typeless_logical_input(&self) -> TypelessLogicalInput { + into_key(self.to_logical_input()) + } + fn get_mock_output(&self) -> Flatten { + let bytes_vec = self + .bytes + .bytes() + .iter() + .map(|b| b.as_ref().value().get_lower_64() as u8) + .collect_vec(); + let logical_input = KeccakLogicalInput::new(bytes_vec); + let output_val = logical_input.compute_output(); + output_val.into() + } + fn as_any(&self) -> &dyn Any { + self + } +} + +/// Keccak promise call for fixed-length inputs. +#[derive(Clone, Debug, Getters)] +pub struct KeccakVarLenCall { + #[getset(get = "pub")] + bytes: VarLenBytesVec, + min_len: usize, +} +impl KeccakVarLenCall { + pub fn new(bytes: VarLenBytesVec, min_len: usize) -> Self { + Self { bytes, min_len } + } + pub fn to_logical_input(&self) -> KeccakLogicalInput { + let len = self.bytes.len().value().get_lower_64() as usize; + let bytes_vec = self.bytes.bytes()[..len] + .iter() + .map(|b| b.as_ref().value().get_lower_64() as u8) + .collect_vec(); + KeccakLogicalInput::new(bytes_vec) + } + /// Returns `num_keccak_f - 1`, where + /// `num_keccak_f = bytes.len() / NUM_BYTES_TO_ABSORB + 1` is the true + /// number of `keccak_f` permutations necessary for variable input length `bytes.len()`. + pub fn num_keccak_f_m1( + &self, + gate_ctx: &mut Context, + range_chip: &RangeChip, + ) -> AssignedValue { + let max_len = self.bytes.max_len(); + let num_bits = bit_length(max_len as u64); + let len = *self.bytes.len(); + let (num_keccak_f_m1, _) = + range_chip.div_mod(gate_ctx, len, BigUint::from(NUM_BYTES_TO_ABSORB), num_bits); + num_keccak_f_m1 + } +} + +impl PromiseCallWitness for KeccakVarLenCall { + fn get_component_type_id(&self) -> ComponentTypeId { + ComponentTypeKeccak::::get_type_id() + } + fn get_capacity(&self) -> usize { + get_num_keccak_f(self.bytes.len().value().get_lower_64() as usize) + } + fn to_rlc( + &self, + (gate_ctx, rlc_ctx): (&mut Context, &mut Context), + range_chip: &RangeChip, + rlc_chip: &RlcChip, + ) -> AssignedValue { + let bytes = self.bytes.ensure_0_padding(gate_ctx, &range_chip.gate); + let num_keccak_f_m1 = self.num_keccak_f_m1(gate_ctx, range_chip); + + let len = bytes.len(); + let len_p1 = range_chip.gate.inc(gate_ctx, *len); + + let num_keccak_f = range_chip.gate.inc(gate_ctx, num_keccak_f_m1); + let packed_input = format_input(gate_ctx, &range_chip.gate, bytes.bytes(), len_p1); + let packed_input = packed_input.into_iter().flatten().flatten(); + + let rlc_len = range_chip.gate.mul( + gate_ctx, + Constant(F::from(NUM_WITNESS_PER_KECCAK_F as u64)), + num_keccak_f, + ); + let rlc_trace = rlc_chip.compute_rlc_with_min_len( + (gate_ctx, rlc_ctx), + &range_chip.gate, + packed_input, + rlc_len, + get_num_keccak_f(self.min_len) * NUM_WITNESS_PER_KECCAK_F, + ); + rlc_trace.rlc_val + } + fn to_typeless_logical_input(&self) -> TypelessLogicalInput { + into_key(self.to_logical_input()) + } + fn get_mock_output(&self) -> Flatten { + let len = self.bytes.len().value().get_lower_64() as usize; + let bytes_vec = self.bytes.bytes()[..len] + .iter() + .map(|b| b.as_ref().value().get_lower_64() as u8) + .collect_vec(); + let logical_input: KeccakLogicalInput = KeccakLogicalInput::new(bytes_vec); + let output_val: as ComponentType>::OutputValue = + logical_input.compute_output(); + output_val.into() + } + fn as_any(&self) -> &dyn Any { + self + } +} + +fn get_dummy_key(native_poseidon: &mut Poseidon) -> F { + native_poseidon.clear(); + // copied from encode_native_input, but save re-creating Spec above + let witnesses_per_keccak_f = pack_native_input(&[]); + for witnesses in witnesses_per_keccak_f { + for absorbing in witnesses.chunks(POSEIDON_RATE) { + // To avoid absorbing witnesses crossing keccak_fs together, pad 0s to make sure absorb.len() == RATE. + let mut padded_absorb = [F::ZERO; POSEIDON_RATE]; + padded_absorb[..absorbing.len()].copy_from_slice(absorbing); + native_poseidon.update(&padded_absorb); + } + } + native_poseidon.squeeze() +} + +/// KeccakComponentCommiter implements the commitment computation in KeccakComponentShardCircuit which uses a legacy way to compute commitment. +pub struct KeccakComponentCommiter(PhantomData); + +impl ComponentCommiter for KeccakComponentCommiter { + /// This must match the commitment computation in [zkevm_hashes::keccak::component::circuit::shard::encode_inputs_from_keccak_fs] and + /// [zkevm_hashes::keccak::component::circuit::shard::KeccakComponentShardCircuit::publish_outputs] with `publish_raw_outputs = false`. + fn compute_commitment( + builder: &mut BaseCircuitBuilder, + witness_virtual_rows: &[(Flatten>, Flatten>)], + ) -> AssignedValue { + let range_chip = &builder.range_chip(); + let ctx = builder.main(0); + + let mut hasher = create_hasher::(); + hasher.initialize_consts(ctx, &range_chip.gate); + let dummy_key = { + let mut native_poseidon = Poseidon::from_spec(&NativeLoader, hasher.spec().clone()); + get_dummy_key(&mut native_poseidon) + }; + let dummy_input = ctx.load_constant(dummy_key); + let parsed_virtual_rows: Vec<(KeccakVirtualInput<_>, KeccakVirtualOutput<_>)> = + witness_virtual_rows + .iter() + .map(|(v_i, v_o)| { + (v_i.clone().try_into().unwrap(), v_o.clone().try_into().unwrap()) + }) + .collect_vec(); + // Constraint is_final of each virtual row + let mut remaining_keccak_f = ctx.load_zero(); + for (v_i, _) in &parsed_virtual_rows { + let (_, length_placeholder) = range_chip.div_mod( + ctx, + v_i.packed_input[0], + BigUint::from(1u128 << NUM_BITS_PER_WORD), + NUM_BITS_PER_WORD * num_word_per_witness::(), + ); + // num_keccak_f = length / NUM_BYTES_TO_ABSORB + 1 + // num_keccak_f_dec = num_keccak_f - 1 + let (num_keccak_f_dec, _) = range_chip.div_mod( + ctx, + length_placeholder, + BigUint::from(NUM_BYTES_TO_ABSORB), + NUM_BITS_PER_WORD, + ); + let remaining_keccak_f_is_zero = range_chip.gate.is_zero(ctx, remaining_keccak_f); + let remaining_keccak_f_dec = range_chip.gate.dec(ctx, remaining_keccak_f); + remaining_keccak_f = range_chip.gate.select( + ctx, + num_keccak_f_dec, + remaining_keccak_f_dec, + remaining_keccak_f_is_zero, + ); + let is_final = range_chip.gate.is_zero(ctx, remaining_keccak_f); + ctx.constrain_equal(&is_final, &v_i.is_final); + } + + let mut inputs_to_poseidon = Vec::with_capacity(parsed_virtual_rows.len()); + let mut virtual_outputs = Vec::with_capacity(parsed_virtual_rows.len()); + for (v_i, v_o) in parsed_virtual_rows { + inputs_to_poseidon.push(v_i.into()); + virtual_outputs.push(v_o); + } + let poseidon_results = + hasher.hash_compact_chunk_inputs(ctx, &range_chip.gate, &inputs_to_poseidon); + let keccak_outputs = poseidon_results + .into_iter() + .zip_eq(virtual_outputs) + .map(|(po, vo)| { + let key = range_chip.gate.select(ctx, po.hash(), dummy_input, po.is_final()); + vec![key, vo.hash.lo(), vo.hash.hi()] + }) + .concat(); + hasher.hash_fix_len_array(ctx, &range_chip.gate, &keccak_outputs) + } + + /// This code path is currently never used, but it should still be consistent with + /// `self.compute_commitment`. + /// + /// We do not do input validation of `is_final` in this function and just assume it is correct. + fn compute_native_commitment(witness_virtual_rows: &[(Flatten, Flatten)]) -> F { + let mut hasher = native_poseidon_hasher(); + let dummy_key = get_dummy_key(&mut hasher); + hasher.clear(); + let keccak_outputs: Vec<_> = witness_virtual_rows + .iter() + .flat_map(|(v_i, v_o)| { + let (v_i, v_o): (KeccakVirtualInput<_>, KeccakVirtualOutput<_>) = + (v_i.clone().try_into().unwrap(), v_o.clone().try_into().unwrap()); + hasher.update(&v_i.packed_input); + let key = if v_i.is_final == F::ONE { + let key = hasher.squeeze(); + hasher.clear(); + key + } else { + dummy_key + }; + let [hi, lo] = v_o.hash.hi_lo(); + [key, lo, hi] + }) + .collect(); + hasher.clear(); + hasher.update(&keccak_outputs); + hasher.squeeze() + } +} + +/// A helper function to fulfill keccak promises for a Component for testing. +pub fn generate_keccak_shards_from_calls( + comp_circuit: &dyn ComponentCircuit, + capacity: usize, +) -> anyhow::Result { + let calls = comp_circuit.compute_promise_calls()?; + let keccak_type_id = ComponentTypeKeccak::::get_type_id(); + let keccak_calls = calls.get(&keccak_type_id).ok_or(anyhow::anyhow!("no keccak calls"))?; + let mut used_capacity = 0; + let responses = keccak_calls + .iter() + .map(|call| { + let li = try_from_key::(&call.logical_input).unwrap(); + used_capacity += >::get_capacity(&li); + (li.bytes.clone().into(), None) + }) + .collect_vec(); + log::info!("Keccak used capacity: {}", used_capacity); + if used_capacity > capacity { + return Err(anyhow::anyhow!( + "used capacity {} exceeds capacity {}", + used_capacity, + capacity + )); + } + Ok(OutputKeccakShard { responses, capacity }) +} diff --git a/axiom-eth/src/keccak/tests.rs b/axiom-eth/src/keccak/tests.rs index 8a88421a..cf4c42dd 100644 --- a/axiom-eth/src/keccak/tests.rs +++ b/axiom-eth/src/keccak/tests.rs @@ -1,207 +1,186 @@ -#![allow(unused_imports)] -use super::*; use crate::{ - halo2_proofs::{ - arithmetic::FieldExt, - circuit::{Layouter, SimpleFloorPlanner, Value}, - dev::MockProver, - halo2curves::bn256::{Bn256, Fr, G1Affine}, - plonk::*, - poly::commitment::{Params, ParamsProver}, - poly::kzg::{ - commitment::{KZGCommitmentScheme, ParamsKZG}, - multiopen::{ProverGWC, ProverSHPLONK, VerifierSHPLONK}, - strategy::SingleStrategy, - }, - transcript::{Blake2bRead, Blake2bWrite, Challenge255}, - transcript::{TranscriptReadBuffer, TranscriptWriterBuffer}, + keccak::types::{ComponentTypeKeccak, KeccakVirtualInput, KeccakVirtualOutput}, + rlc::circuit::builder::RlcCircuitBuilder, + utils::component::{ + promise_loader::{comp_loader::ComponentCommiter, flatten_witness_to_rlc}, + types::{FixLenLogical, Flatten}, + utils::{into_key, load_logical_value}, + ComponentCircuit, ComponentType, LogicalResult, PromiseCallWitness, }, - rlp::rlc::RlcConfig, }; -use ark_std::{end_timer, start_timer}; + use halo2_base::{ - gates::{ - flex_gate::{FlexGateConfig, GateStrategy}, - range::{RangeConfig, RangeStrategy}, - }, - utils::{fe_to_biguint, fs::gen_srs, value_to_option, ScalarField}, - SKIP_FIRST_PASS, + gates::GateInstructions, halo2_proofs::halo2curves::bn256::Fr, safe_types::SafeTypeChip, + AssignedValue, Context, }; -use itertools::{assert_equal, Itertools}; -use rand::{rngs::StdRng, Rng, SeedableRng}; -use rand_core::OsRng; -use serde::{Deserialize, Serialize}; -use std::{ - env::{set_var, var}, - fs::File, - io::{BufRead, BufReader, Write}, +use itertools::Itertools; +use snark_verifier_sdk::CircuitExt; +use zkevm_hashes::keccak::{ + component::circuit::shard::{KeccakComponentShardCircuit, KeccakComponentShardCircuitParams}, + vanilla::param::NUM_BYTES_TO_ABSORB, }; -use zkevm_keccak::keccak_packed_multi::get_keccak_capacity; - -fn test_keccak_circuit( - k: u32, - mut builder: RlcThreadBuilder, - inputs: Vec>, - var_len: bool, -) -> KeccakCircuitBuilder> { - let prover = builder.witness_gen_only(); - let range = RangeChip::default(8); - let keccak = SharedKeccakChip::default(); - let ctx = builder.gate_builder.main(0); - let mut rng = StdRng::from_seed([0u8; 32]); - for (_idx, input) in inputs.into_iter().enumerate() { - let bytes = input.to_vec(); - let mut bytes_assigned = - ctx.assign_witnesses(bytes.iter().map(|byte| F::from(*byte as u64))); - let len = - if var_len && !bytes.is_empty() { rng.gen_range(0..bytes.len()) } else { bytes.len() }; - for byte in bytes_assigned[len..].iter_mut() { - *byte = ctx.load_zero(); - } - - let len = ctx.load_witness(F::from(len as u64)); - - let _hash = if var_len { - keccak.borrow_mut().keccak_var_len(ctx, &range, bytes_assigned, Some(bytes), len, 0) - } else { - keccak.borrow_mut().keccak_fixed_len(ctx, &range.gate, bytes_assigned, Some(bytes)) - }; - } - let circuit = KeccakCircuitBuilder::new( - builder, - keccak, - range, - None, - |_: &mut RlcThreadBuilder, _: RlpChip, _: (FixedLenRLCs, VarLenRLCs)| {}, + +use super::{ + promise::{KeccakComponentCommiter, KeccakFixLenCall, KeccakVarLenCall}, + types::KeccakLogicalInput, +}; + +fn verify_rlc_consistency( + logical_input: KeccakLogicalInput, + f: impl Fn(&mut Context) -> Box>, +) { + let output = logical_input.compute_output(); + + let mut builder = RlcCircuitBuilder::::new(false, 32); + builder.set_k(18); + builder.set_lookup_bits(8); + // Mock gamma for testing. + builder.gamma = Some(Fr::from([1, 5, 7, 8])); + let range_chip = &builder.range_chip(); + let rlc_chip = builder.rlc_chip(&range_chip.gate); + let (gate_ctx, rlc_ctx) = builder.rlc_ctx_pair(); + + let assigned_output: KeccakVirtualOutput> = + load_logical_value(gate_ctx, &output); + let call = f(gate_ctx); + + let key = into_key(logical_input.clone()); + assert_eq!(&call.to_typeless_logical_input(), &key); + + let lr = + LogicalResult::>::new(logical_input.clone(), output.clone()); + let vrs_from_results = ComponentTypeKeccak::::logical_result_to_virtual_rows(&lr); + let assigned_vrs_from_results: Vec<(KeccakVirtualInput<_>, KeccakVirtualOutput<_>)> = + vrs_from_results + .into_iter() + .map(|(input, output)| { + (load_logical_value(gate_ctx, &input), load_logical_value(gate_ctx, &output)) + }) + .collect_vec(); + let rlc_from_results = ComponentTypeKeccak::::rlc_virtual_rows( + (gate_ctx, rlc_ctx), + range_chip, + &rlc_chip, + &assigned_vrs_from_results, + ); + + let mut rlc_from_call = call.to_rlc((gate_ctx, rlc_ctx), range_chip, &rlc_chip); + let output_rlc = flatten_witness_to_rlc(rlc_ctx, &rlc_chip, &assigned_output.into()); + let output_multiplier = rlc_chip.rlc_pow_fixed( + gate_ctx, + &range_chip.gate, + KeccakVirtualOutput::::get_num_fields(), ); - if !prover { - let unusable_rows = - var("UNUSABLE_ROWS").unwrap_or_else(|_| "109".to_string()).parse().unwrap(); - circuit.config(k as usize, Some(unusable_rows)); - } - circuit + rlc_from_call = range_chip.gate.mul_add(gate_ctx, rlc_from_call, output_multiplier, output_rlc); + + assert_eq!(rlc_from_results.last().unwrap().value(), rlc_from_call.value()); +} + +#[test] +fn test_rlc_consistency() { + let raw_bytes: [u8; 135] = [1; NUM_BYTES_TO_ABSORB - 1]; + let logical_input: KeccakLogicalInput = KeccakLogicalInput::new(raw_bytes.to_vec()); + // Fix-len + verify_rlc_consistency(logical_input.clone(), |gate_ctx| { + let assigned_raw_bytes = + gate_ctx.assign_witnesses(raw_bytes.into_iter().map(|b| Fr::from(b as u64))); + let fix_len_call = KeccakFixLenCall::new(SafeTypeChip::unsafe_to_fix_len_bytes_vec( + assigned_raw_bytes, + raw_bytes.len(), + )); + Box::new(fix_len_call) + }); + // Var-len + verify_rlc_consistency(logical_input, |gate_ctx| { + let max_len = NUM_BYTES_TO_ABSORB; + let len = gate_ctx.load_witness(Fr::from(raw_bytes.len() as u64)); + let var_len_bytes = vec![1; max_len]; + + let assigned_var_len_bytes = + gate_ctx.assign_witnesses(var_len_bytes.into_iter().map(|b| Fr::from(b as u64))); + let var_len_call = KeccakVarLenCall::new( + SafeTypeChip::unsafe_to_var_len_bytes_vec(assigned_var_len_bytes, len, max_len), + 10, + ); + Box::new(var_len_call) + }); } -/// Cmdline: KECCAK_DEGREE=14 RUST_LOG=info cargo test -- --nocapture test_keccak +// Test compute outputs against `instances()` implementation #[test] -pub fn test_keccak() { - let _ = env_logger::builder().is_test(true).try_init(); +fn test_compute_outputs_commit_keccak() { + let k: usize = 15; + let num_unusable_row: usize = 109; + let capacity: usize = 10; + let publish_raw_outputs: bool = false; - let k: u32 = var("KECCAK_DEGREE").unwrap_or_else(|_| "14".to_string()).parse().unwrap(); let inputs = vec![ + (0u8..200).collect::>(), vec![], (0u8..1).collect::>(), (0u8..135).collect::>(), (0u8..136).collect::>(), (0u8..200).collect::>(), ]; - let circuit = test_keccak_circuit(k, RlcThreadBuilder::mock(), inputs.clone(), false); - MockProver::::run(k, &circuit, vec![]).unwrap().assert_satisfied(); - println!("Fixed len keccak passed"); + // used capacity = 9 + let mut padded_inputs = inputs.clone(); + padded_inputs.push(vec![]); - let circuit = test_keccak_circuit(k, RlcThreadBuilder::mock(), inputs, true); - MockProver::::run(k, &circuit, vec![]).unwrap().assert_satisfied(); - println!("Var len keccak passed"); -} + let params = + KeccakComponentShardCircuitParams::new(k, num_unusable_row, capacity, publish_raw_outputs); + let circuit = KeccakComponentShardCircuit::::new(vec![], params, false); + circuit.feed_input(Box::new(inputs)).unwrap(); + let commit = circuit.instances()[0][0]; -#[derive(Serialize, Deserialize)] -pub struct KeccakBenchConfig { - degree: usize, - range_advice: Vec, - num_rlc: usize, - unusable_rows: usize, - rows_per_round: usize, + let res = circuit.compute_outputs().unwrap(); + assert_eq!(res.leaves()[0].commit, commit); + assert_eq!( + res.shards()[0].1.iter().map(|(i, _o)| i.clone()).collect_vec(), + padded_inputs + .into_iter() + .map(|bytes| into_key(KeccakLogicalInput::new(bytes))) + .collect_vec() + ); } +/// Test `compute_native_commitment` against the custom `compute_outputs` implementation #[test] -fn bench_keccak() { - let _ = env_logger::builder().is_test(true).try_init(); - let var_len = true; - - let bench_params_file = File::open("configs/bench/keccak.json").unwrap(); - std::fs::create_dir_all("data/bench").unwrap(); - let mut fs_results = File::create("data/bench/keccak.csv").unwrap(); - writeln!( - fs_results, - "degree,advice_columns,unusable_rows,rows_per_round,keccak_f/s,num_keccak_f,proof_time,proof_size,verify_time" - ) - .unwrap(); - - let bench_params_reader = BufReader::new(bench_params_file); - let bench_params: Vec = - serde_json::from_reader(bench_params_reader).unwrap(); - for bench_params in bench_params { - println!( - "---------------------- degree = {} ------------------------------", - bench_params.degree - ); - let k = bench_params.degree as u32; - let num_rows = (1 << k) - bench_params.unusable_rows; - set_var("KECCAK_ROWS", bench_params.rows_per_round.to_string()); - let capacity = get_keccak_capacity(num_rows); - println!("Performing {capacity} keccak_f permutations"); - let inputs = vec![vec![0; 135]; capacity]; - let circuit = test_keccak_circuit(k, RlcThreadBuilder::keygen(), inputs.clone(), var_len); - - // MockProver::::run(k, &circuit, vec![]).unwrap().assert_satisfied(); - - let params = gen_srs(k); - let vk = keygen_vk(¶ms, &circuit).unwrap(); - let pk = keygen_pk(¶ms, vk, &circuit).unwrap(); - let break_points = circuit.break_points.take(); - - let inputs = (0..capacity) - .map(|_| (0..135).map(|_| rand::random::()).collect_vec()) - .collect_vec(); - // create a proof - let proof_time = start_timer!(|| "Create proof SHPLONK"); - let circuit = test_keccak_circuit(k, RlcThreadBuilder::prover(), inputs.clone(), var_len); - *circuit.break_points.borrow_mut() = break_points; - let mut transcript = Blake2bWrite::<_, _, Challenge255<_>>::init(vec![]); - create_proof::< - KZGCommitmentScheme, - ProverSHPLONK<'_, Bn256>, - Challenge255, - _, - Blake2bWrite, G1Affine, Challenge255>, - _, - >(¶ms, &pk, &[circuit], &[&[]], OsRng, &mut transcript) - .unwrap(); - let proof = transcript.finalize(); - end_timer!(proof_time); - - let verify_time = start_timer!(|| "Verify time"); - let verifier_params = params.verifier_params(); - let strategy = SingleStrategy::new(¶ms); - let mut transcript = Blake2bRead::<_, G1Affine, Challenge255<_>>::init(&proof[..]); - verify_proof::< - KZGCommitmentScheme, - VerifierSHPLONK<'_, Bn256>, - Challenge255, - Blake2bRead<&[u8], G1Affine, Challenge255>, - SingleStrategy<'_, Bn256>, - >(verifier_params, pk.get_vk(), strategy, &[&[]], &mut transcript) - .unwrap(); - end_timer!(verify_time); - - let auto_params: EthConfigParams = - serde_json::from_str(var("ETH_CONFIG_PARAMS").unwrap().as_str()).unwrap(); - let keccak_advice = std::env::var("KECCAK_ADVICE_COLUMNS") - .unwrap_or_else(|_| "0".to_string()) - .parse::() - .unwrap(); - writeln!( - fs_results, - "{},{},{},{},{:.2},{},{:.2}s,{:?}", - auto_params.degree, - auto_params.num_range_advice.iter().sum::() + keccak_advice + 2, - var("UNUSABLE_ROWS").unwrap(), - auto_params.keccak_rows_per_round, - f64::from(capacity as u32) / proof_time.time.elapsed().as_secs_f64(), - capacity, - proof_time.time.elapsed().as_secs_f64(), - verify_time.time.elapsed() - ) - .unwrap(); - } +fn test_compute_native_commit_keccak() { + let k: usize = 15; + let num_unusable_row: usize = 109; + let capacity: usize = 10; + let publish_raw_outputs: bool = false; + + let inputs = vec![ + (0u8..200).collect::>(), + vec![], + (0u8..1).collect::>(), + (0u8..135).collect::>(), + (0u8..136).collect::>(), + (0u8..200).collect::>(), + ]; + // used capacity = 9 + let mut padded_inputs = inputs.clone(); + padded_inputs.push(vec![]); + + let params = + KeccakComponentShardCircuitParams::new(k, num_unusable_row, capacity, publish_raw_outputs); + let circuit = KeccakComponentShardCircuit::::new(vec![], params, false); + circuit.feed_input(Box::new(inputs)).unwrap(); + + let res = circuit.compute_outputs().unwrap(); + let commit = res.leaves()[0].commit; + + let vt = padded_inputs + .into_iter() + .flat_map(|bytes| { + let logical_input = KeccakLogicalInput::new(bytes); + let output = logical_input.compute_output(); + let lr = LogicalResult::>::new(logical_input, output); + ComponentTypeKeccak::::logical_result_to_virtual_rows(&lr) + }) + .map(|(v_i, v_o)| (Flatten::from(v_i), Flatten::from(v_o))) + .collect_vec(); + let commit2 = KeccakComponentCommiter::compute_native_commitment(&vt); + assert_eq!(commit, commit2); } diff --git a/axiom-eth/src/keccak/types.rs b/axiom-eth/src/keccak/types.rs new file mode 100644 index 00000000..437a0d14 --- /dev/null +++ b/axiom-eth/src/keccak/types.rs @@ -0,0 +1,394 @@ +use std::{marker::PhantomData, sync::RwLock}; + +use anyhow::anyhow; + +use ethers_core::{ + types::{Bytes, H256}, + utils::keccak256, +}; +use halo2_base::{ + gates::{GateInstructions, RangeChip}, + poseidon::hasher::PoseidonCompactChunkInput, + safe_types::{SafeBytes32, SafeTypeChip}, + utils::ScalarField, + AssignedValue, Context, +}; +use itertools::Itertools; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; + +use type_map::concurrent::TypeMap; +use zkevm_hashes::keccak::{ + component::{encode::pack_native_input, output::KeccakCircuitOutput, param::POSEIDON_RATE}, + vanilla::keccak_packed_multi::get_num_keccak_f, +}; + +use crate::{ + rlc::chip::RlcChip, + utils::{ + component::{ + types::{FixLenLogical, Flatten}, + ComponentType, ComponentTypeId, LogicalInputValue, LogicalResult, + }, + hilo::HiLo, + AssignedH256, + }, + Field, +}; + +use super::promise::KeccakComponentCommiter; + +#[derive(Clone, Debug)] +pub struct KeccakFixedLenQuery { + /// Input in bytes + pub input_assigned: Vec>, + /// The hash digest, in bytes + // For backwards compatbility we always compute this; we can consider computing it on-demand in the future + pub output_bytes: SafeBytes32, + /// The hash digest, hi 128 bits (range checked by lookup table) + pub output_hi: AssignedValue, + /// The hash digest, lo 128 bits (range checked by lookup table) + pub output_lo: AssignedValue, +} + +impl KeccakFixedLenQuery { + pub fn hi_lo(&self) -> AssignedH256 { + [self.output_hi, self.output_lo] + } +} + +#[derive(Clone, Debug)] +pub struct KeccakVarLenQuery { + pub min_bytes: usize, + // pub max_bytes: usize, // equal to input_assigned.len() + // pub num_bytes: usize, + /// Actual length of input + pub length: AssignedValue, + pub input_assigned: Vec>, + /// The hash digest, in bytes + // For backwards compatbility we always compute this; we can consider computing it on-demand in the future + pub output_bytes: SafeBytes32, + /// The hash digest, hi 128 bits (range checked by lookup table) + pub output_hi: AssignedValue, + /// The hash digest, lo 128 bits (range checked by lookup table) + pub output_lo: AssignedValue, +} + +impl KeccakVarLenQuery { + pub fn hi_lo(&self) -> AssignedH256 { + [self.output_hi, self.output_lo] + } +} + +/// The core logical input to the keccak component circuit. +pub type CoreInputKeccak = Vec>; + +#[derive(Clone, Debug, Default, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +pub struct OutputKeccakShard { + /// The (assumed to be deduplicated) list of requests, in the form of variable + /// length byte arrays to be hashed. Optionally include the calculated hash. + pub responses: Vec<(Bytes, Option)>, + /// To prevent inconsistencies, also specify the capacity of the keccak circuit + pub capacity: usize, +} + +impl OutputKeccakShard { + /// Createa a dummy OutputKeccakShard with the given capacity. + pub fn create_dummy(capacity: usize) -> Self { + Self { responses: vec![], capacity } + } + pub fn into_logical_results(self) -> Vec>> { + let mut total_capacity = 0; + let mut promise_results = self + .responses + .into_iter() + .map(|(input, output)| { + let input = KeccakLogicalInput::new(input.to_vec()); + total_capacity += get_num_keccak_f(input.bytes.len()); + let v_output = + if let Some(hash) = output { hash.into() } else { input.compute_output::() }; + LogicalResult::>::new(input, v_output) + }) + .collect_vec(); + assert!(total_capacity <= self.capacity); + if total_capacity < self.capacity { + let target_len = self.capacity - total_capacity + promise_results.len(); + let dummy = dummy_circuit_output::(); + promise_results.resize( + target_len, + LogicalResult::new( + KeccakLogicalInput::new(vec![]), + KeccakVirtualOutput:: { + hash: HiLo::from_hi_lo([dummy.hash_hi, dummy.hash_lo]), + }, + ), + ); + } + promise_results + } +} + +/// KeccakLogicalInput is the logical input of Keccak Component. +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct KeccakLogicalInput { + pub bytes: Vec, +} +impl KeccakLogicalInput { + // Create KeccakLogicalInput + pub fn new(bytes: Vec) -> Self { + Self { bytes } + } + pub fn compute_output(&self) -> KeccakVirtualOutput { + let hash = H256(keccak256(&self.bytes)); + hash.into() + } +} + +impl LogicalInputValue for KeccakLogicalInput { + fn get_capacity(&self) -> usize { + get_num_keccak_f(self.bytes.len()) + } +} + +pub(crate) const NUM_WITNESS_PER_KECCAK_F: usize = 6; +const KECCAK_VIRTUAL_INPUT_FIELD_SIZE: [usize; NUM_WITNESS_PER_KECCAK_F + 1] = [ + 192, 192, 192, 192, 192, 192, // packed_input + 1, // is_final +]; +const KECCAK_VIRTUAL_OUTPUT_FIELD_SIZE: [usize; 2] = [128, 128]; + +/// Virtual input of Keccak Component. +/// TODO: this cannot work if F::capacity < 192. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct KeccakVirtualInput { + // 1 length + 17 64-byte words, every 3 are compressed into 1 witness. + // spec: https://github.com/axiom-crypto/halo2-lib/blob/9e6c9a16196e7e2ce58ccb6ffc31984fc0ba69d9/hashes/zkevm/src/keccak/component/encode.rs#L25 + pub packed_input: [T; NUM_WITNESS_PER_KECCAK_F], + // Whether this is the last chunk of the input. + // TODO: this is hacky because it can be derived from packed_input but it's not really committed. + pub is_final: T, +} + +impl KeccakVirtualInput { + pub fn new(packed_input: [T; NUM_WITNESS_PER_KECCAK_F], is_final: T) -> Self { + Self { packed_input, is_final } + } +} + +impl TryFrom> for KeccakVirtualInput { + type Error = anyhow::Error; + + fn try_from(value: Flatten) -> std::result::Result { + if value.field_size != KECCAK_VIRTUAL_INPUT_FIELD_SIZE { + return Err(anyhow::anyhow!("invalid field size")); + } + if value.field_size.len() != value.fields.len() { + return Err(anyhow::anyhow!("field length doesn't match")); + } + + Ok(Self { + packed_input: value.fields[0..NUM_WITNESS_PER_KECCAK_F] + .try_into() + .map_err(|_| anyhow!("failed to convert flatten to KeccakVirtualInput"))?, + is_final: value.fields[NUM_WITNESS_PER_KECCAK_F], + }) + } +} +impl From> for Flatten { + fn from(val: KeccakVirtualInput) -> Self { + Self { + fields: [val.packed_input.as_slice(), [val.is_final].as_slice()].concat(), + field_size: &KECCAK_VIRTUAL_INPUT_FIELD_SIZE, + } + } +} +impl FixLenLogical for KeccakVirtualInput { + fn get_field_size() -> &'static [usize] { + &KECCAK_VIRTUAL_INPUT_FIELD_SIZE + } +} + +impl From>> + for PoseidonCompactChunkInput +{ + fn from(val: KeccakVirtualInput>) -> Self { + let KeccakVirtualInput::> { packed_input, is_final } = val; + assert!(packed_input.len() % POSEIDON_RATE == 0); + let inputs: Vec<[AssignedValue; POSEIDON_RATE]> = packed_input + .into_iter() + .chunks(POSEIDON_RATE) + .into_iter() + .map(|c| c.collect_vec().try_into().unwrap()) + .collect_vec(); + let is_final = SafeTypeChip::unsafe_to_bool(is_final); + Self::new(inputs, is_final) + } +} + +/// Virtual input of Keccak Component. +#[derive(Default, Debug, Clone, Hash, PartialEq, Eq)] +pub struct KeccakVirtualOutput { + /// Keccak hash result + pub hash: HiLo, +} + +impl KeccakVirtualOutput { + pub fn new(hash: HiLo) -> Self { + Self { hash } + } +} + +impl TryFrom> for KeccakVirtualOutput { + type Error = anyhow::Error; + + fn try_from(value: Flatten) -> std::result::Result { + if value.field_size != KECCAK_VIRTUAL_OUTPUT_FIELD_SIZE { + return Err(anyhow::anyhow!("invalid field size")); + } + if value.field_size.len() != value.fields.len() { + return Err(anyhow::anyhow!("field length doesn't match")); + } + + Ok(Self { + hash: HiLo::from_hi_lo( + value + .fields + .try_into() + .map_err(|_| anyhow!("failed to convert flatten to KeccakVirtualOutput"))?, + ), + }) + } +} +impl From> for Flatten { + fn from(val: KeccakVirtualOutput) -> Self { + Self { fields: val.hash.hi_lo().to_vec(), field_size: &KECCAK_VIRTUAL_OUTPUT_FIELD_SIZE } + } +} +impl FixLenLogical for KeccakVirtualOutput { + fn get_field_size() -> &'static [usize] { + &KECCAK_VIRTUAL_OUTPUT_FIELD_SIZE + } +} +impl From for KeccakVirtualOutput { + fn from(hash: H256) -> Self { + let hash_hi = u128::from_be_bytes(hash[..16].try_into().unwrap()); + let hash_lo = u128::from_be_bytes(hash[16..].try_into().unwrap()); + Self { hash: HiLo::from_hi_lo([F::from_u128(hash_hi), F::from_u128(hash_lo)]) } + } +} + +#[derive(Debug, Clone)] +pub struct ComponentTypeKeccak(PhantomData); + +impl ComponentType for ComponentTypeKeccak { + type InputValue = KeccakVirtualInput; + type InputWitness = KeccakVirtualInput>; + type OutputValue = KeccakVirtualOutput; + type OutputWitness = KeccakVirtualOutput>; + type LogicalInput = KeccakLogicalInput; + type Commiter = KeccakComponentCommiter; + + fn get_type_id() -> ComponentTypeId { + "axiom-eth:ComponentTypeKeccak".to_string() + } + + fn logical_result_to_virtual_rows_impl( + ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + let virtual_inputs = Self::logical_input_to_virtual_rows_impl(&ins.input); + let len = virtual_inputs.len(); + let mut virtual_outputs = Vec::with_capacity(len); + let dummy = dummy_circuit_output(); + virtual_outputs.resize( + len - 1, + Self::OutputValue { hash: HiLo::from_hi_lo([dummy.hash_hi, dummy.hash_lo]) }, + ); + virtual_outputs.push(ins.output.clone()); + virtual_inputs.into_iter().zip_eq(virtual_outputs).collect_vec() + } + fn logical_input_to_virtual_rows_impl(li: &Self::LogicalInput) -> Vec { + let mut packed_inputs = pack_native_input::(&li.bytes); + let len = packed_inputs.len(); + for (i, packed_input) in packed_inputs.iter_mut().enumerate() { + let is_final = if i + 1 == len { F::ONE } else { F::ZERO }; + packed_input.push(is_final); + } + packed_inputs + .into_iter() + .map(|p| KeccakVirtualInput::try_from_raw(p).unwrap()) + .collect_vec() + } + fn rlc_virtual_rows( + (gate_ctx, rlc_ctx): (&mut Context, &mut Context), + range_chip: &RangeChip, + rlc_chip: &RlcChip, + virtual_rows: &[(Self::InputWitness, Self::OutputWitness)], + ) -> Vec> { + let gate = &range_chip.gate; + let one = gate_ctx.load_constant(F::ONE); + let zero = gate_ctx.load_zero(); + let empty_input_rlc = rlc_chip.rlc_pow_fixed(gate_ctx, gate, NUM_WITNESS_PER_KECCAK_F - 1); + // = rlc_chip.compute_rlc_fixed_len(rlc_ctx, [one, zero, zero, zero, zero, zero]).rlc_val; + // empty_input_rlc[0] = empty_input_len + 1 = 1. empty_input corresponds to input = [] + + let chunk_multiplier = + rlc_chip.rlc_pow_fixed(gate_ctx, &range_chip.gate, NUM_WITNESS_PER_KECCAK_F); + let output_multiplier = rlc_chip.rlc_pow_fixed( + gate_ctx, + &range_chip.gate, + Self::OutputWitness::get_num_fields(), + ); + + // If last chunk is a final chunk. + let mut last_is_final = one; + // RLC of the current logical input. + let mut curr_rlc = zero; + let mut virtual_row_rlcs = Vec::with_capacity(virtual_rows.len()); + for (input, output) in virtual_rows { + let mut input_to_rlc = input.packed_input; + // +1 to length when calculating RLC in order to make sure 0 is not a valid RLC for any input. Therefore the lookup + // table column doesn't need a selector. + input_to_rlc[0] = range_chip.gate.add(gate_ctx, input_to_rlc[0], last_is_final); + + let chunk_rlc = rlc_chip.compute_rlc_fixed_len(rlc_ctx, input_to_rlc).rlc_val; + curr_rlc = range_chip.gate.mul_add(gate_ctx, curr_rlc, chunk_multiplier, chunk_rlc); + + let input_rlc = + range_chip.gate.select(gate_ctx, curr_rlc, empty_input_rlc, input.is_final); + let output_rlc = rlc_chip.compute_rlc_fixed_len(rlc_ctx, output.hash.hi_lo()).rlc_val; + let virtual_row_rlc = + range_chip.gate.mul_add(gate_ctx, input_rlc, output_multiplier, output_rlc); + virtual_row_rlcs.push(virtual_row_rlc); + + curr_rlc = range_chip.gate.select(gate_ctx, zero, curr_rlc, input.is_final); + + last_is_final = input.is_final; + } + virtual_row_rlcs + } +} + +lazy_static! { + /// We cache the dummy circuit output to avoid re-computing it. + /// The recomputation involves creating an optimized Poseidon spec, which is + /// time intensive. + static ref CACHED_DUMMY_CIRCUIT_OUTPUT: RwLock = RwLock::new(TypeMap::new()); +} + +/// The default dummy_circuit_output needs to do Poseidon. Poseidon generic over F +/// requires re-computing the optimized Poseidon spec, which is computationally +/// intensive. Since we call dummy_circuit_output very often, we cache the result +/// as a performance optimization. +fn dummy_circuit_output() -> KeccakCircuitOutput { + use zkevm_hashes::keccak::component::output::dummy_circuit_output; + + let cached_output = + CACHED_DUMMY_CIRCUIT_OUTPUT.read().unwrap().get::>().cloned(); + if let Some(cached_output) = cached_output { + return cached_output; + } + let output = dummy_circuit_output::(); + CACHED_DUMMY_CIRCUIT_OUTPUT.write().unwrap().insert(output); + output +} diff --git a/axiom-eth/src/lib.rs b/axiom-eth/src/lib.rs index fe268b62..2f8a71bd 100644 --- a/axiom-eth/src/lib.rs +++ b/axiom-eth/src/lib.rs @@ -1,303 +1,30 @@ -#![feature(array_zip)] -#![feature(int_log)] #![feature(trait_alias)] -#![feature(return_position_impl_trait_in_trait)] -#![allow(incomplete_features)] +#![feature(associated_type_defaults)] +#![feature(associated_type_bounds)] #![warn(clippy::useless_conversion)] -use std::env::{set_var, var}; +use serde::{de::DeserializeOwned, Serialize}; -#[cfg(feature = "display")] -use ark_std::{end_timer, start_timer}; -use halo2_base::{ - gates::{ - builder::{CircuitBuilderStage, MultiPhaseThreadBreakPoints}, - flex_gate::FlexGateConfig, - range::RangeConfig, - RangeChip, - }, - halo2_proofs::{ - self, - circuit::{Layouter, SimpleFloorPlanner}, - halo2curves::bn256::{Bn256, Fr}, - plonk::{Circuit, Column, ConstraintSystem, Error, Instance}, - poly::{commitment::Params, kzg::commitment::ParamsKZG}, - }, - AssignedValue, -}; -pub use mpt::EthChip; -use serde::{Deserialize, Serialize}; -use snark_verifier_sdk::halo2::aggregation::AggregationCircuit; -pub use zkevm_keccak::util::eth_types::Field; -use zkevm_keccak::KeccakConfig; +pub use halo2_base; +pub use halo2_base::halo2_proofs; +pub use halo2_base::halo2_proofs::halo2curves; +pub use snark_verifier; +pub use snark_verifier_sdk; +pub use zkevm_hashes; -use crate::rlp::{ - builder::{RlcThreadBreakPoints, RlcThreadBuilder}, - rlc::RlcConfig, - RlpConfig, -}; -use keccak::{FnSynthesize, KeccakCircuitBuilder, SharedKeccakChip}; -use util::EthConfigParams; - -pub mod batch_query; pub mod block_header; pub mod keccak; pub mod mpt; +pub mod receipt; +pub mod rlc; pub mod rlp; +pub mod solidity; pub mod storage; -pub mod util; +pub mod transaction; +pub mod utils; #[cfg(feature = "providers")] pub mod providers; -pub(crate) const ETH_LOOKUP_BITS: usize = 8; // always want 8 to range check bytes - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] -pub enum Network { - Mainnet, - Goerli, -} - -impl std::fmt::Display for Network { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Network::Mainnet => write!(f, "mainnet"), - Network::Goerli => write!(f, "goerli"), - } - } -} - -#[derive(Clone, Debug)] -pub struct MPTConfig { - pub rlp: RlpConfig, - pub keccak: KeccakConfig, -} - -impl MPTConfig { - pub fn configure(meta: &mut ConstraintSystem, params: EthConfigParams) -> Self { - let degree = params.degree; - let mut rlp = RlpConfig::configure( - meta, - params.num_rlc_columns, - ¶ms.num_range_advice, - ¶ms.num_lookup_advice, - params.num_fixed, - params.lookup_bits.unwrap_or(ETH_LOOKUP_BITS), - degree as usize, - ); - set_var("KECCAK_DEGREE", degree.to_string()); - set_var("KECCAK_ROWS", params.keccak_rows_per_round.to_string()); - let keccak = KeccakConfig::new(meta, rlp.rlc.gamma); - set_var("UNUSABLE_ROWS", meta.minimum_rows().to_string()); - #[cfg(feature = "display")] - println!("Unusable rows: {}", meta.minimum_rows()); - rlp.range.gate.max_rows = (1 << degree) - meta.minimum_rows(); - Self { rlp, keccak } - } -} - -#[derive(Clone, Debug)] -/// Config shared for block header and storage proof circuits -pub struct EthConfig { - mpt: MPTConfig, - pub instance: Column, -} - -impl EthConfig { - pub fn configure(meta: &mut ConstraintSystem, params: impl Into) -> Self { - let mpt = MPTConfig::configure(meta, params.into()); - let instance = meta.instance_column(); - meta.enable_equality(instance); - Self { mpt, instance } - } - - pub fn gate(&self) -> &FlexGateConfig { - &self.mpt.rlp.range.gate - } - pub fn range(&self) -> &RangeConfig { - &self.mpt.rlp.range - } - pub fn rlc(&self) -> &RlcConfig { - &self.mpt.rlp.rlc - } - pub fn rlp(&self) -> &RlpConfig { - &self.mpt.rlp - } - pub fn keccak(&self) -> &KeccakConfig { - &self.mpt.keccak - } - pub fn mpt(&self) -> &MPTConfig { - &self.mpt - } -} - -/// This is an extension of [`KeccakCircuitBuilder`] that adds support for public instances (aka public inputs+outputs) -/// -/// The intended design is that [`KeccakCircuitBuilder`] is constructed and populated. In the process, the builder produces some assigned instances, which are supplied as `assigned_instances` to this struct. -/// The [`Circuit`] implementation for this struct will then expose these instances and constrain them using the Halo2 API. -pub struct EthCircuitBuilder> { - pub circuit: KeccakCircuitBuilder, - pub assigned_instances: Vec>, -} - -impl> EthCircuitBuilder { - pub fn new( - assigned_instances: Vec>, - builder: RlcThreadBuilder, - keccak: SharedKeccakChip, - range: RangeChip, - break_points: Option, - synthesize_phase1: FnPhase1, - ) -> Self { - Self { - assigned_instances, - circuit: KeccakCircuitBuilder::new( - builder, - keccak, - range, - break_points, - synthesize_phase1, - ), - } - } - - pub fn config(&self, k: usize, minimum_rows: Option) -> EthConfigParams { - self.circuit.config(k, minimum_rows) - } - - pub fn break_points(&self) -> RlcThreadBreakPoints { - self.circuit.break_points.borrow().clone() - } - - pub fn instance_count(&self) -> usize { - self.assigned_instances.len() - } - - pub fn instance(&self) -> Vec { - self.assigned_instances.iter().map(|v| *v.value()).collect() - } -} - -impl> Circuit for EthCircuitBuilder { - type Config = EthConfig; - type FloorPlanner = SimpleFloorPlanner; - - fn without_witnesses(&self) -> Self { - unimplemented!() - } - - fn configure(meta: &mut ConstraintSystem) -> Self::Config { - let params: EthConfigParams = - serde_json::from_str(&var("ETH_CONFIG_PARAMS").unwrap()).unwrap(); - EthConfig::configure(meta, params) - } - - fn synthesize( - &self, - config: Self::Config, - mut layouter: impl Layouter, - ) -> Result<(), Error> { - // we later `take` the builder, so we need to save this value - let witness_gen_only = self.circuit.builder.borrow().witness_gen_only(); - let assigned_advices = self.circuit.two_phase_synthesize(&config.mpt, &mut layouter); - - if !witness_gen_only { - // expose public instances - let mut layouter = layouter.namespace(|| "expose"); - for (i, instance) in self.assigned_instances.iter().enumerate() { - let cell = instance.cell.unwrap(); - let (cell, _) = assigned_advices - .get(&(cell.context_id, cell.offset)) - .expect("instance not assigned"); - layouter.constrain_instance(*cell, config.instance, i); - } - } - Ok(()) - } -} - -#[cfg(feature = "aggregation")] -impl> snark_verifier_sdk::CircuitExt - for EthCircuitBuilder -{ - fn num_instance(&self) -> Vec { - vec![self.instance_count()] - } - fn instances(&self) -> Vec> { - vec![self.instance()] - } -} - -/// Trait for objects that can be used to create an [`EthCircuitBuilder`] instantiation. -pub trait EthPreCircuit: Sized { - /// Creates a circuit without auto-configuring it. - fn create( - self, - builder: RlcThreadBuilder, - break_points: Option, - ) -> EthCircuitBuilder>; - - /// If feature 'production' is on, this is the same as `create`. Otherwise, it will read `ETH_CONFIG_PARAMS` - /// from the environment to determine the desired circuit degree and number of unusable rows and then auto-configure - /// the circuit and set environmental variables. - fn create_circuit( - self, - builder: RlcThreadBuilder, - break_points: Option, - ) -> EthCircuitBuilder> { - let prover = builder.witness_gen_only(); - #[cfg(feature = "display")] - let start = start_timer!(|| "EthPreCircuit: create_circuit"); - let circuit = self.create(builder, break_points); - #[cfg(feature = "display")] - end_timer!(start); - #[cfg(not(feature = "production"))] - if !prover { - let config_params: EthConfigParams = serde_json::from_str( - var("ETH_CONFIG_PARAMS").expect("ETH_CONFIG_PARAMS is not set").as_str(), - ) - .unwrap(); - circuit.config(config_params.degree as usize, Some(config_params.unusable_rows)); - } - circuit - } -} - -/// Trait for objects that can be used to create a [`RangeWithInstanceCircuitBuilder`] instantiation. -pub trait AggregationPreCircuit: Sized { - /// Creates a circuit without auto-configuring it. - /// - /// `params` should be the universal trusted setup for the present aggregation circuit. - /// We assume the trusted setup for the previous SNARKs is compatible with `params` in the sense that - /// the generator point and toxic waste `tau` are the same. - fn create( - self, - stage: CircuitBuilderStage, - break_points: Option, - lookup_bits: usize, - params: &ParamsKZG, - ) -> AggregationCircuit; - - /// If feature 'production' is on, this is the same as `create`. Otherwise, it will determine the desired - /// circuit degree from `params.k()` and auto-configure the circuit and set environmental variables. - fn create_circuit( - self, - stage: CircuitBuilderStage, - break_points: Option, - lookup_bits: usize, - params: &ParamsKZG, - ) -> AggregationCircuit { - #[cfg(feature = "display")] - let start = start_timer!(|| "AggregationPreCircuit: create_circuit"); - let circuit = self.create(stage, break_points, lookup_bits, params); - #[cfg(feature = "display")] - end_timer!(start); - #[cfg(not(feature = "production"))] - if stage != CircuitBuilderStage::Prover { - let minimum_rows = var("UNUSABLE_ROWS").map(|s| s.parse().unwrap_or(10)).unwrap_or(10); - circuit.config(params.k(), Some(minimum_rows)); - } - circuit - } -} +pub trait RawField = zkevm_hashes::util::eth_types::Field; +pub trait Field = RawField + Serialize + DeserializeOwned; diff --git a/axiom-eth/src/mpt/mod.rs b/axiom-eth/src/mpt/mod.rs index 0c9dc467..91e9fb36 100644 --- a/axiom-eth/src/mpt/mod.rs +++ b/axiom-eth/src/mpt/mod.rs @@ -1,23 +1,29 @@ //! Merkle Patricia Trie (MPT) inclusion & exclusion proofs in ZK. //! //! See https://hackmd.io/@axiom/ry35GZ4l3 for a technical walkthrough of circuit structure and logic +//! +//! # Assumptions +//! - We have tuned our circuit constants (see [`MAX_BRANCH_ITEM_LENS`]) for the case where the MPT value never ends as the 17th item in a branch. +//! - This only happens if a key in the trie is the prefix of another key in the trie. +//! - This never happens for Ethereum tries: +//! - Either the trie has fixed key length (state, storage) +//! - The key is of the form `rlp(idx)` and bytes are converted to even number of hexes (transaction, receipt). +//! If two `i, j` had `rlp(i)` prefix of `rlp(j)`, that means there would have been no way to RLP decode `rlp(j)` (since you would decode it at first as if it were `rlp(i)`). +//! - If one needed to handle this case, one can add some additional handling using a different `MAX_BRANCH_ITEM_LENS` when parsing the terminal node. +use crate::Field; use crate::{ - keccak::{self, ContainsParallelizableKeccakQueries, KeccakChip}, - rlp::{ - max_rlp_len_len, - rlc::{ - rlc_is_equal, rlc_select, rlc_select_by_indicator, rlc_select_from_idx, RlcContextPair, - RlcTrace, RlcVar, - }, - RlpChip, RlpFieldTrace, + keccak::{types::KeccakVarLenQuery, KeccakChip}, + rlc::{ + chip::{rlc_is_equal, rlc_select, rlc_select_by_indicator, rlc_select_from_idx, RlcChip}, + circuit::builder::RlcContextPair, + types::{RlcFixedTrace, RlcTrace, RlcVar}, }, - rlp::{rlc::RlcChip, RlpArrayTraceWitness}, - Field, + rlp::{max_rlp_encoding_len, types::RlpFieldTrace, RlpChip}, }; use ethers_core::{types::H256, utils::hex::FromHex}; use halo2_base::{ gates::{GateChip, GateInstructions, RangeChip, RangeInstructions}, - utils::{bit_length, ScalarField}, + utils::{bit_length, log2_ceil, ScalarField}, AssignedValue, Context, QuantumCell::{Constant, Existing, Witness}, }; @@ -27,284 +33,72 @@ use rlp::Rlp; use serde::{Deserialize, Serialize}; use std::{ cmp::max, - iter::{self, once}, + iter::{self}, }; #[cfg(test)] mod tests; +mod types; + +pub use types::*; + +pub const BRANCH_NUM_ITEMS: usize = 17; +pub const MAX_BRANCH_ITEM_LENS: [usize; BRANCH_NUM_ITEMS] = max_branch_lens(1).0; // max_vt_bytes = 0 is likely also ok; for our use cases, the value in a branch is always empty +pub const MAX_BRANCH_ENCODING_BYTES: usize = max_branch_lens(1).1; lazy_static! { - pub static ref MAX_BRANCH_LENS: (Vec, usize) = max_branch_lens(); - static ref DUMMY_BRANCH: Vec = Vec::from_hex("f1808080808080808080808080808080a0000000000000000000000000000000000000000000000000000000000000000080").unwrap(); + static ref DUMMY_BRANCH: Vec = Vec::from_hex("d18080808080808080808080808080808080").unwrap(); static ref DUMMY_EXT: Vec = Vec::from_hex( "e21ba00000000000000000000000000000000000000000000000000000000000000000").unwrap(); /// rlp(["", 0x0]) static ref NULL_LEAF: Vec = Vec::from_hex( "c3818000").unwrap(); /// keccak(rlp("")) = keccak(0x80) - static ref KECCAK_RLP_EMPTY_STRING: Vec = Vec::from_hex( + pub static ref KECCAK_RLP_EMPTY_STRING: Vec = Vec::from_hex( "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(); static ref RLP_EMPTY_STRING: Vec = Vec::from_hex( "80").unwrap(); } -#[derive(Clone, Debug)] -pub struct LeafTrace { - key_path: RlpFieldTrace, - value: RlpFieldTrace, - hash_rlc: RlcVar, -} - -#[derive(Clone, Debug)] -pub struct LeafTraceWitness { - pub rlp: RlpArrayTraceWitness, - pub hash_query_idx: usize, - // pub max_leaf_bytes: usize, -} - -#[derive(Clone, Debug)] -pub struct ExtensionTrace { - key_path: RlpFieldTrace, - node_ref: RlpFieldTrace, - hash_rlc: RlcVar, -} - -#[derive(Clone, Debug)] -pub struct ExtensionTraceWitness { - pub rlp: RlpArrayTraceWitness, - pub hash_query_idx: usize, - // pub max_ext_bytes: usize, -} - -#[derive(Clone, Debug)] -pub struct BranchTrace { - node_refs: [RlpFieldTrace; 17], - hash_rlc: RlcVar, -} - -#[derive(Clone, Debug)] -pub struct BranchTraceWitness { - pub rlp: RlpArrayTraceWitness, - pub hash_query_idx: usize, - // pub max_branch_bytes: usize, -} - -// helper types for readability -pub type AssignedBytes = Vec>; -pub type AssignedNibbles = Vec>; - -#[derive(Clone, Debug)] -pub struct MPTNode { - pub rlp_bytes: AssignedBytes, - /// 0 = branch, 1 = extension - pub node_type: AssignedValue, -} - -#[derive(Clone, Debug)] -/// The `node_type` flag selects whether the node is parsed as a branch or extension node. -pub struct MPTNodeWitness { - /// 0 = branch, 1 = extension - pub node_type: AssignedValue, - /// The node parsed as an extension node, or dummy extension node otherwise - pub ext: ExtensionTraceWitness, - /// The node parsed as a branch node, or dummy branch node otherwise - pub branch: BranchTraceWitness, -} - -#[derive(Clone, Debug)] -/// The `node_type` flag selects whether the node is parsed as a branch or extension node. -pub struct MPTNodeTrace { - /// 0 = branch, 1 = extension - pub node_type: AssignedValue, - /// The node parsed as an extension node, or dummy extension node otherwise - ext: ExtensionTrace, - /// The node parsed as a branch node, or dummy branch node otherwise - branch: BranchTrace, -} - -#[derive(Clone, Debug)] -/// A fragment of the key (bytes), stored as nibbles before hex-prefix encoding -pub struct MPTKeyFragment { - /// A variable length string of hex-numbers, resized to a fixed max length with 0s - pub nibbles: AssignedNibbles, - pub is_odd: AssignedValue, - // hex_len = 2 * byte_len + is_odd - 2 - // if nibble for branch: byte_len = is_odd = 1 - /// The byte length of the hex-prefix encoded fragment - pub byte_len: AssignedValue, -} - -#[derive(Clone, Debug)] -pub struct MPTFixedKeyProof { - // claim specification: (key, value) - /// The key bytes, fixed length - pub key_bytes: AssignedBytes, - /// The RLP encoded `value` as bytes, variable length, resized to `value_max_byte_len` - pub value_bytes: AssignedBytes, - pub value_byte_len: AssignedValue, - pub root_hash_bytes: AssignedBytes, - - // proof specification - /// The variable length of the proof, including the leaf node if !slot_is_empty. - pub depth: AssignedValue, - /// RLP encoding of the final leaf node - pub leaf_bytes: AssignedBytes, - /// The non-leaf nodes of the mpt proof, resized to `max_depth - 1` with dummy **branch** nodes. - /// The actual variable length is `depth - 1` if `slot_is_empty == true` (excludes leaf node), otherwise `depth`. - pub nodes: Vec>, - /// The key fragments of the mpt proof, variable length, resized to `max_depth` with dummy fragments. - /// Each fragment (nibbles aka hexes) is variable length, resized to `2 * key_byte_len` with 0s - pub key_frag: Vec>, - /// Boolean indicating whether the MPT contains a value at `key` - pub slot_is_empty: AssignedValue, - - // pub key_byte_len: usize, - // pub value_max_byte_len: usize, - /// `max_depth` should be `>=1` - pub max_depth: usize, -} - -#[derive(Clone, Debug)] -pub struct MPTFixedKeyProofWitness { - // pub proof: MPTFixedKeyProof, - // we keep only the parts of the proof necessary: - pub value_bytes: AssignedBytes, - pub value_byte_len: AssignedValue, - pub root_hash_bytes: AssignedBytes, - /// The variable length of the proof. This includes the leaf node in the case of an inclusion proof. There is no leaf node in the case of a non-inclusion proof. - pub depth: AssignedValue, - /// The non-leaf nodes of the mpt proof, resized to `max_depth - 1`. Each node has been parsed with both a hypothetical branch and extension node. The actual type is determined by the `node_type` flag. - /// - /// The actual variable length of `nodes` is `depth - 1` if `slot_is_empty == true` (excludes leaf node), otherwise `depth`. - pub nodes: Vec>, - /// The leaf parsed - pub leaf_parsed: LeafTraceWitness, - - /// Boolean indicating whether the MPT contains a value at `key` - pub slot_is_empty: AssignedValue, - - pub key_byte_len: usize, - pub max_depth: usize, - - /// The key fragments (nibbles), without encoding, provided as private inputs - pub key_frag: Vec>, - /// The hex-prefix encoded path for (potential) extension nodes (hex-prefix encoding has leaf vs. extension distinction). - /// These are derived from the nodes themselves. - pub key_frag_ext_bytes: Vec>, - /// The hex-prefix encoded path for (potential) leaf nodes (hex-prefix encoding has leaf vs. extension distinction). - /// These are derived from the nodes themselves. - pub key_frag_leaf_bytes: Vec>, - pub frag_lens: Vec>, - pub key_hexs: AssignedNibbles, -} - -impl ContainsParallelizableKeccakQueries for MPTFixedKeyProofWitness { - // Currently all indices are with respect to `keccak.var_len_queries` (see `EthChip::mpt_hash_phase0`). - fn shift_query_indices(&mut self, _: usize, shift: usize) { - self.leaf_parsed.hash_query_idx += shift; - for node in self.nodes.iter_mut() { - node.ext.hash_query_idx += shift; - node.branch.hash_query_idx += shift; - } - } -} - -#[derive(Clone, Debug, Hash, Serialize, Deserialize)] -/// The pre-assigned inputs for the MPT fixed key proof -pub struct MPTFixedKeyInput { - // claim specification: (path, value) - /// A Merkle-Patricia Trie is a mapping `path => value` - /// - /// As an example, the MPT state trie of Ethereum has - /// `path = keccak256(address) => value = rlp(account)` - pub path: H256, - pub value: Vec, - pub root_hash: H256, - - pub proof: Vec>, - - pub slot_is_empty: bool, - - pub value_max_byte_len: usize, - pub max_depth: usize, -} - -/* // TODO -#[derive(Clone, Debug)] -pub struct MPTVarKeyProof { - // claim specification - key_bytes: AssignedBytes, - key_byte_len: AssignedValue, - value_bytes: AssignedBytes, - value_byte_len: AssignedValue, - root_hash_bytes: AssignedBytes, - - // proof specification - leaf_bytes: AssignedBytes, - proof_nodes: Vec>, - node_types: Vec>, // index 0 = root; 0 = branch, 1 = extension - depth: AssignedValue, - - key_frag_hexs: Vec>, - // hex_len = 2 * byte_len + is_odd - 2 - key_frag_is_odd: Vec>, - key_frag_byte_len: Vec>, - - key_max_byte_len: usize, - value_max_byte_len: usize, - max_depth: usize, -} -*/ - -pub fn max_leaf_lens(max_key_bytes: usize, max_value_bytes: usize) -> (Vec, usize) { +pub const fn max_leaf_lens(max_key_bytes: usize, max_value_bytes: usize) -> ([usize; 2], usize) { let max_encoded_path_bytes = max_key_bytes + 1; - let max_encoded_path_rlp_bytes = - 1 + max_rlp_len_len(max_encoded_path_bytes) + max_encoded_path_bytes; - let max_value_rlp_bytes = 1 + max_rlp_len_len(max_value_bytes) + max_value_bytes; - let max_field_bytes = vec![max_encoded_path_rlp_bytes, max_value_rlp_bytes]; - let max_leaf_bytes: usize = - 1 + max_rlp_len_len(max_field_bytes.iter().sum()) + max_field_bytes.iter().sum::(); + let max_encoded_path_rlp_bytes = max_rlp_encoding_len(max_encoded_path_bytes); + let max_value_rlp_bytes = max_rlp_encoding_len(max_value_bytes); + let max_field_bytes = [max_encoded_path_rlp_bytes, max_value_rlp_bytes]; + let max_leaf_bytes = max_rlp_encoding_len(max_encoded_path_rlp_bytes + max_value_rlp_bytes); (max_field_bytes, max_leaf_bytes) } -pub fn max_ext_lens(max_key_bytes: usize) -> (Vec, usize) { +pub const fn max_ext_lens(max_key_bytes: usize) -> ([usize; 2], usize) { let max_node_ref_bytes = 32; let max_encoded_path_bytes = max_key_bytes + 1; - let max_encoded_path_rlp_bytes = - 1 + max_rlp_len_len(max_encoded_path_bytes) + max_encoded_path_bytes; - let max_node_ref_rlp_bytes = 1 + max_rlp_len_len(max_node_ref_bytes) + max_node_ref_bytes; - let max_field_bytes = vec![max_encoded_path_rlp_bytes, max_node_ref_rlp_bytes]; - let max_ext_bytes: usize = - 1 + max_rlp_len_len(max_field_bytes.iter().sum()) + max_field_bytes.iter().sum::(); + let max_encoded_path_rlp_bytes = max_rlp_encoding_len(max_encoded_path_bytes); + let max_node_ref_rlp_bytes = max_rlp_encoding_len(max_node_ref_bytes); + let max_field_bytes = [max_encoded_path_rlp_bytes, max_node_ref_rlp_bytes]; + let max_ext_bytes = max_rlp_encoding_len(max_encoded_path_rlp_bytes + max_node_ref_rlp_bytes); (max_field_bytes, max_ext_bytes) } -pub fn max_branch_lens() -> (Vec, usize) { +pub const fn max_branch_lens(max_vt_bytes: usize) -> ([usize; BRANCH_NUM_ITEMS], usize) { let max_node_ref_bytes = 32; - let max_node_ref_rlp_bytes = 1 + max_rlp_len_len(max_node_ref_bytes) + max_node_ref_bytes; - let mut max_field_bytes = vec![max_node_ref_rlp_bytes; 16]; - max_field_bytes.push(2); - let max_branch_bytes: usize = - 1 + max_rlp_len_len(max_field_bytes.iter().sum()) + max_field_bytes.iter().sum::(); + let max_node_ref_rlp_bytes = max_rlp_encoding_len(max_node_ref_bytes); + let mut max_field_bytes = [max_node_ref_rlp_bytes; BRANCH_NUM_ITEMS]; + max_field_bytes[BRANCH_NUM_ITEMS - 1] = max_rlp_encoding_len(max_vt_bytes); + let max_field_bytes_sum = 16 * max_node_ref_rlp_bytes + max_field_bytes[BRANCH_NUM_ITEMS - 1]; + let max_branch_bytes = max_rlp_encoding_len(max_field_bytes_sum); (max_field_bytes, max_branch_bytes) } /// Thread-safe chip for performing Merkle Patricia Trie (MPT) inclusion proofs. -// renaming MPTChip -> EthChip to avoid confusion #[derive(Clone, Debug)] -pub struct EthChip<'chip, F: Field> { - pub rlp: RlpChip<'chip, F>, - /// The Keccak RLCs will be available at the start of `SecondPhase`. These must be manually loaded. - keccak_rlcs: Option<(keccak::FixedLenRLCs, keccak::VarLenRLCs)>, - // we explicitly do not include KeccakChip in MPTChip because it is not thread-safe; the queries in KeccakChip must be added in a deterministic way, which is not guaranteed if parallelism is enabled +pub struct MPTChip<'r, F: Field> { + pub rlp: RlpChip<'r, F>, + pub keccak: &'r KeccakChip, } -impl<'chip, F: Field> EthChip<'chip, F> { - pub fn new( - rlp: RlpChip<'chip, F>, - keccak_rlcs: Option<(keccak::FixedLenRLCs, keccak::VarLenRLCs)>, - ) -> Self { - Self { rlp, keccak_rlcs } +impl<'r, F: Field> MPTChip<'r, F> { + pub fn new(rlp: RlpChip<'r, F>, keccak: &'r KeccakChip) -> Self { + Self { rlp, keccak } } pub fn gate(&self) -> &GateChip { @@ -319,29 +113,12 @@ impl<'chip, F: Field> EthChip<'chip, F> { self.rlp.rlc() } - pub fn rlp(&self) -> &RlpChip { - &self.rlp - } - - pub fn keccak_fixed_len_rlcs(&self) -> &keccak::FixedLenRLCs { - &self.keccak_rlcs.as_ref().expect("Keccak RLCs have not been loaded").0 + pub fn rlp(&self) -> RlpChip { + self.rlp } - pub fn keccak_var_len_rlcs(&self) -> &keccak::VarLenRLCs { - &self.keccak_rlcs.as_ref().expect("Keccak RLCs have not been loaded").1 - } - - fn ext_max_byte_len(max_key_bytes: usize) -> usize { - let max_node_ref_bytes = 32; - let max_encoded_path_bytes = max_key_bytes + 1; - let max_encoded_path_rlp_bytes = - 1 + max_rlp_len_len(max_encoded_path_bytes) + max_encoded_path_bytes; - let max_node_ref_rlp_bytes = 1 + max_rlp_len_len(max_node_ref_bytes) + max_node_ref_bytes; - let max_field_bytes = vec![max_encoded_path_rlp_bytes, max_node_ref_rlp_bytes]; - let max_ext_bytes: usize = 1 - + max_rlp_len_len(max_field_bytes.iter().sum()) - + max_field_bytes.iter().sum::(); - max_ext_bytes + pub fn keccak(&self) -> &'r KeccakChip { + self.keccak } /// When one node is referenced inside another node, what is included is H(rlp.encode(x)), where H(x) = keccak256(x) if len(x) >= 32 else x and rlp.encode is the RLP encoding function. @@ -349,28 +126,27 @@ impl<'chip, F: Field> EthChip<'chip, F> { /// Assumes that `bytes` is non-empty. pub fn mpt_hash_phase0( &self, - ctx: &mut Context, // ctx_gate in FirstPhase - keccak: &mut KeccakChip, // we explicitly do not include KeccakChip in MPTChip because it is not thread-safe; the queries in KeccakChip must be added in a deterministic way, which is not guaranteed if parallelism is enabled + ctx: &mut Context, // ctx_gate in FirstPhase bytes: AssignedBytes, len: AssignedValue, - ) -> usize { + ) -> MPTHashWitness { assert!(!bytes.is_empty()); - keccak.keccak_var_len(ctx, self.range(), bytes, None, len, 0usize) + self.keccak.keccak_var_len(ctx, bytes, len, 0usize) } /// When one node is referenced inside another node, what is included is H(rlp.encode(x)), where H(x) = keccak256(x) if len(x) >= 32 else x and rlp.encode is the RLP encoding function. /// We only return the RLC value of the MPT hash pub fn mpt_hash_phase1( &self, - ctx_gate: &mut Context, // ctx in SecondPhase - hash_query_idx: usize, - ) -> RlcVar { - let keccak_query = &self.keccak_var_len_rlcs()[hash_query_idx]; - let hash_rlc = keccak_query.1.rlc_val; - let input_rlc = keccak_query.0.rlc_val; - let len = keccak_query.0.len; - let max_len = std::cmp::max(keccak_query.0.max_len, 32); - let thirty_two = self.gate().get_field_element(32); + (ctx_gate, ctx_rlc): RlcContextPair, // ctxs in SecondPhase + keccak_query: &KeccakVarLenQuery, + ) -> MPTHashTrace { + // RLC of keccak(rlp.encode(x)) + let hash_bytes = keccak_query.output_bytes.as_ref().iter().copied(); + let hash_rlc = self.rlc().compute_rlc_fixed_len(ctx_rlc, hash_bytes); + let len = keccak_query.length; + let max_len = std::cmp::max(keccak_query.input_assigned.len(), 32); + let thirty_two = F::from(32); let is_short = self.range().is_less_than( ctx_gate, len, @@ -378,32 +154,99 @@ impl<'chip, F: Field> EthChip<'chip, F> { bit_length(max_len as u64), ); let mpt_hash_len = self.gate().select(ctx_gate, len, Constant(thirty_two), is_short); - let mpt_hash_rlc = self.gate().select(ctx_gate, input_rlc, hash_rlc, is_short); - RlcVar { rlc_val: mpt_hash_rlc, len: mpt_hash_len } + // input_assigned = rlp.encode(x), we then truncate to at most 32 bytes + let mut input_trunc = keccak_query.input_assigned.clone(); + input_trunc.truncate(32); + // Compute RLC(input_trunc) = RLC( (rlp.encode(x))[0..min(input_assigned.len, 32] ) + // We will only use this value if is_short = 1 + let input_rlc = + self.rlc().compute_rlc((ctx_gate, ctx_rlc), self.gate(), input_trunc, mpt_hash_len); + let mpt_hash_rlc = + self.gate().select(ctx_gate, input_rlc.rlc_val, hash_rlc.rlc_val, is_short); + MPTHashTrace { hash: hash_rlc, mpt_hash: RlcTrace::new(mpt_hash_rlc, mpt_hash_len, 32) } } /// Parse the RLP encoding of an assumed leaf node. /// Computes the keccak hash (or literal, see [`mpt_hash_phase0`]) of the node's RLP encoding. /// - /// This is the same as [`parse_ext_phase0`] except that the assumed maximum length of a leaf node + /// This is the same as the first part of [`parse_mpt_inclusion_phase0`] + /// except that the assumed maximum length of a leaf node /// may be different from that of an extension node. + pub fn parse_terminal_node_phase0( + &self, + ctx: &mut Context, + leaf_bytes: MPTNode, + max_key_bytes: usize, + max_value_bytes: usize, + ) -> TerminalWitness { + let (_, max_leaf_bytes) = max_leaf_lens(max_key_bytes, max_value_bytes); + let (_, max_ext_bytes) = max_ext_lens(max_key_bytes); + let max_ext_bytes = max(max_ext_bytes, MAX_BRANCH_ENCODING_BYTES); + let max_leaf_bytes = max(max_ext_bytes, max_leaf_bytes); + assert_eq!(leaf_bytes.rlp_bytes.len(), max_leaf_bytes); + + let [dummy_branch, dummy_ext] = + [DUMMY_BRANCH.clone(), DUMMY_EXT.clone()].map(|mut dummy| { + dummy.resize(max_leaf_bytes, 0u8); + dummy.into_iter().map(|b| Constant(F::from(b as u64))).collect_vec() + }); + + let (ext_in, branch_in): (AssignedBytes, AssignedBytes) = leaf_bytes + .rlp_bytes + .into_iter() + .zip(dummy_ext) + .zip(dummy_branch) + .map(|((node_byte, dummy_ext_byte), dummy_branch_byte)| { + ( + self.gate().select(ctx, node_byte, dummy_ext_byte, leaf_bytes.node_type), + self.gate().select(ctx, dummy_branch_byte, node_byte, leaf_bytes.node_type), + ) + }) + .unzip(); + + let ext_parsed = self.parse_leaf_phase0(ctx, ext_in, max_key_bytes, max_value_bytes); + let branch_parsed = { + assert_eq!(branch_in.len(), max_leaf_bytes); + + let rlp = + self.rlp.decompose_rlp_array_phase0(ctx, branch_in, &MAX_BRANCH_ITEM_LENS, false); + let hash_query = self.mpt_hash_phase0(ctx, rlp.rlp_array.clone(), rlp.rlp_len); + BranchWitness { rlp, hash_query } + }; + TerminalWitness { node_type: leaf_bytes.node_type, ext: ext_parsed, branch: branch_parsed } + } + + pub fn parse_terminal_node_phase1( + &self, + (ctx_gate, ctx_rlc): RlcContextPair, + witness: TerminalWitness, + ) -> TerminalTrace { + let ext = self.parse_leaf_phase1((ctx_gate, ctx_rlc), witness.ext); + // phase 1 parsing is the same for terminal or non-terminal branches, since the max length is already determined + let branch = self.parse_nonterminal_branch_phase1((ctx_gate, ctx_rlc), witness.branch); + TerminalTrace { node_type: witness.node_type, ext, branch } + } + pub fn parse_leaf_phase0( &self, ctx: &mut Context, - keccak: &mut KeccakChip, leaf_bytes: AssignedBytes, max_key_bytes: usize, max_value_bytes: usize, - ) -> LeafTraceWitness { + ) -> LeafWitness { let (max_field_bytes, max_leaf_bytes) = max_leaf_lens(max_key_bytes, max_value_bytes); + // for small values, max_ext_bytes may be larger than max_leaf_bytes + let (max_ext_field_bytes, max_ext_bytes) = max_ext_lens(max_key_bytes); + let max_ext_bytes = max(max_ext_bytes, MAX_BRANCH_ENCODING_BYTES); + let max_leaf_bytes = max(max_ext_bytes, max_leaf_bytes); + let max_field_bytes = [max_field_bytes[0], max(max_field_bytes[1], max_ext_field_bytes[1])]; assert_eq!(leaf_bytes.len(), max_leaf_bytes); - let rlp_witness = self.rlp.decompose_rlp_array_phase0(ctx, leaf_bytes, &max_field_bytes, false); // TODO: remove unnecessary clones somehow? - let hash_query_idx = - self.mpt_hash_phase0(ctx, keccak, rlp_witness.rlp_array.clone(), rlp_witness.rlp_len); - LeafTraceWitness { rlp: rlp_witness, hash_query_idx } + let hash_query = + self.mpt_hash_phase0(ctx, rlp_witness.rlp_array.clone(), rlp_witness.rlp_len); + LeafWitness { rlp: rlp_witness, hash_query } } /// Parse the RLP encoding of an assumed leaf node. @@ -411,13 +254,13 @@ impl<'chip, F: Field> EthChip<'chip, F> { pub fn parse_leaf_phase1( &self, (ctx_gate, ctx_rlc): RlcContextPair, - witness: LeafTraceWitness, + witness: LeafWitness, ) -> LeafTrace { let rlp_trace = self.rlp.decompose_rlp_array_phase1((ctx_gate, ctx_rlc), witness.rlp, false); let [key_path, value]: [RlpFieldTrace; 2] = rlp_trace.field_trace.try_into().unwrap(); - let hash_rlc = self.mpt_hash_phase1(ctx_gate, witness.hash_query_idx); - LeafTrace { key_path, value, hash_rlc } + let rlcs = self.mpt_hash_phase1((ctx_gate, ctx_rlc), &witness.hash_query); + LeafTrace { key_path, value, rlcs } } /// Parse the RLP encoding of an assumed extension node. @@ -425,20 +268,18 @@ impl<'chip, F: Field> EthChip<'chip, F> { pub fn parse_ext_phase0( &self, ctx: &mut Context, - keccak: &mut KeccakChip, ext_bytes: AssignedBytes, max_key_bytes: usize, - ) -> ExtensionTraceWitness { + ) -> ExtensionWitness { let (max_field_bytes, max_ext_bytes) = max_ext_lens(max_key_bytes); - let max_branch_bytes = MAX_BRANCH_LENS.1; - let max_ext_bytes = max(max_ext_bytes, max_branch_bytes); + let max_ext_bytes = max(max_ext_bytes, MAX_BRANCH_ENCODING_BYTES); assert_eq!(ext_bytes.len(), max_ext_bytes); let rlp_witness = self.rlp.decompose_rlp_array_phase0(ctx, ext_bytes, &max_field_bytes, false); - let hash_query_idx = - self.mpt_hash_phase0(ctx, keccak, rlp_witness.rlp_array.clone(), rlp_witness.rlp_len); - ExtensionTraceWitness { rlp: rlp_witness, hash_query_idx } + let hash_query = + self.mpt_hash_phase0(ctx, rlp_witness.rlp_array.clone(), rlp_witness.rlp_len); + ExtensionWitness { rlp: rlp_witness, hash_query } } /// Parse the RLP encoding of an assumed extension node. @@ -446,13 +287,13 @@ impl<'chip, F: Field> EthChip<'chip, F> { pub fn parse_ext_phase1( &self, (ctx_gate, ctx_rlc): RlcContextPair, - witness: ExtensionTraceWitness, + witness: ExtensionWitness, ) -> ExtensionTrace { let rlp_trace = self.rlp.decompose_rlp_array_phase1((ctx_gate, ctx_rlc), witness.rlp, false); let [key_path, node_ref]: [RlpFieldTrace; 2] = rlp_trace.field_trace.try_into().unwrap(); - let hash_rlc = self.mpt_hash_phase1(ctx_gate, witness.hash_query_idx); - ExtensionTrace { key_path, node_ref, hash_rlc } + let rlcs = self.mpt_hash_phase1((ctx_gate, ctx_rlc), &witness.hash_query); + ExtensionTrace { key_path, node_ref, rlcs } } /// Parse the RLP encoding of an assumed branch node. @@ -460,33 +301,30 @@ impl<'chip, F: Field> EthChip<'chip, F> { pub fn parse_nonterminal_branch_phase0( &self, ctx: &mut Context, - keccak: &mut KeccakChip, branch_bytes: AssignedBytes, max_key_bytes: usize, - ) -> BranchTraceWitness { - let max_field_bytes = &MAX_BRANCH_LENS.0; - let max_branch_bytes = MAX_BRANCH_LENS.1; + ) -> BranchWitness { let (_, max_ext_bytes) = max_ext_lens(max_key_bytes); - let max_branch_bytes = max(max_ext_bytes, max_branch_bytes); + let max_branch_bytes = max(max_ext_bytes, MAX_BRANCH_ENCODING_BYTES); assert_eq!(branch_bytes.len(), max_branch_bytes); let rlp_witness = - self.rlp.decompose_rlp_array_phase0(ctx, branch_bytes, max_field_bytes, false); - let hash_query_idx = - self.mpt_hash_phase0(ctx, keccak, rlp_witness.rlp_array.clone(), rlp_witness.rlp_len); - BranchTraceWitness { rlp: rlp_witness, hash_query_idx } + self.rlp.decompose_rlp_array_phase0(ctx, branch_bytes, &MAX_BRANCH_ITEM_LENS, false); + let hash_query = + self.mpt_hash_phase0(ctx, rlp_witness.rlp_array.clone(), rlp_witness.rlp_len); + BranchWitness { rlp: rlp_witness, hash_query } } pub fn parse_nonterminal_branch_phase1( &self, (ctx_gate, ctx_rlc): RlcContextPair, - witness: BranchTraceWitness, + witness: BranchWitness, ) -> BranchTrace { let rlp_trace = self.rlp.decompose_rlp_array_phase1((ctx_gate, ctx_rlc), witness.rlp, false); let node_refs: [RlpFieldTrace; 17] = rlp_trace.field_trace.try_into().unwrap(); - let hash_rlc = self.mpt_hash_phase1(ctx_gate, witness.hash_query_idx); - BranchTrace { node_refs, hash_rlc } + let rlcs = self.mpt_hash_phase1((ctx_gate, ctx_rlc), &witness.hash_query); + BranchTrace { node_refs, rlcs } } pub fn compute_rlc_trace( @@ -498,30 +336,31 @@ impl<'chip, F: Field> EthChip<'chip, F> { self.rlc().compute_rlc(ctx, self.gate(), inputs, len) } - pub fn parse_mpt_inclusion_fixed_key_phase0( + /* Inputs must follow the following unchecked constraints: + * keys must have positive length + * values must have length at least 32 + * trie has to be nonempty (root is not KECCAK_RLP_EMPTY_STRING) + */ + /// Loads input witnesses into the circuit and parses the RLP encoding of nodes and leaves + pub fn parse_mpt_inclusion_phase0( &self, ctx: &mut Context, - keccak: &mut KeccakChip, - proof: MPTFixedKeyProof, - ) -> MPTFixedKeyProofWitness { - let key_byte_len = proof.key_bytes.len(); + proof: MPTProof, + ) -> MPTProofWitness { + let max_key_byte_len = proof.max_key_byte_len; let value_max_byte_len = proof.value_bytes.len(); let max_depth = proof.max_depth; assert_eq!(proof.nodes.len(), max_depth - 1); assert_eq!(proof.root_hash_bytes.len(), 32); - - let ext_max_byte_len = Self::ext_max_byte_len(key_byte_len); - let branch_max_byte_len = MAX_BRANCH_LENS.1; - let node_max_byte_len = max(ext_max_byte_len, branch_max_byte_len); - - let mut dummy_ext = DUMMY_EXT.clone(); - dummy_ext.resize(node_max_byte_len, 0u8); - let mut dummy_branch = DUMMY_BRANCH.clone(); - dummy_branch.resize(node_max_byte_len, 0u8); - let dummy_ext: Vec<_> = - dummy_ext.into_iter().map(|b| Constant(F::from(b as u64))).collect(); - let dummy_branch: Vec<_> = - dummy_branch.into_iter().map(|b| Constant(F::from(b as u64))).collect(); + assert_eq!(proof.key_bytes.len(), max_key_byte_len); + let (_, ext_max_byte_len) = max_ext_lens(max_key_byte_len); + let node_max_byte_len = max(ext_max_byte_len, MAX_BRANCH_ENCODING_BYTES); + + let [dummy_branch, dummy_ext] = + [DUMMY_BRANCH.clone(), DUMMY_EXT.clone()].map(|mut dummy| { + dummy.resize(node_max_byte_len, 0u8); + dummy.into_iter().map(|b| Constant(F::from(b as u64))).collect_vec() + }); /* Validate inputs, check that: * all inputs are bytes @@ -537,12 +376,13 @@ impl<'chip, F: Field> EthChip<'chip, F> { .chain(proof.key_bytes.iter()) .chain(proof.value_bytes.iter()) .chain(proof.root_hash_bytes.iter()) - .chain(proof.leaf_bytes.iter()) + .chain(proof.leaf.rlp_bytes.iter()) .chain(proof.nodes.iter().flat_map(|node| node.rlp_bytes.iter())) { self.range().range_check(ctx, *byte, 8); } for bit in iter::once(&proof.slot_is_empty) + .chain(iter::once(&proof.leaf.node_type)) .chain(proof.nodes.iter().map(|node| &node.node_type)) .chain(proof.key_frag.iter().map(|frag| &frag.is_odd)) { @@ -553,18 +393,30 @@ impl<'chip, F: Field> EthChip<'chip, F> { } self.range().check_less_than_safe(ctx, proof.depth, max_depth as u64 + 1); self.range().check_less_than_safe(ctx, proof.value_byte_len, value_max_byte_len as u64 + 1); - for frag_len in proof.key_frag.iter().map(|frag| frag.byte_len) { - self.range().check_less_than_safe(ctx, frag_len, key_byte_len as u64 + 2); + if let Some(key_byte_len) = proof.key_byte_len { + self.range().check_less_than_safe(ctx, key_byte_len, max_key_byte_len as u64 + 1); + let two = ctx.load_constant(F::from(2u64)); + let frag_ub = self.gate().add(ctx, two, key_byte_len); + for frag_len in proof.key_frag.iter().map(|frag| frag.byte_len) { + self.range().check_less_than( + ctx, + frag_len, + frag_ub, + log2_ceil(max_key_byte_len as u64) + 2, + ); + } + } else { + for frag_len in proof.key_frag.iter().map(|frag| frag.byte_len) { + self.range().check_less_than_safe(ctx, frag_len, max_key_byte_len as u64 + 2); + } } - /* Parse RLP - * RLP Leaf for leaf_bytes + * RLP Terminal for leaf * RLP Extension for select(dummy_extension[idx], nodes[idx], node_types[idx]) * RLP Branch for select(nodes[idx], dummy_branch[idx], node_types[idx]) */ - let leaf_parsed = - self.parse_leaf_phase0(ctx, keccak, proof.leaf_bytes, key_byte_len, value_max_byte_len); - + let terminal_node = + self.parse_terminal_node_phase0(ctx, proof.leaf, max_key_byte_len, value_max_byte_len); let nodes: Vec<_> = proof .nodes .into_iter() @@ -583,27 +435,27 @@ impl<'chip, F: Field> EthChip<'chip, F> { }) .unzip(); - let ext_parsed = self.parse_ext_phase0(ctx, keccak, ext_in, key_byte_len); + let ext_parsed = self.parse_ext_phase0(ctx, ext_in, max_key_byte_len); let branch_parsed = - self.parse_nonterminal_branch_phase0(ctx, keccak, branch_in, key_byte_len); + self.parse_nonterminal_branch_phase0(ctx, branch_in, max_key_byte_len); MPTNodeWitness { node_type: node.node_type, ext: ext_parsed, branch: branch_parsed } }) .collect(); - // Check key fragment and prefix consistency let mut key_frag_ext_bytes = Vec::with_capacity(max_depth - 1); let mut key_frag_leaf_bytes = Vec::with_capacity(max_depth); let mut frag_lens = Vec::with_capacity(max_depth); // assert to avoid capacity checks? assert_eq!(proof.key_frag.len(), max_depth); + for (idx, key_frag) in proof.key_frag.iter().enumerate() { - assert_eq!(key_frag.nibbles.len(), 2 * key_byte_len); + assert_eq!(key_frag.nibbles.len(), 2 * max_key_byte_len); let leaf_path_bytes = hex_prefix_encode( ctx, self.gate(), &key_frag.nibbles, key_frag.is_odd, - key_byte_len, + max_key_byte_len, false, ); if idx < max_depth - 1 { @@ -624,7 +476,7 @@ impl<'chip, F: Field> EthChip<'chip, F> { frag_lens.push(frag_len); } - let mut key_hexs = Vec::with_capacity(2 * key_byte_len); + let mut key_hexs = Vec::with_capacity(2 * max_key_byte_len); for byte in proof.key_bytes.into_iter() { let bits = self.gate().num_to_bits(ctx, byte, 8); let hexs = [4, 0].map(|i| { @@ -636,32 +488,35 @@ impl<'chip, F: Field> EthChip<'chip, F> { }); key_hexs.extend(hexs); } - - MPTFixedKeyProofWitness { + MPTProofWitness { value_bytes: proof.value_bytes, value_byte_len: proof.value_byte_len, root_hash_bytes: proof.root_hash_bytes, + key_byte_len: proof.key_byte_len, depth: proof.depth, nodes, - key_frag: proof.key_frag, - key_byte_len, + terminal_node, + slot_is_empty: proof.slot_is_empty, + max_key_byte_len, max_depth, - leaf_parsed, + key_frag: proof.key_frag, key_frag_ext_bytes, key_frag_leaf_bytes, - key_hexs, frag_lens, - slot_is_empty: proof.slot_is_empty, + key_hexs, } } - pub fn parse_mpt_inclusion_fixed_key_phase1( + /// Checks constraints after the proof is parsed in phase 0 + pub fn parse_mpt_inclusion_phase1( &self, (ctx_gate, ctx_rlc): RlcContextPair, - mut witness: MPTFixedKeyProofWitness, + mut witness: MPTProofWitness, ) { + let gate = self.gate(); let max_depth = witness.max_depth; - let leaf_parsed = self.parse_leaf_phase1((ctx_gate, ctx_rlc), witness.leaf_parsed); + let terminal_node = + self.parse_terminal_node_phase1((ctx_gate, ctx_rlc), witness.terminal_node.clone()); let nodes: Vec> = witness .nodes .into_iter() @@ -685,61 +540,62 @@ impl<'chip, F: Field> EthChip<'chip, F> { .map(|(bytes, frag)| self.compute_rlc_trace((ctx_gate, ctx_rlc), bytes, frag.byte_len)) .collect(); let key_hexs = witness.key_hexs; - let slot_is_empty = witness.slot_is_empty; let slot_is_occupied = self.gate().not(ctx_gate, slot_is_empty); - - let depth_minus_one = self.gate().sub(ctx_gate, witness.depth, Constant(F::one())); - // depth_minus_one_indicator[idx] = (idx == depth - 1); this is used many times below - let depth_minus_one_indicator = - self.gate().idx_to_indicator(ctx_gate, depth_minus_one, max_depth); + let mut proof_is_empty = self.gate().is_zero(ctx_gate, witness.depth); + proof_is_empty = self.gate().and(ctx_gate, proof_is_empty, slot_is_empty); + // set `depth = 1` if proof is empty + let pseudo_depth = self.gate().add(ctx_gate, witness.depth, proof_is_empty); + let pseudo_depth_minus_one = self.gate().sub(ctx_gate, pseudo_depth, Constant(F::ONE)); + // pseudo_depth_minus_one_indicator[idx] = (idx == pseudo_depth - 1); this is used many times below + let pseudo_depth_minus_one_indicator = + self.gate().idx_to_indicator(ctx_gate, pseudo_depth_minus_one, max_depth); // Match fragments to node key for (((key_frag_ext_rlc, node), is_last), frag_len) in key_frag_ext_rlcs .into_iter() .zip(nodes.iter()) - .zip(depth_minus_one_indicator.iter()) + .zip(pseudo_depth_minus_one_indicator.iter()) .zip(witness.frag_lens.iter_mut()) { // When node is extension, check node key RLC equals key frag RLC - let mut node_key_is_equal = rlc_is_equal( + let node_key_path_rlc = rlc_select( ctx_gate, - self.gate(), + gate, + terminal_node.ext.key_path.field_trace, node.ext.key_path.field_trace, - key_frag_ext_rlc, + *is_last, ); + let node_type = + self.gate().select(ctx_gate, terminal_node.node_type, node.node_type, *is_last); + let mut node_key_is_equal = + rlc_is_equal(ctx_gate, self.gate(), node_key_path_rlc, key_frag_ext_rlc); // The key fragments must be equal unless either: // * node is not extension // * slot_is_empty and this is the last node - // If slot_is_empty && this is the last node && node is extension, then node key fragment must NOT equal key fragment (which is the last key fragment) // Reminder: node_type = 1 if extension, 0 if branch - let is_ext = node.node_type; - let is_branch = self.gate().not(ctx_gate, node.node_type); + let is_ext = node_type; + let is_branch = self.gate().not(ctx_gate, node_type); // is_ext ? node_key_is_equal : 1 node_key_is_equal = self.gate().mul_add(ctx_gate, node_key_is_equal, is_ext, is_branch); // !is_last || !is_ext = !(is_last && is_ext) = 1 - is_last * is_ext - let mut expected = { - let val = F::one() - *is_last.value() * is_ext.value(); - ctx_gate.assign_region( - [Witness(val), Existing(*is_last), Existing(is_ext), Constant(F::one())], - [0], - ); - ctx_gate.get(-4) - }; + let mut expected = self.gate().sub_mul(ctx_gate, Constant(F::ONE), *is_last, is_ext); // (slot_is_empty ? !(is_last && is_ext) : 1), we cache slot_is_occupied as an optimization - expected = self.gate().mul_add(ctx_gate, expected, slot_is_empty, slot_is_occupied); + let is_not_last = self.gate().not(ctx_gate, *is_last); + let slot_is_occupied_expected = + self.gate().and(ctx_gate, slot_is_occupied, is_not_last); + expected = + self.gate().mul_add(ctx_gate, expected, slot_is_empty, slot_is_occupied_expected); // assuming node type is not extension if idx > pf.len() [we don't care what happens for these idx] ctx_gate.constrain_equal(&node_key_is_equal, &expected); // We enforce that the frag_len is 1 if the node is a branch, unless it is the last node (idx = depth - 1) - // This check is only necessary if slot_is_empty; otherwise, the fixed key length and overall concatenation check will enforce this + // This check is only necessary if slot_is_empty; otherwise, the key length and overall concatenation check will enforce this let is_branch_and_not_last = self.gate().mul_not(ctx_gate, *is_last, is_branch); *frag_len = - self.gate().select(ctx_gate, Constant(F::one()), *frag_len, is_branch_and_not_last); + self.gate().select(ctx_gate, Constant(F::ONE), *frag_len, is_branch_and_not_last); } - - // Question for auditers: is the following necessary? // match hex-prefix encoding of leaf path (gotten from witness.key_frag) to the parsed leaf encoded path // ignore leaf if slot_is_empty { @@ -747,26 +603,35 @@ impl<'chip, F: Field> EthChip<'chip, F> { ctx_gate, self.gate(), key_frag_leaf_rlcs, - depth_minus_one_indicator.clone(), + pseudo_depth_minus_one_indicator.clone(), ); let mut check = rlc_is_equal( ctx_gate, self.gate(), leaf_encoded_path_rlc, - leaf_parsed.key_path.field_trace, + terminal_node.ext.key_path.field_trace, ); check = self.gate().or(ctx_gate, check, slot_is_empty); - self.gate().assert_is_const(ctx_gate, &check, &F::one()); + self.gate().assert_is_const(ctx_gate, &check, &F::ONE); } // Check key fragments concatenate to key using hex RLC // We supply witness key fragments so this check passes even if the slot is empty let fragment_first_nibbles = { - let key_hex_rlc = self.rlc().compute_rlc_fixed_len(ctx_rlc, key_hexs); + let key_hex_rlc = if let Some(key_byte_len) = witness.key_byte_len { + // key_hex_len = 2 * key_byte_len + let key_hex_len = self.gate().add(ctx_gate, key_byte_len, key_byte_len); + self.rlc().compute_rlc((ctx_gate, ctx_rlc), self.gate(), key_hexs, key_hex_len) + } else { + let RlcFixedTrace { rlc_val, len: max_len } = + self.rlc().compute_rlc_fixed_len(ctx_rlc, key_hexs); + let len = ctx_gate.load_constant(F::from(max_len as u64)); + RlcTrace { rlc_val, len, max_len } + }; let (fragment_rlcs, fragment_first_nibbles): (Vec<_>, Vec<_>) = witness .key_frag .into_iter() - .zip(witness.frag_lens.into_iter()) + .zip(witness.frag_lens) .map(|(key_frag, frag_len)| { let first_nibble = key_frag.nibbles[0]; ( @@ -780,26 +645,20 @@ impl<'chip, F: Field> EthChip<'chip, F> { ) }) .unzip(); - self.rlc().load_rlc_cache( (ctx_gate, ctx_rlc), self.gate(), - bit_length(2 * witness.key_byte_len as u64), + bit_length(2 * witness.max_key_byte_len as u64), ); - let key_len = - ctx_gate.load_constant(self.gate().get_field_element(key_hex_rlc.len as u64)); - - self.rlc().constrain_rlc_concat_var( + self.rlc().constrain_rlc_concat( ctx_gate, self.gate(), - fragment_rlcs.into_iter().map(|f| (f.rlc_val, f.len, f.max_len)), - (&key_hex_rlc.rlc_val, &key_len), - witness.depth, + fragment_rlcs, + &key_hex_rlc, + Some(pseudo_depth), ); - fragment_first_nibbles }; - /* Check value matches. Currently value_bytes is RLC encoded * and value_byte_len is the RLC encoding's length */ @@ -807,20 +666,23 @@ impl<'chip, F: Field> EthChip<'chip, F> { let value_rlc_trace = self.rlp.rlc().compute_rlc( (ctx_gate, ctx_rlc), self.gate(), - witness.value_bytes, + witness.value_bytes.clone(), witness.value_byte_len, ); // value doesn't matter if slot is empty; by default we will make leaf.value = 0 in that case + let branch_value_trace = terminal_node.branch.node_refs[16].field_trace; + let value_trace = rlc_select( + ctx_gate, + self.gate(), + terminal_node.ext.value.field_trace, + branch_value_trace, + terminal_node.node_type, + ); let value_equals_leaf = - rlc_is_equal(ctx_gate, self.gate(), value_rlc_trace, leaf_parsed.value.field_trace); + rlc_is_equal(ctx_gate, self.gate(), value_rlc_trace, value_trace); let value_check = self.gate().or(ctx_gate, value_equals_leaf, slot_is_empty); - self.gate().assert_is_const(ctx_gate, &value_check, &F::one()); + self.gate().assert_is_const(ctx_gate, &value_check, &F::ONE); } - - // if proof_is_empty, then root_hash = keccak(rlp("")) = keccak(0x80) = 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421 - let mut proof_is_empty = self.gate().is_zero(ctx_gate, witness.depth); - proof_is_empty = self.gate().and(ctx_gate, proof_is_empty, slot_is_empty); - /* Check hash chains: Recall that nodes.len() = slot_is_empty ? depth : depth - 1 @@ -832,11 +694,11 @@ impl<'chip, F: Field> EthChip<'chip, F> { if slot_is_empty: we assume that depth < max_depth: if depth == max_depth, then we set hash(nodes[depth - 1]) := 0. The circuit will try to prove 0 in nodes[max_depth - 2], which will either fail or (succeed => still shows MPT does not include `key`) if proof_is_empty: - + then root_hash = keccak(rlp("")) = keccak(0x80) = 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421 */ + // if proof_is_empty, then root_hash = keccak(rlp("")) = keccak(0x80) = 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421 let root_hash_rlc = self.rlc().compute_rlc_fixed_len(ctx_rlc, witness.root_hash_bytes); - let thirty_two = ctx_gate.load_constant(self.gate().get_field_element(32)); let null_root_rlc = { let keccak_rlp_null = KECCAK_RLP_EMPTY_STRING .iter() @@ -847,53 +709,166 @@ impl<'chip, F: Field> EthChip<'chip, F> { let root_is_null = self.gate().is_equal(ctx_gate, root_hash_rlc.rlc_val, null_root_rlc.rlc_val); - let mut matches = Vec::with_capacity(max_depth - 1); - let mut branch_refs_are_null = Vec::with_capacity(max_depth - 1); + let mut matches = Vec::with_capacity(max_depth); + let mut branch_refs_are_null = Vec::with_capacity(max_depth); // assert so later array indexing doesn't do bound check assert_eq!(nodes.len(), max_depth - 1); + + // makes match_sums[idx] = idx later on + matches.push(ctx_gate.load_constant(F::ZERO)); + + let pseudo_depth_indicator = + self.gate().idx_to_indicator(ctx_gate, pseudo_depth, max_depth); + + let leaf_hash_rlc = terminal_node.mpt_hash_rlc(ctx_gate, self.gate()); // TODO: maybe use rust iterators instead here, would make it harder to read though + for idx in 0..max_depth { - // `node_hash_rlc` can be viewed as a fixed length RLC - let mut node_hash_rlc = leaf_parsed.hash_rlc; - if idx < max_depth - 1 { - node_hash_rlc = rlc_select( - ctx_gate, - self.gate(), - nodes[idx].ext.hash_rlc, - nodes[idx].branch.hash_rlc, - nodes[idx].node_type, - ); - // is_leaf = (idx == depth - 1) && !slot_is_empty - let is_last = depth_minus_one_indicator[idx]; - let is_leaf = self.gate().mul_not(ctx_gate, slot_is_empty, is_last); - node_hash_rlc = - rlc_select(ctx_gate, self.gate(), leaf_parsed.hash_rlc, node_hash_rlc, is_leaf); - } if idx == 0 { // if !proof_is_empty: // check hash(nodes[0]) == root_hash // else: // check root_hash == keccak(rlp("")) - let mut root_check = rlc_is_equal( - ctx_gate, - self.gate(), - node_hash_rlc, - RlcVar { rlc_val: root_hash_rlc.rlc_val, len: thirty_two }, - ); + + // The root_hash is always the keccak hash of the root node. + // However `node_hash_rlc` above is the `mpt_hash` of the node, which could be + // just the RLP of the node itself is its length is less than 32 bytes. + // Therefore we have to specially extract the actual hash (denoted _hash32) in this case below + let leaf_hash32_rlc = terminal_node.keccak_rlc(ctx_gate, self.gate()); + let node_hash32_rlc = if idx < max_depth - 1 { + let node_hash32_rlc = nodes[idx].keccak_rlc(ctx_gate, self.gate()); + // is_last = (idx == pseudo_depth - 1) + let is_last = pseudo_depth_minus_one_indicator[idx]; + //self.gate().mul_not(ctx_gate, slot_is_empty, is_last); + self.gate().select(ctx_gate, leaf_hash32_rlc, node_hash32_rlc, is_last) + } else { + leaf_hash32_rlc + }; + let mut root_check = + self.gate().is_equal(ctx_gate, node_hash32_rlc, root_hash_rlc.rlc_val); root_check = self.gate().select(ctx_gate, root_is_null, root_check, proof_is_empty); - self.gate().assert_is_const(ctx_gate, &root_check, &F::one()); + self.gate().assert_is_const(ctx_gate, &root_check, &F::ONE); } else { + // we check that the mpt hash of a node matches its reference in a previous node + // since `terminal_node` is stored separately, we always need extra handling for it, with a select + let mut node_hash_rlc = leaf_hash_rlc; + if idx < nodes.len() { + node_hash_rlc = nodes[idx].mpt_hash_rlc(ctx_gate, self.gate()); + // is_last = (idx == pseudo_depth - 1) + let is_last = pseudo_depth_minus_one_indicator[idx]; + node_hash_rlc = + rlc_select(ctx_gate, self.gate(), leaf_hash_rlc, node_hash_rlc, is_last); + } + let prev_is_last = pseudo_depth_indicator[idx]; + // Get the previous node. there are three types to consider: + // - extension + // - branch + // - terminal branch + // - terminal branch should be used instead of branch if `prev_is_last == true` + // In each case, if the current node is extension/leaf AND the value of the current node is small enough, + // then the previous node contains the RLP of the current node as a 2-item list, and not as a hash (byte string). + // We therefore need to parse the node reference in two ways to account for this. + // This condition is equivalent to when `node_hash_rlc` is actually not the keccak of the node, but the RLP of the node itself. let ext_ref_rlc = nodes[idx - 1].ext.node_ref.field_trace; - let branch_ref_rlc = rlc_select_from_idx( + let ext_ref_rlp_rlc = nodes[idx - 1].ext.node_ref.rlp_trace; + let nibble = fragment_first_nibbles[idx - 1]; + let [(branch_ref_rlc, branch_ref_rlp_rlc), (terminal_branch_ref_rlc, terminal_branch_ref_rlp_rlc)] = + [&nodes[idx - 1].branch.node_refs, &terminal_node.branch.node_refs].map( + |node_refs| { + // the RLC of the decoded node reference, assuming it's a byte string + let ref_rlc = rlc_select_from_idx( + ctx_gate, + self.gate(), + node_refs.iter().map(|node| node.field_trace), + nibble, + ); + // the RLC of the RLP encoding of the node reference + let ref_rlp_rlc = rlc_select_from_idx( + ctx_gate, + self.gate(), + node_refs.iter().map(|node| node.rlp_trace), + nibble, + ); + (ref_rlc, ref_rlp_rlc) + }, + ); + let mut get_maybe_short_ref_rlc = + |ref_rlc: RlcVar, ref_rlp_rlc: RlcVar| -> RlcVar { + let is_short = self.range().is_less_than_safe(ctx_gate, ref_rlc.len, 32); + let is_null = self.range().is_less_than_safe(ctx_gate, ref_rlc.len, 2); + let is_not_hash = self.gate().mul_not(ctx_gate, is_null, is_short); + rlc_select(ctx_gate, self.gate(), ref_rlp_rlc, ref_rlc, is_not_hash) + }; + let mut branch_ref_rlc = + get_maybe_short_ref_rlc(branch_ref_rlc, branch_ref_rlp_rlc); + let ext_ref_rlc = + get_maybe_short_ref_rlc(ext_ref_rlc.into(), ext_ref_rlp_rlc.into()); + let terminal_branch_ref_rlc = + get_maybe_short_ref_rlc(terminal_branch_ref_rlc, terminal_branch_ref_rlp_rlc); + branch_ref_rlc = rlc_select( + ctx_gate, + self.gate(), + terminal_branch_ref_rlc, + branch_ref_rlc, + prev_is_last, + ); + let branch_ref_rlp_rlc = rlc_select_from_idx( + ctx_gate, + self.gate(), + nodes[idx - 1].branch.node_refs.iter().map(|node| node.rlp_trace), + fragment_first_nibbles[idx - 1], + ); + let mut terminal_branch_ref_rlc = rlc_select_from_idx( ctx_gate, self.gate(), - nodes[idx - 1].branch.node_refs.iter().map(|node| node.field_trace), + terminal_node.branch.node_refs.iter().map(|node| node.field_trace), fragment_first_nibbles[idx - 1], ); + let terminal_branch_ref_rlp_rlc = rlc_select_from_idx( + ctx_gate, + self.gate(), + terminal_node.branch.node_refs.iter().map(|node| node.rlp_trace), + fragment_first_nibbles[idx - 1], + ); + let is_short = self.range().is_less_than_safe(ctx_gate, branch_ref_rlc.len, 32); + let is_null = self.range().is_less_than_safe(ctx_gate, branch_ref_rlc.len, 2); + let is_not_null = self.gate().not(ctx_gate, is_null); + let is_not_hash = self.gate().and(ctx_gate, is_short, is_not_null); + branch_ref_rlc = rlc_select( + ctx_gate, + self.gate(), + branch_ref_rlp_rlc, + branch_ref_rlc, + is_not_hash, + ); + let is_short = self.range().is_less_than_safe(ctx_gate, ext_ref_rlc.len, 32); + let is_null = self.range().is_less_than_safe(ctx_gate, ext_ref_rlc.len, 2); + let is_not_null = self.gate().not(ctx_gate, is_null); + let is_not_hash = self.gate().and(ctx_gate, is_short, is_not_null); + let ext_ref_rlc = + rlc_select(ctx_gate, self.gate(), ext_ref_rlp_rlc, ext_ref_rlc, is_not_hash); + let is_short = + self.range().is_less_than_safe(ctx_gate, terminal_branch_ref_rlc.len, 32); + let is_null = + self.range().is_less_than_safe(ctx_gate, terminal_branch_ref_rlc.len, 2); + let is_not_null = self.gate().not(ctx_gate, is_null); + let is_not_hash = self.gate().and(ctx_gate, is_short, is_not_null); + terminal_branch_ref_rlc = rlc_select( + ctx_gate, + self.gate(), + terminal_branch_ref_rlp_rlc, + terminal_branch_ref_rlc, + is_not_hash, + ); + branch_ref_rlc = rlc_select( + ctx_gate, + self.gate(), + terminal_branch_ref_rlc, + branch_ref_rlc, + prev_is_last, + ); // branch_ref_rlc should equal NULL = "" (empty string) if slot_is_empty and idx == depth and nodes[idx - 1] is a branch node; we save these checks for all idx and `select` for `depth` later - let mut branch_ref_is_null = self.gate().is_zero(ctx_gate, branch_ref_rlc.len); - branch_ref_is_null = - self.gate().or(ctx_gate, branch_ref_is_null, nodes[idx - 1].node_type); + let branch_ref_is_null = self.gate().is_zero(ctx_gate, branch_ref_rlc.len); branch_refs_are_null.push(branch_ref_is_null); // the node that nodes[idx - 1] actually points to @@ -902,7 +877,7 @@ impl<'chip, F: Field> EthChip<'chip, F> { self.gate(), ext_ref_rlc, branch_ref_rlc, - nodes[idx - 1].node_type, + nodes[idx - 1].node_type, // does not need terminal_node.node_type because match_cnt ignores idx >= depth ); if idx == max_depth - 1 { // if slot_is_empty: we set hash(nodes[max_depth - 1]) := 0 to rule out the case depth == max_depth @@ -913,49 +888,34 @@ impl<'chip, F: Field> EthChip<'chip, F> { matches.push(is_match); } } + // padding by 0 to avoid empty vector + branch_refs_are_null.push(ctx_gate.load_constant(F::from(0))); // constrain hash chain { let match_sums = self.gate().partial_sums(ctx_gate, matches.iter().copied()).collect_vec(); let match_cnt = self.gate().select_by_indicator( ctx_gate, - once(Constant(F::zero())).chain(match_sums.into_iter().map(Existing)), - depth_minus_one_indicator.clone(), + match_sums.into_iter().map(Existing), + pseudo_depth_minus_one_indicator.clone(), ); - ctx_gate.constrain_equal(&match_cnt, &depth_minus_one); + ctx_gate.constrain_equal(&match_cnt, &pseudo_depth_minus_one); } // if slot_is_empty: check that nodes[depth - 1] points to null if it is branch node { let mut branch_ref_check = self.gate().select_by_indicator( ctx_gate, branch_refs_are_null.into_iter().map(Existing), - depth_minus_one_indicator, + pseudo_depth_minus_one_indicator, ); + branch_ref_check = self.gate().or(ctx_gate, branch_ref_check, terminal_node.node_type); branch_ref_check = - self.gate().select(ctx_gate, branch_ref_check, Constant(F::one()), slot_is_empty); + self.gate().select(ctx_gate, branch_ref_check, Constant(F::ONE), slot_is_empty); // nothing to check if proof is empty branch_ref_check = self.gate().or(ctx_gate, branch_ref_check, proof_is_empty); - self.gate().assert_is_const(ctx_gate, &branch_ref_check, &F::one()); + self.gate().assert_is_const(ctx_gate, &branch_ref_check, &F::ONE); } } - - /* - pub fn parse_mpt_inclusion_var_key( - &self, - _ctx: &mut Context, - _range: &RangeConfig, - proof: &MPTVarKeyProof, - key_max_byte_len: usize, - value_max_byte_len: usize, - max_depth: usize, - ) { - assert_eq!(proof.key_max_byte_len, key_max_byte_len); - assert_eq!(proof.value_max_byte_len, value_max_byte_len); - assert_eq!(proof.max_depth, max_depth); - - todo!() - } - */ } /// # Assumptions @@ -967,8 +927,8 @@ pub fn hex_prefix_encode_first( is_odd: AssignedValue, is_ext: bool, ) -> AssignedValue { - let sixteen = gate.get_field_element(16); - let thirty_two = gate.get_field_element(32); + let sixteen = F::from(16); + let thirty_two = F::from(32); if is_ext { gate.inner_product( ctx, @@ -978,20 +938,8 @@ pub fn hex_prefix_encode_first( } else { // (1 - is_odd) * 32 + is_odd * (48 + x_0) // | 32 | 16 | is_odd | 32 + 16 * is_odd | is_odd | x_0 | out | - let pre_val = thirty_two + sixteen * is_odd.value(); - let val = pre_val + *first_nibble.value() * is_odd.value(); - ctx.assign_region_last( - [ - Constant(thirty_two), - Constant(sixteen), - Existing(is_odd), - Witness(pre_val), - Existing(is_odd), - Existing(first_nibble), - Witness(val), - ], - [0, 3], - ) + let tmp = gate.mul_add(ctx, Constant(sixteen), is_odd, Constant(thirty_two)); + gate.mul_add(ctx, is_odd, first_nibble, tmp) } } @@ -1006,7 +954,7 @@ pub fn hex_prefix_encode( is_ext: bool, ) -> AssignedBytes { let mut path_bytes = Vec::with_capacity(key_byte_len); - let sixteen = gate.get_field_element(16); + let sixteen = F::from(16); for byte_idx in 0..=key_byte_len { if byte_idx == 0 { let byte = hex_prefix_encode_first(ctx, gate, key_frag_hexs[0], is_odd, is_ext); @@ -1018,7 +966,7 @@ pub fn hex_prefix_encode( Existing(key_frag_hexs[2 * byte_idx - 1 - is_even]), Constant(sixteen), if is_even == 0 && byte_idx >= key_byte_len { - Constant(F::zero()) + Constant(F::ZERO) } else { Existing(key_frag_hexs[2 * byte_idx - is_even]) }, @@ -1037,7 +985,7 @@ pub fn hex_prefix_len( key_frag_byte_len: AssignedValue, is_odd: AssignedValue, ) -> AssignedValue { - let two = gate.get_field_element(2); + let two = F::from(2); let pre_val = two * key_frag_byte_len.value() + is_odd.value(); // 2 * key_frag_byte_len + is_odd - 2 let val = pre_val - two; @@ -1048,151 +996,20 @@ pub fn hex_prefix_len( Existing(key_frag_byte_len), Witness(pre_val), Constant(-two), - Constant(F::one()), + Constant(F::ONE), Witness(val), ], [0, 3], ); let byte_len_is_zero = gate.is_zero(ctx, key_frag_byte_len); // TODO: should we constrain is_odd to be 0 when is_zero = 1? - gate.select(ctx, Constant(F::zero()), hex_len, byte_len_is_zero) + gate.select(ctx, Constant(F::ZERO), hex_len, byte_len_is_zero) } -impl MPTFixedKeyInput { - pub fn assign(self, ctx: &mut Context) -> MPTFixedKeyProof { - let Self { - path, - mut value, - root_hash, - mut proof, - value_max_byte_len, - max_depth, - slot_is_empty, - } = self; - let depth = proof.len(); - let key_byte_len = 32; - //assert!(depth <= max_depth - usize::from(slot_is_empty)); - - let bytes_to_nibbles = |bytes: &[u8]| { - let mut nibbles = Vec::with_capacity(bytes.len() * 2); - for byte in bytes { - nibbles.push(byte >> 4); - nibbles.push(byte & 0xf); - } - nibbles - }; - let hex_len = |byte_len: usize, is_odd: bool| 2 * byte_len + usize::from(is_odd) - 2; - - let path_nibbles = bytes_to_nibbles(path.as_bytes()); - let mut path_idx = 0; - - // below "key" and "path" are used interchangeably, sorry for confusion - // if slot_is_empty, leaf is dummy, but with value 0x0 to make constraints pass (assuming claimed value is also 0x0) - let mut leaf = if slot_is_empty { NULL_LEAF.clone() } else { proof.pop().unwrap() }; - let (_, max_leaf_bytes) = max_leaf_lens(key_byte_len, value_max_byte_len); - - let (_, max_ext_bytes) = max_ext_lens(key_byte_len); - let max_branch_bytes = MAX_BRANCH_LENS.1; - let max_node_bytes = max(max_ext_bytes, max_branch_bytes); - - let mut key_frag = Vec::with_capacity(max_depth); - let mut nodes = Vec::with_capacity(max_depth - 1); - let mut process_node = |node: &[u8]| { - let decode = Rlp::new(node); - let node_type = decode.item_count().unwrap() == 2; - if node_type { - let encoded_path = decode.at(0).unwrap().data().unwrap(); - let byte_len = encoded_path.len(); - let encoded_nibbles = bytes_to_nibbles(encoded_path); - let is_odd = encoded_nibbles[0] == 1u8 || encoded_nibbles[0] == 3u8; - let mut frag = encoded_nibbles[2 - usize::from(is_odd)..].to_vec(); - path_idx += frag.len(); - frag.resize(2 * key_byte_len, 0); - key_frag.push((frag, byte_len, is_odd)); - } else { - let mut frag = vec![0u8; 2 * key_byte_len]; - frag[0] = path_nibbles[path_idx]; - key_frag.push((frag, 1, true)); - path_idx += 1; - } - node_type - }; - for mut node in proof { - let node_type = process_node(&node); - node.resize(max_node_bytes, 0); - nodes.push((node, node_type)); - } - let mut dummy_branch = DUMMY_BRANCH.clone(); - dummy_branch.resize(max_node_bytes, 0); - nodes.resize(max_depth - 1, (dummy_branch, false)); - - process_node(&leaf); - leaf.resize(max_leaf_bytes, 0); - - // if slot_is_empty, we modify key_frag so it still concatenates to `path` - if slot_is_empty { - // remove just added leaf frag - key_frag.pop().unwrap(); - if key_frag.is_empty() { - let nibbles = bytes_to_nibbles(path.as_bytes()); // that means proof was empty - key_frag = vec![(nibbles, 33, false)]; - } else { - // the last frag in non-inclusion doesn't match path - key_frag.pop().unwrap(); - let hex_len = key_frag - .iter() - .map(|(_, byte_len, is_odd)| hex_len(*byte_len, *is_odd)) - .sum::(); - let mut remaining = path_nibbles[hex_len..].to_vec(); - let is_odd = remaining.len() % 2 == 1; - let byte_len = (remaining.len() + 2 - usize::from(is_odd)) / 2; - remaining.resize(2 * key_byte_len, 0); - key_frag.push((remaining, byte_len, is_odd)); - } - } - key_frag.resize(max_depth, (vec![0u8; 2 * key_byte_len], 0, false)); - - // assign all values - let value_byte_len = ctx.load_witness(F::from(value.len() as u64)); - let depth = ctx.load_witness(F::from(depth as u64)); - let mut load_bytes = - |bytes: &[u8]| ctx.assign_witnesses(bytes.iter().map(|x| F::from(*x as u64))); - let key_bytes = load_bytes(path.as_bytes()); - value.resize(value_max_byte_len, 0); - let value_bytes = load_bytes(&value); - let root_hash_bytes = load_bytes(root_hash.as_bytes()); - let leaf_bytes = load_bytes(&leaf); - let nodes = nodes - .into_iter() - .map(|(node_bytes, node_type)| { - let rlp_bytes = ctx.assign_witnesses(node_bytes.iter().map(|x| F::from(*x as u64))); - let node_type = ctx.load_witness(F::from(node_type)); - MPTNode { rlp_bytes, node_type } - }) - .collect_vec(); - let key_frag = key_frag - .into_iter() - .map(|(nibbles, byte_len, is_odd)| { - let nibbles = ctx.assign_witnesses(nibbles.iter().map(|x| F::from(*x as u64))); - let byte_len = ctx.load_witness(F::from(byte_len as u64)); - let is_odd = ctx.load_witness(F::from(is_odd)); - MPTKeyFragment { nibbles, is_odd, byte_len } - }) - .collect_vec(); - - let slot_is_empty = ctx.load_witness(F::from(slot_is_empty)); - - MPTFixedKeyProof { - key_bytes, - value_bytes, - value_byte_len, - root_hash_bytes, - leaf_bytes, - nodes, - depth, - key_frag, - max_depth, - slot_is_empty, - } - } +#[test] +fn test_dummy_branch() { + assert_eq!( + ::rlp::encode_list::, Vec>(&vec![vec![]; 17]).as_ref(), + &DUMMY_BRANCH[..] + ); } diff --git a/axiom-eth/src/mpt/tests.rs b/axiom-eth/src/mpt/tests.rs deleted file mode 100644 index 1a7ee420..00000000 --- a/axiom-eth/src/mpt/tests.rs +++ /dev/null @@ -1,231 +0,0 @@ -use super::*; -use crate::{ - halo2_proofs::{ - dev::MockProver, - halo2curves::bn256::{Bn256, Fr, G1Affine}, - poly::commitment::ParamsProver, - poly::kzg::{ - commitment::KZGCommitmentScheme, - multiopen::{ProverSHPLONK, VerifierSHPLONK}, - strategy::SingleStrategy, - }, - transcript::{ - Blake2bRead, Blake2bWrite, Challenge255, TranscriptReadBuffer, TranscriptWriterBuffer, - }, - }, - keccak::{FixedLenRLCs, FnSynthesize, KeccakCircuitBuilder, VarLenRLCs}, - rlp::builder::RlcThreadBuilder, - util::EthConfigParams, -}; -use ark_std::{end_timer, start_timer}; -use ethers_core::utils::keccak256; -use halo2_base::{ - halo2_proofs::plonk::{create_proof, keygen_pk, keygen_vk, verify_proof}, - utils::fs::gen_srs, -}; -use hex::FromHex; -use rand_core::OsRng; -use std::{ - cell::RefCell, - env::{set_var, var}, - fs::File, - io::{BufReader, Write}, - path::Path, -}; -use test_log::test; - -fn test_mpt_circuit( - k: u32, - mut builder: RlcThreadBuilder, - inputs: MPTFixedKeyInput, -) -> KeccakCircuitBuilder> { - let prover = builder.witness_gen_only(); - let range = RangeChip::default(8); - let mut keccak = KeccakChip::default(); - let mpt = EthChip::new(RlpChip::new(&range, None), None); - let ctx = builder.gate_builder.main(0); - let mpt_proof = inputs.assign(ctx); - let mpt_witness = mpt.parse_mpt_inclusion_fixed_key_phase0(ctx, &mut keccak, mpt_proof); - - let circuit = KeccakCircuitBuilder::new( - builder, - RefCell::new(keccak), - range, - None, - move |builder: &mut RlcThreadBuilder, - rlp: RlpChip, - keccak_rlcs: (FixedLenRLCs, VarLenRLCs)| { - // hard to tell rust that &range lives long enough, so just remake the MPTChip - let mut mpt = EthChip::new(rlp, None); - mpt.keccak_rlcs = Some(keccak_rlcs); - let (ctx_gate, ctx_rlc) = builder.rlc_ctx_pair(); - mpt.parse_mpt_inclusion_fixed_key_phase1((ctx_gate, ctx_rlc), mpt_witness); - }, - ); - if !prover { - let unusable_rows = - var("UNUSABLE_ROWS").unwrap_or_else(|_| "109".to_string()).parse().unwrap(); - circuit.config(k as usize, Some(unusable_rows)); - } - circuit -} - -fn from_hex(s: &str) -> Vec { - let s = if s.len() % 2 == 1 { format!("0{s}") } else { s.to_string() }; - Vec::from_hex(s).unwrap() -} - -// The input file is generated by running `query_test.sh` in the `scripts/input_gen` directory of this repo -fn mpt_input(path: impl AsRef, slot_is_empty: bool, max_depth: usize) -> MPTFixedKeyInput { - /*let block: serde_json::Value = - serde_json::from_reader(File::open("scripts/input_gen/block.json").unwrap()).unwrap();*/ - - let pf_str = std::fs::read_to_string(path).unwrap(); - let pf: serde_json::Value = serde_json::from_str(pf_str.as_str()).unwrap(); - // let acct_pf = pf["accountProof"].clone(); - let storage_pf = pf["storageProof"][0].clone(); - // println!("acct_pf {:?}", acct_pf); - // println!("storage_root {:?}", pf["storageHash"]); - // println!("storage_pf {:?}", storage_pf); - - let key_bytes_str: String = serde_json::from_value(storage_pf["key"].clone()).unwrap(); - let path = keccak256(from_hex(&key_bytes_str)); - let value_bytes_str: String = serde_json::from_value(storage_pf["value"].clone()).unwrap(); - let value_bytes_str = if value_bytes_str.len() % 2 == 1 { - format!("0{}", &value_bytes_str[2..]) - } else { - value_bytes_str[2..].to_string() - }; - let value = ::rlp::encode(&from_hex(&value_bytes_str)).to_vec(); - let root_hash_str: String = serde_json::from_value(pf["storageHash"].clone()).unwrap(); - let pf_strs: Vec = serde_json::from_value(storage_pf["proof"].clone()).unwrap(); - - let value_max_byte_len = 33; - let proof = pf_strs.into_iter().map(|pf| from_hex(&pf[2..])).collect(); - - MPTFixedKeyInput { - path: H256(path), - value, - root_hash: H256::from_slice(&from_hex(&root_hash_str[2..])), - proof, - value_max_byte_len, - max_depth, - slot_is_empty, - } -} - -fn default_input() -> MPTFixedKeyInput { - mpt_input("scripts/input_gen/default_storage_pf.json", false, 8) -} - -#[test] -pub fn test_mock_mpt_inclusion_fixed() { - let params = EthConfigParams::from_path("configs/tests/mpt.json"); - // std::env::set_var("ETH_CONFIG_PARAMS", serde_json::to_string(¶ms).unwrap()); - let k = params.degree; - let input = mpt_input("scripts/input_gen/default_storage_pf.json", false, 5); // depth = max_depth - let circuit = test_mpt_circuit(k, RlcThreadBuilder::::mock(), input); - MockProver::run(k, &circuit, vec![]).unwrap().assert_satisfied(); - - let input = mpt_input("scripts/input_gen/default_storage_pf.json", false, 6); // depth != max_depth - let circuit = test_mpt_circuit(k, RlcThreadBuilder::::mock(), input); - MockProver::run(k, &circuit, vec![]).unwrap().assert_satisfied(); -} - -#[test] -pub fn test_mpt_noninclusion_branch_fixed() { - let params = EthConfigParams::from_path("configs/tests/mpt.json"); - let k = params.degree; - let input = mpt_input("scripts/input_gen/noninclusion_branch_pf.json", true, 5); - let circuit = test_mpt_circuit(k, RlcThreadBuilder::::mock(), input); - MockProver::run(k, &circuit, vec![]).unwrap().assert_satisfied(); -} - -#[test] -pub fn test_mpt_noninclusion_extension_fixed() { - let params = EthConfigParams::from_path("configs/tests/mpt.json"); - let k = params.degree; - let input = mpt_input("scripts/input_gen/noninclusion_extension_pf.json", true, 6); // require depth < max_depth - let circuit = test_mpt_circuit(k, RlcThreadBuilder::::mock(), input); - MockProver::run(k, &circuit, vec![]).unwrap().assert_satisfied(); -} - -#[test] -fn bench_mpt_inclusion_fixed() -> Result<(), Box> { - let bench_params_file = File::open("configs/bench/mpt.json").unwrap(); - std::fs::create_dir_all("data/bench")?; - let mut fs_results = File::create("data/bench/mpt.csv").unwrap(); - writeln!(fs_results, "degree,total_advice,num_rlc_columns,num_advice,num_lookup,num_fixed,proof_time,verify_time")?; - - let bench_params_reader = BufReader::new(bench_params_file); - let bench_params: Vec = serde_json::from_reader(bench_params_reader).unwrap(); - for bench_params in bench_params { - println!( - "---------------------- degree = {} ------------------------------", - bench_params.degree - ); - // set_var("ETH_CONFIG_PARAMS", serde_json::to_string(&bench_params).unwrap()); - set_var("KECCAK_ROWS", bench_params.keccak_rows_per_round.to_string()); - let k = bench_params.degree; - let params = gen_srs(k); - let circuit = test_mpt_circuit(k, RlcThreadBuilder::::keygen(), default_input()); - // circuit.config(k as usize, Some(bench_params.unusable_rows)); - let vk = keygen_vk(¶ms, &circuit)?; - let pk = keygen_pk(¶ms, vk, &circuit)?; - let break_points = circuit.break_points.take(); - - // create a proof - let proof_time = start_timer!(|| "Create proof SHPLONK"); - let circuit = test_mpt_circuit(k, RlcThreadBuilder::::prover(), default_input()); - assert_eq!(circuit.keccak.borrow().num_rows_per_round, bench_params.keccak_rows_per_round); - *circuit.break_points.borrow_mut() = break_points; - let mut transcript = Blake2bWrite::<_, _, Challenge255<_>>::init(vec![]); - create_proof::< - KZGCommitmentScheme, - ProverSHPLONK<'_, Bn256>, - Challenge255, - _, - Blake2bWrite, G1Affine, Challenge255>, - _, - >(¶ms, &pk, &[circuit], &[&[]], OsRng, &mut transcript)?; - let proof = transcript.finalize(); - end_timer!(proof_time); - - let verify_time = start_timer!(|| "Verify time"); - let verifier_params = params.verifier_params(); - let strategy = SingleStrategy::new(¶ms); - let mut transcript = Blake2bRead::<_, _, Challenge255<_>>::init(&proof[..]); - verify_proof::< - KZGCommitmentScheme, - VerifierSHPLONK<'_, Bn256>, - Challenge255, - Blake2bRead<&[u8], G1Affine, Challenge255>, - SingleStrategy<'_, Bn256>, - >(verifier_params, pk.get_vk(), strategy, &[&[]], &mut transcript) - .unwrap(); - end_timer!(verify_time); - - // auto generated - let bench_params: EthConfigParams = - serde_json::from_str(var("ETH_CONFIG_PARAMS").unwrap().as_str()).unwrap(); - let keccak_advice = - std::env::var("KECCAK_ADVICE_COLUMNS").unwrap().parse::().unwrap(); - writeln!( - fs_results, - "{},{},{},{:?},{:?},{},{:.2}s,{:?}", - bench_params.degree, - bench_params.num_rlc_columns - + bench_params.num_range_advice.iter().sum::() - + bench_params.num_lookup_advice.iter().sum::() - + keccak_advice, - bench_params.num_rlc_columns, - bench_params.num_range_advice, - bench_params.num_lookup_advice, - bench_params.num_fixed, - proof_time.time.elapsed().as_secs_f64(), - verify_time.time.elapsed() - ) - .unwrap(); - } - Ok(()) -} diff --git a/axiom-eth/src/mpt/tests/README.md b/axiom-eth/src/mpt/tests/README.md new file mode 100644 index 00000000..4b55f649 --- /dev/null +++ b/axiom-eth/src/mpt/tests/README.md @@ -0,0 +1,7 @@ +Here is some useful information regarding the mpt_tests + +With 500 keys, it is sufficient to use the parameters max_depth = 6 and max_key_byte_len = 3. + +loose - in these tests, we use max_depth = 6 and max_key_byte_len = 32, key_byte_len = Some(key.len()) +tight - in these tests, we use max_depth = proof.len() + slot_is_empty, max_key_byte_len = key.len(), key_byte_len = Some(key.len()) +fixed - in these tests, we use max_depth = 6, max_key_byte_len = key.len(), key_byte_len = None \ No newline at end of file diff --git a/axiom-eth/src/mpt/tests/mod.rs b/axiom-eth/src/mpt/tests/mod.rs new file mode 100644 index 00000000..9839a277 --- /dev/null +++ b/axiom-eth/src/mpt/tests/mod.rs @@ -0,0 +1,231 @@ +/// Tests using cita-trie to generate random tx tries +mod tx; + +use super::*; +use crate::{ + rlc::circuit::{builder::RlcCircuitBuilder, RlcCircuitParams}, + utils::eth_circuit::{ + create_circuit, EthCircuitImpl, EthCircuitInstructions, EthCircuitParams, + }, +}; +use ark_std::{end_timer, start_timer}; +use ethers_core::utils::keccak256; +use halo2_base::{ + gates::circuit::{BaseCircuitParams, CircuitBuilderStage}, + halo2_proofs::{ + dev::MockProver, + halo2curves::bn256::Fr, + plonk::{keygen_pk, keygen_vk}, + }, + utils::{ + fs::gen_srs, + testing::{check_proof_with_instances, gen_proof_with_instances}, + }, +}; +use hex::FromHex; +use std::{fs::File, io::Write, marker::PhantomData, path::Path}; +use test_case::test_case; +use test_log::test; + +const TEST_K: u32 = 15; + +#[derive(Clone)] +struct MptTest(MPTInput, PhantomData); + +impl EthCircuitInstructions for MptTest { + type FirstPhasePayload = MPTProofWitness; + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + ) -> Self::FirstPhasePayload { + let ctx = builder.base.main(0); + let mpt_proof = self.0.clone().assign(ctx); + mpt.parse_mpt_inclusion_phase0(ctx, mpt_proof) + } + + fn virtual_assign_phase1( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + mpt_witness: Self::FirstPhasePayload, + ) { + let (ctx_gate, ctx_rlc) = builder.rlc_ctx_pair(); + mpt.parse_mpt_inclusion_phase1((ctx_gate, ctx_rlc), mpt_witness); + } +} + +fn test_mpt_circuit( + stage: CircuitBuilderStage, + params: RlcCircuitParams, + inputs: MPTInput, +) -> EthCircuitImpl> { + let test = MptTest(inputs, PhantomData); + let mut circuit = create_circuit(stage, params, test); + circuit.mock_fulfill_keccak_promises(None); + if !stage.witness_gen_only() { + circuit.calculate_params(); + } + circuit +} + +/// Assumes string does **not** start with `0x` +fn from_hex(s: &str) -> Vec { + let s = if s.len() % 2 == 1 { format!("0{s}") } else { s.to_string() }; + Vec::from_hex(s).unwrap() +} + +// The input file is generated by running `query_test.sh` in the `scripts/input_gen` directory of this repo +fn mpt_input_storage( + path: impl AsRef, + slot_is_empty: bool, + max_depth: usize, + max_key_byte_len: usize, + key_byte_len: Option, +) -> MPTInput { + /*let block: serde_json::Value = + serde_json::from_reader(File::open("scripts/input_gen/block.json").unwrap()).unwrap();*/ + + let pf_str = std::fs::read_to_string(path).unwrap(); + let pf: serde_json::Value = serde_json::from_str(pf_str.as_str()).unwrap(); + // let acct_pf = pf["accountProof"].clone(); + let storage_pf = pf["storageProof"][0].clone(); + // println!("acct_pf {:?}", acct_pf); + // println!("storage_root {:?}", pf["storageHash"]); + // println!("storage_pf {:?}", storage_pf); + + let key_bytes_str: String = serde_json::from_value(storage_pf["key"].clone()).unwrap(); + let path = keccak256(from_hex(&key_bytes_str)).to_vec().into(); + // let path = keccak256(from_hex(&key_bytes_str)); + let value_bytes_str: String = serde_json::from_value(storage_pf["value"].clone()).unwrap(); + let value_bytes_str = if value_bytes_str.len() % 2 == 1 { + format!("0{}", &value_bytes_str[2..]) + } else { + value_bytes_str[2..].to_string() + }; + let value = ::rlp::encode(&from_hex(&value_bytes_str)).to_vec(); + let root_hash_str: String = serde_json::from_value(pf["storageHash"].clone()).unwrap(); + let pf_strs: Vec = serde_json::from_value(storage_pf["proof"].clone()).unwrap(); + + let value_max_byte_len = 33; + let proof = pf_strs.into_iter().map(|pf| from_hex(&pf[2..])).collect(); + + MPTInput { + path, + value, + root_hash: H256::from_slice(&from_hex(&root_hash_str[2..])), + proof, + slot_is_empty, + value_max_byte_len, + max_depth, + max_key_byte_len, + key_byte_len, + } +} + +fn default_input() -> MPTInput { + mpt_input_storage("scripts/input_gen/default_storage_pf.json", false, 8, 32, Some(32)) +} + +fn default_params() -> RlcCircuitParams { + let mut params = RlcCircuitParams::default(); + params.base.num_instance_columns = 1; + params.base.lookup_bits = Some(8); + params.base.k = TEST_K as usize; + params +} + +#[test_case("scripts/input_gen/default_storage_pf.json", false, 5, 32, None; "default storage inclusion fixed")] +#[test_case("scripts/input_gen/default_storage_pf.json", false, 5, 32, Some(32); "default storage inclusion var")] +#[test_case("scripts/input_gen/noninclusion_branch_pf.json", true, 5, 32, None; "noninclusion branch fixed")] +#[test_case("scripts/input_gen/noninclusion_branch_pf.json", true, 5, 32, Some(32); "noninclusion branch var")] +#[test_case("scripts/input_gen/noninclusion_extension_pf.json", true, 6, 32, None; "noninclusion extension fixed")] +#[test_case("scripts/input_gen/noninclusion_extension_pf.json", true, 6, 32, Some(32); "noninclusion extension var")] +#[test_case("scripts/input_gen/noninclusion_extension_pf2.json", true, 6, 32, None; "noninclusion branch then extension fixed")] +#[test_case("scripts/input_gen/noninclusion_extension_pf2.json", true, 6, 32, Some(32); "noninclusion branch then extension var")] +#[test_case("scripts/input_gen/empty_storage_pf.json", true, 5, 32, None; "empty root fixed")] +#[test_case("scripts/input_gen/empty_storage_pf.json", true, 5, 32, Some(32); "empty root var")] +pub fn test_mock_mpt( + path: &str, + slot_is_empty: bool, + max_depth: usize, + max_key_byte_len: usize, + key_byte_len: Option, +) { + let _ = env_logger::builder().is_test(true).try_init(); + let input = mpt_input_storage(path, slot_is_empty, max_depth, max_key_byte_len, key_byte_len); + let circuit = test_mpt_circuit::(CircuitBuilderStage::Mock, default_params(), input); + let instances = circuit.instances(); + assert_eq!(instances.len(), 1); + MockProver::run(TEST_K, &circuit, instances).unwrap().assert_satisfied(); +} + +#[test] +#[ignore = "bench"] +fn bench_mpt_inclusion_fixed() -> Result<(), Box> { + let bench_params_file = File::create("configs/bench/mpt.json").unwrap(); + std::fs::create_dir_all("data/bench")?; + let mut fs_results = File::create("data/bench/mpt.csv").unwrap(); + writeln!(fs_results, "degree,total_advice,num_rlc_columns,num_advice,num_lookup,num_fixed,proof_time,verify_time")?; + + let bench_k = 15..18; + let mut all_bench_params = vec![]; + // let bench_params: Vec = serde_json::from_reader(bench_params_file).unwrap(); + for k in bench_k { + println!("---------------------- degree = {k} ------------------------------",); + let params = gen_srs(k); + let mut bench_params = EthCircuitParams::default().rlc; + bench_params.base.k = k as usize; + + let mut circuit = + test_mpt_circuit(CircuitBuilderStage::Keygen, bench_params, default_input()); + let bench_params = circuit.calculate_params().rlc; + all_bench_params.push(bench_params.clone()); + let vk = keygen_vk(¶ms, &circuit)?; + let pk = keygen_pk(¶ms, vk, &circuit)?; + let break_points = circuit.break_points(); + + // create a proof + let proof_time = start_timer!(|| "Create proof SHPLONK"); + let circuit = + test_mpt_circuit(CircuitBuilderStage::Prover, bench_params.clone(), default_input()) + .use_break_points(break_points); + let instances = circuit.instances(); + assert_eq!(instances.len(), 1); + let proof = gen_proof_with_instances(¶ms, &pk, circuit, &[&instances[0]]); + end_timer!(proof_time); + + let verify_time = start_timer!(|| "Verify time"); + check_proof_with_instances(¶ms, pk.get_vk(), &proof, &[&instances[0]], true); + end_timer!(verify_time); + + let RlcCircuitParams { + base: + BaseCircuitParams { + k, + num_advice_per_phase, + num_fixed, + num_lookup_advice_per_phase, + .. + }, + num_rlc_columns, + } = bench_params; + writeln!( + fs_results, + "{},{},{},{:?},{:?},{},{:.2}s,{:?}", + k, + num_rlc_columns + + num_advice_per_phase.iter().sum::() + + num_lookup_advice_per_phase.iter().sum::(), + num_rlc_columns, + num_advice_per_phase, + num_lookup_advice_per_phase, + num_fixed, + proof_time.time.elapsed().as_secs_f64(), + verify_time.time.elapsed() + ) + .unwrap(); + } + serde_json::to_writer_pretty(bench_params_file, &all_bench_params).unwrap(); + Ok(()) +} diff --git a/axiom-eth/src/mpt/tests/tx.rs b/axiom-eth/src/mpt/tests/tx.rs new file mode 100644 index 00000000..2b2c04c5 --- /dev/null +++ b/axiom-eth/src/mpt/tests/tx.rs @@ -0,0 +1,445 @@ +#![allow(clippy::too_many_arguments)] +use super::*; +use cita_trie::{self, MemoryDB, PatriciaTrie, Trie}; +use ethers_core::utils::keccak256; +use hasher::HasherKeccak; +use std::sync::Arc; +use test_case::test_case; + +// Max Key Length: 3 +// Max Proof Length for noninclusion: 3 +// Max Proof Length for inclusion: 6 + +fn mpt_direct_input( + value: Vec, + proof: Vec>, + hash: Vec, + key: Vec, + slot_is_empty: bool, + max_depth: usize, + max_key_byte_len: usize, + key_byte_len: Option, +) -> MPTInput { + let value_max_byte_len = 48; + + MPTInput { + path: key.into(), + value, + root_hash: H256::from_slice(hash.as_slice()), + proof, + slot_is_empty, + value_max_byte_len, + max_depth, + max_key_byte_len, + key_byte_len, + } +} + +fn verify_key_val( + trie: &PatriciaTrie, + key: Vec, + val: Vec, + root: Vec, + slot_is_empty: bool, + bad_proof: bool, + distort_idx: Option>, + case_type: usize, +) -> bool { + if case_type == 0 { + verify_key_val_loose(trie, key, val, root, slot_is_empty, bad_proof, distort_idx) + } else if case_type == 1 { + verify_key_val_tight(trie, key, val, root, slot_is_empty, bad_proof, distort_idx) + } else if case_type == 2 { + verify_key_val_fixed(trie, key, val, root, slot_is_empty, bad_proof, distort_idx) + } else { + false + } +} + +fn verify_key_val_loose( + trie: &PatriciaTrie, + key: Vec, + val: Vec, + root: Vec, + slot_is_empty: bool, + bad_proof: bool, + distort_idx: Option>, +) -> bool { + let params = default_params(); + let key_byte_len = key.len(); + let mut proof = trie.get_proof(&key).unwrap(); + if bad_proof { + let idx = match distort_idx { + Some(_idx) => _idx, + None => { + // assert!(false); + vec![0x00] + } + }; + proof = distort_proof(proof, idx); + } + let input = + mpt_direct_input(val.to_vec(), proof, root, key, slot_is_empty, 6, 32, Some(key_byte_len)); // depth = max_depth + let k = params.base.k as u32; + let circuit = test_mpt_circuit::(CircuitBuilderStage::Mock, params, input); + let instances = circuit.instances(); + let prover = MockProver::run(k, &circuit, instances).unwrap(); + prover.verify().is_ok() +} + +fn verify_key_val_tight( + trie: &PatriciaTrie, + key: Vec, + val: Vec, + root: Vec, + slot_is_empty: bool, + bad_proof: bool, + distort_idx: Option>, +) -> bool { + let params = default_params(); + let key_byte_len = key.len(); + let mut proof = trie.get_proof(&key).unwrap(); + if bad_proof { + let idx = match distort_idx { + Some(_idx) => _idx, + None => { + // assert!(false); + vec![0x00] + } + }; + proof = distort_proof(proof, idx); + } + let proof_len = proof.len() + slot_is_empty as usize; + let input = mpt_direct_input( + val.to_vec(), + proof, + root, + key, + slot_is_empty, + proof_len, + key_byte_len, + Some(key_byte_len), + ); // depth = max_depth + let k = params.base.k as u32; + let circuit = test_mpt_circuit::(CircuitBuilderStage::Mock, params, input); + let instances = circuit.instances(); + let prover = MockProver::run(k, &circuit, instances).unwrap(); + prover.verify().is_ok() +} + +fn verify_key_val_fixed( + trie: &PatriciaTrie, + key: Vec, + val: Vec, + root: Vec, + slot_is_empty: bool, + bad_proof: bool, + distort_idx: Option>, +) -> bool { + let params = default_params(); + let key_byte_len = key.len(); + let mut proof = trie.get_proof(&key).unwrap(); + if bad_proof { + let idx = match distort_idx { + Some(_idx) => _idx, + None => { + // assert!(false); + vec![0x00] + } + }; + proof = distort_proof(proof, idx); + } + let input = + mpt_direct_input(val.to_vec(), proof, root, key, slot_is_empty, 6, key_byte_len, None); // depth = max_depth + let k = params.base.k as u32; + let circuit = test_mpt_circuit::(CircuitBuilderStage::Mock, params, input); + let instances = circuit.instances(); + let prover = MockProver::run(k, &circuit, instances).unwrap(); + prover.verify().is_ok() +} + +fn gen_tx_tree( + num_keys: usize, + rand_vals: bool, + val_max_bytes: usize, +) -> (PatriciaTrie, Vec>) { + let memdb = Arc::new(MemoryDB::new(true)); + let hasher = Arc::new(HasherKeccak::new()); + let mut trie = PatriciaTrie::new(Arc::clone(&memdb), Arc::clone(&hasher)); + let mut vals: Vec> = Vec::new(); + let mut val = [ + 0x43, 0x52, 0x59, 0x50, 0x54, 0x4f, 0x50, 0x55, 0x4e, 0x4b, 0x53, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x16, + ] + .to_vec(); + for idx in 0..num_keys { + let key = rlp::encode(&from_hex(&format!("{idx:x}").to_string())).to_vec(); + if rand_vals { + let val_len = if val_max_bytes == 32 { 32 } else { 32 + idx % (val_max_bytes - 32) }; + val = keccak256(val).to_vec(); + val.resize(val_len, 0x00); + } + let val2 = val.clone(); + let val3 = val.clone(); + vals.push(val3); + trie.insert(key, val2).unwrap(); + } + let root = trie.root().unwrap(); + trie = PatriciaTrie::from(Arc::clone(&memdb), Arc::clone(&hasher), &root).unwrap(); + (trie, vals) +} + +fn distort_proof(proof: Vec>, idx: Vec) -> Vec> { + let mut proof2 = proof.clone(); + for id in idx { + let realid = if id >= 0 { id as usize } else { proof.len() - id.unsigned_abs() as usize }; + assert!(realid < proof.len()); + proof2[realid] = distort(proof2[realid].clone()); + } + proof2 +} + +fn distort(val: Vec) -> Vec { + let mut val2 = val.clone(); + for i in 0..val.len() { + val2[i] = 255 - val[i]; + } + val2 +} + +#[test_case(1, false; "1 leaf, nonrand vals")] +#[test_case(2, false; "2 keys, nonrand vals")] +#[test_case(20, false; "20 keys, nonrand vals")] +#[test_case(200, false; "200 keys, nonrand vals")] +#[test_case(1, true; "1 leaf, rand vals")] +#[test_case(2, true; "2 keys, rand vals")] +#[test_case(20, true; "20 keys, rand vals")] +#[test_case(200, true; "200 keys, rand vals")] +fn pos_full_tree_test_inclusion(num_keys: usize, randvals: bool) { + let (mut trie, vals) = gen_tx_tree(num_keys, randvals, 48); + for idx in 0..(num_keys + 19) / 20 { + let key = rlp::encode(&from_hex(&format!("{:x}", (20 * idx)).to_string())).to_vec(); + let val = vals[idx * 20].clone(); + let root = trie.root().unwrap(); + assert!(verify_key_val(&trie, key, val, root, false, false, None, 0)); + } +} + +#[test_case(18, 1; "trie has 0x01, 0x10, 0x11, 2 branches")] +fn pos_test_inclusion(num_keys: usize, idx: usize) { + let (mut trie, vals) = gen_tx_tree(num_keys, true, 48); + + let key = rlp::encode(&from_hex(&format!("{idx:x}"))).to_vec(); + let val = vals[idx].clone(); + let root = trie.root().unwrap(); + assert!(verify_key_val(&trie, key, val, root, false, false, None, 0)); +} + +#[test_case(1, false, 0; "1 leaf, nonrand vals, loose")] +#[test_case(200, false, 0; "200 keys, nonrand vals, loose")] +#[test_case(1, true, 0; "1 leaf, rand vals, loose")] +#[test_case(200, true, 0; "200 keys, rand vals, loose")] +#[test_case(1, true, 1; "1 leaf, rand vals, tight")] +#[test_case(200, true, 1; "200 keys, rand vals, tight")] +#[test_case(1, true, 2; "1 leaf, rand vals, fixed")] +#[test_case(200, true, 2; "200 keys, rand vals, fixed")] +fn neg_full_tree_test_inclusion_badval(num_keys: usize, randvals: bool, case_type: usize) { + let (mut trie, _) = gen_tx_tree(num_keys, randvals, 48); + for idx in 0..(num_keys + 19) / 20 { + let key = rlp::encode(&from_hex(&format!("{:x}", (20 * idx)).to_string())).to_vec(); + let val = [0x00; 32]; + let root = trie.root().unwrap(); + assert!(!verify_key_val(&trie, key, val.to_vec(), root, false, false, None, case_type)); + } +} + +#[test_case(1, 0; "1 leaf, loose")] +#[test_case(200, 0; "200 keys, loose")] +#[test_case(1, 1; "1 leaf, tight")] +#[test_case(200, 1; "200 keys, tight")] +#[test_case(1, 2; "1 leaf, fixed")] +#[test_case(200, 2; "200 keys, fixed")] +fn pos_test_noninclusion(num_keys: usize, case_type: usize) { + let _ = env_logger::builder().is_test(true).try_init(); + let (mut trie, _) = gen_tx_tree(num_keys, true, 48); + for i in 0..10 { + let idx = num_keys + 20 * i; + let key = rlp::encode(&from_hex(&format!("{idx:x}").to_string())).to_vec(); + let val = [ + 0x43, 0x52, 0x59, 0x50, 0x54, 0x4f, 0x50, 0x55, 0x4e, 0x4b, 0x53, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x16, + ]; + let root = trie.root().unwrap(); + assert!(verify_key_val(&trie, key, val.to_vec(), root, true, false, None, case_type),); + } +} + +#[test_case(140, false, 1; "undercut by 1 inclusion")] +#[test_case(8, true, 1; "1 depth inclusion")] +#[test_case(300, false, 1; "undercut by 1 noninclusion")] +#[test_case(350, true, 1; "1 depth noninclusion")] +#[test_case(350, false, 0; "undercut by 0 noninclusion")] +fn neg_invalid_max_depth(idx: usize, zero_start: bool, offset: usize) { + let (mut trie, _) = gen_tx_tree(200, true, 48); + let key = rlp::encode(&from_hex(&format!("{idx:x}"))).to_vec(); + let val = [ + 0x43, 0x52, 0x59, 0x50, 0x54, 0x4f, 0x50, 0x55, 0x4e, 0x4b, 0x53, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x16, + ]; + let root = trie.root().unwrap(); + let key_byte_len = key.len(); + let proof = trie.get_proof(&key).unwrap(); + let proof_len = proof.len(); + let depth = if zero_start { offset } else { proof_len - offset }; + assert!(depth <= proof_len); + let input = + mpt_direct_input(val.to_vec(), proof, root, key, idx > 199, depth, 32, Some(key_byte_len)); // depth = max_depth + let params = default_params(); + let k = params.base.k as u32; + let circuit = test_mpt_circuit::(CircuitBuilderStage::Mock, params, input); + let instances = circuit.instances(); + let prover = MockProver::run(k, &circuit, instances).unwrap(); + assert!(prover.verify().is_err(), "Should not verify") + //Every outcome is valid except "Should not verify", in particular those that fail assertions elsewhere in the code +} + +#[test_case(140, false, 1; "undercut by 1 inclusion")] +#[test_case(200, true, 1; "1 len inclusion")] +#[test_case(300, false, 1; "undercut by 1 noninclusion")] +#[test_case(350, true, 1; "1 len noninclusion")] +fn neg_invalid_max_key_byte_len(idx: usize, zero_start: bool, offset: usize) { + let (mut trie, _) = gen_tx_tree(200, true, 48); + let key = rlp::encode(&from_hex(&format!("{idx:x}"))).to_vec(); + let val = [ + 0x43, 0x52, 0x59, 0x50, 0x54, 0x4f, 0x50, 0x55, 0x4e, 0x4b, 0x53, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x16, + ]; + let root = trie.root().unwrap(); + let key_byte_len = key.len(); + let proof = trie.get_proof(&key).unwrap(); + let len = if zero_start { offset } else { key_byte_len - offset }; + assert!(len < key_byte_len); + let input = + mpt_direct_input(val.to_vec(), proof, root, key, idx > 199, 6, len, Some(key_byte_len)); // depth = max_depth + let params = default_params(); + let k = params.base.k as u32; + let circuit = test_mpt_circuit::(CircuitBuilderStage::Mock, params, input); + let instance = circuit.instances(); + let prover = MockProver::run(k, &circuit, instance).unwrap(); + assert!(prover.verify().is_err(), "Should not verify"); +} + +#[test_case(140, false, 1; "undercut by 1 inclusion")] +#[test_case(120, true , 1; "over by 1 inclusion")] +#[test_case(340, false, 1; "undercut by 1 noninclusion")] +#[test_case(320, true , 1; "over by 1 noninclusion")] +fn neg_invalid_key_byte_len(idx: usize, pos: bool, offset: usize) { + let (mut trie, _) = gen_tx_tree(200, true, 48); + let key = rlp::encode(&from_hex(&format!("{idx:x}"))).to_vec(); + let val = [ + 0x43, 0x52, 0x59, 0x50, 0x54, 0x4f, 0x50, 0x55, 0x4e, 0x4b, 0x53, 0x00, 0x00, 0x00, 0x00, + 0x00, + ]; + let root = trie.root().unwrap(); + let key_byte_len = key.len(); + let proof = trie.get_proof(&key).unwrap(); + let len = if pos { key_byte_len + offset } else { key_byte_len - offset }; + let input = mpt_direct_input(val.to_vec(), proof, root, key, idx > 199, 6, 32, Some(len)); // depth = max_depth + let params = default_params(); + let k = params.base.k as u32; + let circuit = test_mpt_circuit::(CircuitBuilderStage::Mock, params, input); + let instance = circuit.instances(); + let prover = MockProver::run(k, &circuit, instance).unwrap(); + assert!(prover.verify().is_err(), "should not verify"); +} + +#[test_case(140, false, 1; "undercut by 1 inclusion")] +#[test_case(120, true , 1; "over by 1 inclusion")] +#[test_case(340, false, 1; "undercut by 1 noninclusion")] +#[test_case(320, true , 1; "over by 1 noninclusion")] +fn neg_invalid_max_key_byte_len_fixed(idx: usize, pos: bool, offset: usize) { + let (mut trie, _) = gen_tx_tree(200, true, 48); + let key = rlp::encode(&from_hex(&format!("{idx:x}"))).to_vec(); + let val = [ + 0x43, 0x52, 0x59, 0x50, 0x54, 0x4f, 0x50, 0x55, 0x4e, 0x4b, 0x53, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x16, + ]; + let root = trie.root().unwrap(); + let key_byte_len = key.len(); + let proof = trie.get_proof(&key).unwrap(); + let len = if pos { key_byte_len + offset } else { key_byte_len - offset }; + let input = mpt_direct_input(val.to_vec(), proof, root, key, idx > 199, 6, len, None); + let params = default_params(); + let k = params.base.k as u32; + let circuit = test_mpt_circuit::(CircuitBuilderStage::Mock, params, input); + let instance = circuit.instances(); + let prover = MockProver::run(k, &circuit, instance).unwrap(); + assert!(prover.verify().is_err(), "should not verify"); +} + +#[test_case(0; "empty tree, loose")] +#[test_case(1; "empty tree, tight")] +#[test_case(2; "empty tree, fixed")] +fn pos_empty_tree_noninclusion(case_type: usize) { + let (mut trie, _) = gen_tx_tree(0, true, 48); + let root = trie.root().unwrap(); + let key = [0; 32].to_vec(); + let val = [0x0].to_vec(); + println!("{root:02x?}"); + assert!(verify_key_val(&trie, key, val, root, true, false, None, case_type)); +} + +#[test_case(200, 0, 300; "empty proof, loose noninclusion")] +#[test_case(200, 1, 300; "empty proof, tight noninclusion")] +#[test_case(200, 2, 300; "empty proof, fixed noninclusion")] +fn neg_nonempty_tree_empty_proof(num_keys: usize, case_type: usize, idx: usize) { + let (mut trie, vals) = gen_tx_tree(num_keys, true, 48); + let root = trie.root().unwrap(); + let key = rlp::encode(&from_hex(&format!("{idx:x}"))).to_vec(); + let inpval = if idx > 199 { [0x0].to_vec() } else { vals[idx].clone() }; + let input = if case_type == 0 { + mpt_direct_input( + inpval, + [].to_vec(), + root, + key.clone(), + idx >= num_keys, + 6, + 32, + Some(key.len()), + ) + } else if case_type == 1 { + mpt_direct_input( + inpval, + [].to_vec(), + root, + key.clone(), + idx >= num_keys, + 1, + key.len(), + Some(key.len()), + ) + } else { + mpt_direct_input( + inpval, + [].to_vec(), + root, + key.clone(), + idx >= num_keys, + 6, + key.len(), + None, + ) + }; + let params = default_params(); + let k = params.base.k as u32; + let circuit = test_mpt_circuit::(CircuitBuilderStage::Mock, params, input); + let instance = circuit.instances(); + let prover = MockProver::run(k, &circuit, instance).unwrap(); + assert!(prover.verify().is_err(), "should not verify"); +} diff --git a/axiom-eth/src/mpt/types/input.rs b/axiom-eth/src/mpt/types/input.rs new file mode 100644 index 00000000..9d9ea4e7 --- /dev/null +++ b/axiom-eth/src/mpt/types/input.rs @@ -0,0 +1,224 @@ +//! Module that handles the logic of formatting an MPT proof into witnesses +//! used as inputs in the MPT chip. This mostly involves +//! - resizing vectors to fixed length by right padding with 0s, +//! - modifying last node in proof for exclusion proofs +//! - extracting the terminal node from the proof +use super::*; + +#[derive(Clone, Debug, Hash, Serialize, Deserialize)] +/// The pre-assigned inputs for the MPT proof +pub struct MPTInput { + // claim specification: (path, value) + /// A Merkle-Patricia Trie is a mapping `path => value` + /// + /// As an example, the MPT state trie of Ethereum has + /// `path = keccak256(address) => value = rlp(account)` + pub path: PathBytes, + pub value: Vec, + pub root_hash: H256, + + /// Inclusion proofs will always end in a terminal node: we extract this terminal node in cases where it was originally embedded inside the last branch node. + pub proof: Vec>, + + pub slot_is_empty: bool, + + pub value_max_byte_len: usize, + pub max_depth: usize, + pub max_key_byte_len: usize, + pub key_byte_len: Option, +} + +/// The assigned input for an MPT proof. +/// The `AssignedBytes` here have **not** been range checked. +/// The range checks are performed in the `parse_mpt_inclusion_phase0` function. +#[derive(Clone, Debug)] +pub struct MPTProof { + // claim specification: (key, value) + /// The key bytes, fixed length + pub key_bytes: AssignedBytes, + /// The RLP encoded `value` as bytes, variable length, resized to `value_max_byte_len` + pub value_bytes: AssignedBytes, + pub value_byte_len: AssignedValue, + pub root_hash_bytes: AssignedBytes, + + // proof specification + /// The variable length of the key + pub key_byte_len: Option>, + /// The variable length of the proof, including the leaf node if !slot_is_empty. + /// We always have the terminal node as a separate node, even if the original proof may embed it into the last branch node. + pub depth: AssignedValue, + /// RLP encoding of the final leaf node + pub leaf: MPTNode, + /// The non-leaf nodes of the mpt proof, resized to `max_depth - 1` with dummy **branch** nodes. + /// The actual variable length is `depth - 1` if `slot_is_empty == true` (excludes leaf node), otherwise `depth`. + pub nodes: Vec>, + /// The key fragments of the mpt proof, variable length, resized to `max_depth` with dummy fragments. + /// Each fragment (nibbles aka hexes) is variable length, resized to `2 * key_byte_len` with 0s + pub key_frag: Vec>, + /// Boolean indicating whether the MPT contains a value at `key` + pub slot_is_empty: AssignedValue, + + /// The maximum byte length of the key + pub max_key_byte_len: usize, + /// `max_depth` should be `>=1` + pub max_depth: usize, +} + +impl MPTInput { + /// Does **not** perform any range checks on witnesses to check if they are actually bytes. + /// This should be done in the `parse_mpt_inclusion_phase0` function + pub fn assign(self, ctx: &mut Context) -> MPTProof { + let Self { + path, + mut value, + root_hash, + mut proof, + value_max_byte_len, + max_depth, + slot_is_empty, + max_key_byte_len, + key_byte_len, + } = self; + let depth = proof.len(); + // if empty, we have a dummy node stored as a terminal node so that the circuit still works + // we ignore any results from the node, however. + if proof.is_empty() { + proof.push(NULL_LEAF.clone()); + } + //assert!(depth <= max_depth - usize::from(slot_is_empty)); + assert!(max_depth > 0); + assert!(max_key_byte_len > 0); + + let bytes_to_nibbles = |bytes: &[u8]| { + let mut nibbles = Vec::with_capacity(bytes.len() * 2); + for byte in bytes { + nibbles.push(byte >> 4); + nibbles.push(byte & 0xf); + } + nibbles + }; + let hex_len = |byte_len: usize, is_odd: bool| 2 * byte_len + usize::from(is_odd) - 2; + let path_nibbles = bytes_to_nibbles(path.as_ref()); + let mut path_idx = 0; + + // below "key" and "path" are used interchangeably, sorry for confusion + // if slot_is_empty, leaf is dummy, but with value 0x0 to make constraints pass (assuming claimed value is also 0x0) + let mut leaf = proof.pop().unwrap(); + let (_, max_leaf_bytes) = max_leaf_lens(max_key_byte_len, value_max_byte_len); + + let (_, max_ext_bytes) = max_ext_lens(max_key_byte_len); + let max_node_bytes = max(max_ext_bytes, MAX_BRANCH_ENCODING_BYTES); + + let mut key_frag = Vec::with_capacity(max_depth); + let mut nodes = Vec::with_capacity(max_depth - 1); + let mut process_node = |node: &[u8]| { + let decode = Rlp::new(node); + let node_type = decode.item_count().unwrap() == 2; + if node_type { + let encoded_path = decode.at(0).unwrap().data().unwrap(); + let byte_len = encoded_path.len(); + let encoded_nibbles = bytes_to_nibbles(encoded_path); + let is_odd = encoded_nibbles[0] == 1u8 || encoded_nibbles[0] == 3u8; + let mut frag = encoded_nibbles[2 - usize::from(is_odd)..].to_vec(); + path_idx += frag.len(); + frag.resize(2 * max_key_byte_len, 0); + key_frag.push((frag, byte_len, is_odd)); + } else { + let mut frag = vec![0u8; 2 * max_key_byte_len]; + frag[0] = path_nibbles[path_idx]; + key_frag.push((frag, 1, true)); + path_idx += 1; + } + node_type + }; + for mut node in proof { + let node_type = process_node(&node); + node.resize(max_node_bytes, 0); + nodes.push((node, node_type)); + } + let mut dummy_branch = DUMMY_BRANCH.clone(); + dummy_branch.resize(max_node_bytes, 0); + nodes.resize(max_depth - 1, (dummy_branch, false)); + + let leaf_type = process_node(&leaf); + let leaf_type = ctx.load_witness(F::from(leaf_type)); + let max_leaf_bytes = max(max_node_bytes, max_leaf_bytes); + leaf.resize(max_leaf_bytes, 0); + let mut path_bytes = path.0; + + let key_byte_len = key_byte_len.map(|key_byte_len| { + #[cfg(not(test))] + assert_eq!(key_byte_len, path_bytes.len()); + ctx.load_witness(F::from(key_byte_len as u64)) + }); + // if slot_is_empty, we modify key_frag so it still concatenates to `path` + if slot_is_empty { + // remove just added leaf frag + // key_frag.pop().unwrap(); + if key_frag.is_empty() { + // that means proof was empty + let mut nibbles = path_nibbles; + nibbles.resize(2 * max_key_byte_len, 0); + key_frag = vec![(nibbles, path_bytes.len() + 1, false)]; + } else { + // the last frag in non-inclusion doesn't match path + key_frag.pop().unwrap(); + let hex_len = key_frag + .iter() + .map(|(_, byte_len, is_odd)| hex_len(*byte_len, *is_odd)) + .sum::(); + let mut remaining = path_nibbles[hex_len..].to_vec(); + let is_odd = remaining.len() % 2 == 1; + let byte_len = (remaining.len() + 2 - usize::from(is_odd)) / 2; + remaining.resize(2 * max_key_byte_len, 0); + key_frag.push((remaining, byte_len, is_odd)); + } + } + key_frag.resize(max_depth, (vec![0u8; 2 * max_key_byte_len], 0, false)); + + // assign all values + let value_byte_len = ctx.load_witness(F::from(value.len() as u64)); + let depth = ctx.load_witness(F::from(depth as u64)); + let load_bytes = |bytes: Vec, ctx: &mut Context| { + ctx.assign_witnesses(bytes.iter().map(|x| F::from(*x as u64))) + }; + path_bytes.resize(max_key_byte_len, 0); + let key_bytes = load_bytes(path_bytes, ctx); + value.resize(value_max_byte_len, 0); + let value_bytes = load_bytes(value.to_vec(), ctx); + let root_hash_bytes = load_bytes(root_hash.as_bytes().to_vec(), ctx); + let leaf_bytes = load_bytes(leaf.to_vec(), ctx); + let nodes = nodes + .into_iter() + .map(|(node_bytes, node_type)| { + let rlp_bytes = ctx.assign_witnesses(node_bytes.iter().map(|x| F::from(*x as u64))); + let node_type = ctx.load_witness(F::from(node_type)); + MPTNode { rlp_bytes, node_type } + }) + .collect_vec(); + let key_frag = key_frag + .into_iter() + .map(|(nibbles, byte_len, is_odd)| { + let nibbles = ctx.assign_witnesses(nibbles.iter().map(|x| F::from(*x as u64))); + let byte_len = ctx.load_witness(F::from(byte_len as u64)); + let is_odd = ctx.load_witness(F::from(is_odd)); + MPTFragment { nibbles, is_odd, byte_len } + }) + .collect_vec(); + let slot_is_empty = ctx.load_witness(F::from(slot_is_empty)); + MPTProof { + key_bytes, + value_bytes, + value_byte_len, + root_hash_bytes, + key_byte_len, + depth, + leaf: MPTNode { rlp_bytes: leaf_bytes, node_type: leaf_type }, + nodes, + key_frag, + slot_is_empty, + max_key_byte_len, + max_depth, + } + } +} diff --git a/axiom-eth/src/mpt/types/mod.rs b/axiom-eth/src/mpt/types/mod.rs new file mode 100644 index 00000000..7e92db31 --- /dev/null +++ b/axiom-eth/src/mpt/types/mod.rs @@ -0,0 +1,249 @@ +use crate::{ + rlc::types::{RlcFixedTrace, RlcTrace}, + rlp::types::{RlpArrayWitness, RlpFieldTrace}, +}; + +use super::*; + +mod input; +pub use input::*; + +/// Witness for the terminal node of an MPT proof. +/// This can be either a leaf (`ext`) or extracted from a branch (`branch`). +/// The type is determined by `node_type`. +#[derive(Clone, Debug)] +pub struct TerminalWitness { + pub node_type: AssignedValue, + pub ext: LeafWitness, + pub branch: BranchWitness, + // pub max_leaf_bytes: usize, +} + +// TODO: there is no difference structurally between `TerminalTrace` and `MPTNodeTrace` right now. Should combine somehow while still keeping the distinction between the two. +/// The RLC traces corresponding to [`TerminalWitness`] +pub struct TerminalTrace { + pub node_type: AssignedValue, + pub ext: LeafTrace, + pub branch: BranchTrace, + // pub max_leaf_bytes: usize, +} + +impl TerminalTrace { + /// Returns the RLC of the MPT hash of the node by correcting selecting based on node type + pub fn mpt_hash_rlc( + &self, + ctx_gate: &mut Context, + gate: &impl GateInstructions, + ) -> RlcVar { + rlc_select( + ctx_gate, + gate, + self.ext.rlcs.mpt_hash, + self.branch.rlcs.mpt_hash, + self.node_type, + ) + } + + /// Returns the RLC of the keccak of the node by correcting selecting based on node type + pub fn keccak_rlc( + &self, + ctx_gate: &mut Context, + gate: &impl GateInstructions, + ) -> AssignedValue { + assert_eq!(self.ext.rlcs.hash.len, self.branch.rlcs.hash.len); + gate.select( + ctx_gate, + self.ext.rlcs.hash.rlc_val, + self.branch.rlcs.hash.rlc_val, + self.node_type, + ) + } +} + +#[derive(Clone, Debug)] +pub struct LeafTrace { + pub key_path: RlpFieldTrace, + pub value: RlpFieldTrace, + pub rlcs: MPTHashTrace, +} + +#[derive(Clone, Debug)] +pub struct LeafWitness { + pub rlp: RlpArrayWitness, + pub hash_query: MPTHashWitness, +} + +#[derive(Clone, Debug)] +pub struct ExtensionTrace { + pub key_path: RlpFieldTrace, + pub node_ref: RlpFieldTrace, + pub rlcs: MPTHashTrace, +} + +#[derive(Clone, Debug)] +pub struct ExtensionWitness { + pub rlp: RlpArrayWitness, + pub hash_query: MPTHashWitness, +} + +#[derive(Clone, Debug)] +pub struct BranchTrace { + // rlc without rlp prefix + pub node_refs: [RlpFieldTrace; BRANCH_NUM_ITEMS], + pub rlcs: MPTHashTrace, +} + +#[derive(Clone, Debug)] +pub struct BranchWitness { + pub rlp: RlpArrayWitness, + pub hash_query: MPTHashWitness, +} + +// helper types for readability +pub type AssignedBytes = Vec>; // TODO: use SafeByte +pub type AssignedNibbles = Vec>; + +#[derive(Clone, Debug)] +pub struct MPTNode { + pub rlp_bytes: AssignedBytes, + /// 0 = branch, 1 = extension + pub node_type: AssignedValue, +} + +#[derive(Clone, Debug)] +/// The `node_type` flag selects whether the node is parsed as a branch or extension node. +pub struct MPTNodeWitness { + /// 0 = branch, 1 = extension + pub node_type: AssignedValue, + /// The node parsed as an extension node, or dummy extension node otherwise + pub ext: ExtensionWitness, + /// The node parsed as a branch node, or dummy branch node otherwise + pub branch: BranchWitness, +} + +#[derive(Clone, Debug)] +/// The `node_type` flag selects whether the node is parsed as a branch or extension node. +pub struct MPTNodeTrace { + /// 0 = branch, 1 = extension + pub node_type: AssignedValue, + /// The node parsed as an extension node, or dummy extension node otherwise + pub ext: ExtensionTrace, + /// The node parsed as a branch node, or dummy branch node otherwise + pub branch: BranchTrace, +} + +impl MPTNodeTrace { + /// Returns the RLC of the MPT hash of the node by correcting selecting based on node type + pub fn mpt_hash_rlc( + &self, + ctx_gate: &mut Context, + gate: &impl GateInstructions, + ) -> RlcVar { + rlc_select( + ctx_gate, + gate, + self.ext.rlcs.mpt_hash, + self.branch.rlcs.mpt_hash, + self.node_type, + ) + } + + /// Returns the RLC of the keccak of the node by correcting selecting based on node type + pub fn keccak_rlc( + &self, + ctx_gate: &mut Context, + gate: &impl GateInstructions, + ) -> AssignedValue { + assert_eq!(self.ext.rlcs.hash.len, self.branch.rlcs.hash.len); + gate.select( + ctx_gate, + self.ext.rlcs.hash.rlc_val, + self.branch.rlcs.hash.rlc_val, + self.node_type, + ) + } +} + +#[derive(Clone, Debug)] +/// A fragment of the key (bytes), stored as nibbles before hex-prefix encoding +pub struct MPTFragment { + /// A variable length string of hex-numbers, resized to a fixed max length with 0s + pub nibbles: AssignedNibbles, + pub is_odd: AssignedValue, + // hex_len = 2 * byte_len + is_odd - 2 + // if nibble for branch: byte_len = is_odd = 1 + /// The byte length of the hex-prefix encoded fragment + pub byte_len: AssignedValue, +} + +#[derive(Clone, Debug)] +pub struct MPTProofWitness { + // we keep only the parts of the proof necessary: + pub value_bytes: AssignedBytes, + pub value_byte_len: AssignedValue, + pub root_hash_bytes: AssignedBytes, + /// The variable length of the key + pub key_byte_len: Option>, + /// The variable length of the proof. This includes the leaf node in the case of an inclusion proof. There is no leaf node in the case of a non-inclusion proof. + pub depth: AssignedValue, + /// The non-leaf nodes of the mpt proof, resized to `max_depth - 1`. Each node has been parsed with both a hypothetical branch and extension node. The actual type is determined by the `node_type` flag. + /// + /// The actual variable length of `nodes` is `depth - 1` if `slot_is_empty == true` (excludes leaf node), otherwise `depth`. + pub nodes: Vec>, + /// The last node parsed + pub terminal_node: TerminalWitness, + + /// Boolean indicating whether the MPT contains a value at `key` + pub slot_is_empty: AssignedValue, + + pub max_key_byte_len: usize, + pub max_depth: usize, + + /// The key fragments (nibbles), without encoding, provided as private inputs + pub key_frag: Vec>, + /// The hex-prefix encoded path for (potential) extension nodes (hex-prefix encoding has leaf vs. extension distinction). + /// These are derived from the nodes themselves. + pub key_frag_ext_bytes: Vec>, + /// The hex-prefix encoded path for (potential) leaf nodes (hex-prefix encoding has leaf vs. extension distinction). + /// These are derived from the nodes themselves. + pub key_frag_leaf_bytes: Vec>, + pub frag_lens: Vec>, + pub key_hexs: AssignedNibbles, +} + +pub type MPTHashWitness = KeccakVarLenQuery; + +#[derive(Clone, Copy, Debug)] +pub struct MPTHashTrace { + /// 32-byte keccak hash RLC + pub hash: RlcFixedTrace, + /// input if len < 32, hash otherwise. + pub mpt_hash: RlcTrace, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Hash)] +pub struct PathBytes(pub Vec); + +impl> From<&T> for PathBytes { + fn from(value: &T) -> Self { + Self(value.as_ref().to_vec()) + } +} + +impl From> for PathBytes { + fn from(value: Vec) -> Self { + Self(value) + } +} + +impl From for PathBytes { + fn from(value: H256) -> Self { + Self(value.0.to_vec()) + } +} + +impl AsRef<[u8]> for PathBytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} diff --git a/axiom-eth/src/providers.rs b/axiom-eth/src/providers.rs deleted file mode 100644 index 41397d20..00000000 --- a/axiom-eth/src/providers.rs +++ /dev/null @@ -1,428 +0,0 @@ -use crate::{ - batch_query::response::native::{FullStorageQuery, FullStorageResponse}, - mpt::MPTFixedKeyInput, - storage::{ - EthBlockStorageInput, EthStorageInput, ACCOUNT_PROOF_VALUE_MAX_BYTE_LEN, - ACCOUNT_STATE_FIELDS_MAX_BYTES, STORAGE_PROOF_VALUE_MAX_BYTE_LEN, - }, -}; -use ethers_core::types::{Address, Block, Bytes, EIP1186ProofResponse, H256}; -use ethers_core::utils::keccak256; -use ethers_providers::{JsonRpcClient, Middleware, Provider, ProviderError}; -// use halo2_mpt::mpt::{max_branch_lens, max_leaf_lens}; -use futures::future::{join, join_all}; -use rlp::{Encodable, Rlp, RlpStream}; -use serde::{Deserialize, Serialize}; -use serde_with::serde_as; -use std::{ - fs::{self, File}, - path::PathBuf, -}; -use tokio::runtime::Runtime; - -pub const MAINNET_PROVIDER_URL: &str = "https://mainnet.infura.io/v3/"; -pub const GOERLI_PROVIDER_URL: &str = "https://goerli.infura.io/v3/"; - -/// Makes concurrent JSON-RPC calls to get the blocks with the given block numbers. -pub fn get_blocks( - provider: &Provider

, - block_numbers: impl IntoIterator, -) -> Result>>, ProviderError> { - let rt = Runtime::new().unwrap(); - rt.block_on(join_all( - block_numbers.into_iter().map(|block_number| provider.get_block(block_number)), - )) - .into_iter() - .collect() -} - -async fn get_account_query( - provider: &Provider

, - block_number: u64, - addr: Address, - acct_pf_max_depth: usize, -) -> EthStorageInput { - let block = provider.get_block(block_number).await.unwrap().unwrap(); - let pf = provider.get_proof(addr, vec![], Some(block_number.into())).await.unwrap(); - - let acct_key = H256(keccak256(addr)); - let slot_is_empty = !is_assigned_slot(&acct_key, &pf.account_proof); - EthStorageInput { - addr, - acct_state: get_acct_list(&pf), - acct_pf: MPTFixedKeyInput { - path: acct_key, - value: get_acct_rlp(&pf), - root_hash: block.state_root, - proof: pf.account_proof.into_iter().map(|x| x.to_vec()).collect(), - value_max_byte_len: ACCOUNT_PROOF_VALUE_MAX_BYTE_LEN, - max_depth: acct_pf_max_depth, - slot_is_empty, - }, - storage_pfs: vec![], - } -} - -pub fn get_account_queries( - provider: &Provider

, - queries: Vec<(u64, Address)>, - acct_pf_max_depth: usize, -) -> Vec { - let rt = Runtime::new().unwrap(); - rt.block_on(join_all(queries.into_iter().map(|(block_number, addr)| { - get_account_query(provider, block_number, addr, acct_pf_max_depth) - }))) -} - -/// Does not provide state root -async fn get_storage_query( - provider: &Provider

, - block_number: u64, - addr: Address, - slots: Vec, - acct_pf_max_depth: usize, - storage_pf_max_depth: usize, -) -> EthStorageInput { - let pf = provider.get_proof(addr, slots, Some(block_number.into())).await.unwrap(); - - let acct_key = H256(keccak256(addr)); - let slot_is_empty = !is_assigned_slot(&acct_key, &pf.account_proof); - log::info!("block: {block_number}, address: {addr}, account is empty: {slot_is_empty}"); - let acct_state = get_acct_list(&pf); - let acct_pf = MPTFixedKeyInput { - path: acct_key, - value: get_acct_rlp(&pf), - root_hash: H256([0u8; 32]), - proof: pf.account_proof.into_iter().map(|x| x.to_vec()).collect(), - value_max_byte_len: ACCOUNT_PROOF_VALUE_MAX_BYTE_LEN, - max_depth: acct_pf_max_depth, - slot_is_empty, - }; - let storage_pfs = pf - .storage_proof - .into_iter() - .map(|storage_pf| { - let path = H256(keccak256(storage_pf.key)); - let slot_is_empty = !is_assigned_slot(&path, &storage_pf.proof); - log::info!("block: {block_number}, address: {addr}, slot: {}, storage slot is empty: {slot_is_empty}", storage_pf.key); - let value = storage_pf.value.rlp_bytes().to_vec(); - ( - storage_pf.key, - storage_pf.value, - MPTFixedKeyInput { - path, - value, - root_hash: pf.storage_hash, - proof: storage_pf.proof.into_iter().map(|x| x.to_vec()).collect(), - value_max_byte_len: STORAGE_PROOF_VALUE_MAX_BYTE_LEN, - max_depth: storage_pf_max_depth, - slot_is_empty, - }, - ) - }) - .collect(); - EthStorageInput { addr, acct_state, acct_pf, storage_pfs } -} - -pub fn get_full_storage_queries( - provider: &Provider

, - queries: Vec, - acct_pf_max_depth: usize, - storage_pf_max_depth: usize, -) -> Result, String> { - let block_futures = - join_all(queries.iter().map(|query| provider.get_block(query.block_number))); - let storage_futures = - queries.into_iter().map(|FullStorageQuery { block_number, addr_slots }| { - let res = addr_slots.map(|(addr, slots)| { - get_storage_query( - provider, - block_number, - addr, - slots, - acct_pf_max_depth, - storage_pf_max_depth, - ) - }); - async { - match res { - Some(res) => Some(res.await), - None => None, - } - } - }); - let storage_futures = join_all(storage_futures); - let (blocks, pfs) = - Runtime::new().unwrap().block_on(async { join(block_futures, storage_futures).await }); - blocks - .into_iter() - .zip(pfs.into_iter()) - .map(|(block, mut pf)| { - block - .map_err(|e| format!("get_block JSON-RPC call failed: {e}")) - .and_then(|block| block.ok_or_else(|| "Block not found".to_string())) - .map(|block| { - if let Some(pf) = pf.as_mut() { - pf.acct_pf.root_hash = block.state_root; - } - FullStorageResponse { block, account_storage: pf } - }) - }) - .collect::, _>>() -} - -pub fn get_storage_queries( - provider: &Provider

, - queries: Vec<(u64, Address, H256)>, - acct_pf_max_depth: usize, - storage_pf_max_depth: usize, -) -> Vec { - let rt = Runtime::new().unwrap(); - rt.block_on(join_all(queries.into_iter().map(|(block_number, addr, slot)| { - get_storage_query( - provider, - block_number, - addr, - vec![slot], - acct_pf_max_depth, - storage_pf_max_depth, - ) - }))) -} - -pub fn get_block_storage_input( - provider: &Provider

, - block_number: u32, - addr: Address, - slots: Vec, - acct_pf_max_depth: usize, - storage_pf_max_depth: usize, -) -> EthBlockStorageInput { - let rt = Runtime::new().unwrap(); - let block = rt - .block_on(provider.get_block(block_number as u64)) - .unwrap() - .unwrap_or_else(|| panic!("Block {block_number} not found")); - let block_hash = block.hash.unwrap(); - let block_header = get_block_rlp(&block); - - let mut storage = rt.block_on(get_storage_query( - provider, - block_number as u64, - addr, - slots, - acct_pf_max_depth, - storage_pf_max_depth, - )); - storage.acct_pf.root_hash = block.state_root; - - EthBlockStorageInput { block, block_number, block_hash, block_header, storage } -} - -pub fn is_assigned_slot(key: &H256, proof: &[Bytes]) -> bool { - let mut key_nibbles = Vec::new(); - for &byte in key.as_bytes() { - key_nibbles.push(byte / 16); - key_nibbles.push(byte % 16); - } - let mut key_frags = Vec::new(); - let mut path_idx = 0; - for node in proof.iter() { - let rlp = Rlp::new(node); - if rlp.item_count().unwrap() == 2 { - let path = rlp.at(0).unwrap().data().unwrap(); - let is_odd = (path[0] / 16 == 1u8) || (path[0] / 16 == 3u8); - let mut frag = Vec::new(); - if is_odd { - frag.push(path[0] % 16); - path_idx += 1; - } - for byte in path.iter().skip(1) { - frag.push(*byte / 16); - frag.push(*byte % 16); - path_idx += 2; - } - key_frags.extend(frag); - } else { - key_frags.extend(vec![key_nibbles[path_idx]]); - path_idx += 1; - } - } - if path_idx == 64 { - for idx in 0..64 { - if key_nibbles[idx] != key_frags[idx] { - return false; - } - } - } else { - return false; - } - true -} - -pub fn get_acct_rlp(pf: &EIP1186ProofResponse) -> Vec { - let mut rlp: RlpStream = RlpStream::new_list(4); - rlp.append(&pf.nonce); - rlp.append(&pf.balance); - rlp.append(&pf.storage_hash); - rlp.append(&pf.code_hash); - rlp.out().into() -} - -/// Format AccountState into list of fixed-length byte arrays -pub fn get_acct_list(pf: &EIP1186ProofResponse) -> Vec> { - let mut nonce_bytes = vec![0u8; 8]; - pf.nonce.to_big_endian(&mut nonce_bytes); - let mut balance_bytes = [0u8; 32]; - pf.balance.to_big_endian(&mut balance_bytes); - let balance_bytes = balance_bytes[32 - ACCOUNT_STATE_FIELDS_MAX_BYTES[1]..].to_vec(); - let storage_root = pf.storage_hash.as_bytes().to_vec(); - let code_hash = pf.code_hash.as_bytes().to_vec(); - vec![nonce_bytes, balance_bytes, storage_root, code_hash] -} - -pub fn get_block_rlp(block: &Block) -> Vec { - let withdrawals_root: Option = block.withdrawals_root; - let base_fee = block.base_fee_per_gas; - let rlp_len = 15 + usize::from(base_fee.is_some()) + usize::from(withdrawals_root.is_some()); - let mut rlp = RlpStream::new_list(rlp_len); - rlp.append(&block.parent_hash); - rlp.append(&block.uncles_hash); - rlp.append(&block.author.unwrap()); - rlp.append(&block.state_root); - rlp.append(&block.transactions_root); - rlp.append(&block.receipts_root); - rlp.append(&block.logs_bloom.unwrap()); - rlp.append(&block.difficulty); - rlp.append(&block.number.unwrap()); - rlp.append(&block.gas_limit); - rlp.append(&block.gas_used); - rlp.append(&block.timestamp); - rlp.append(&block.extra_data.to_vec()); - rlp.append(&block.mix_hash.unwrap()); - rlp.append(&block.nonce.unwrap()); - base_fee.map(|base_fee| rlp.append(&base_fee)); - withdrawals_root.map(|withdrawals_root| rlp.append(&withdrawals_root)); - let encoding: Vec = rlp.out().into(); - assert_eq!(keccak256(&encoding), block.hash.unwrap().0); - encoding -} - -serde_with::serde_conv!( - BytesBase64, - Vec, - |bytes: &Vec| { - use base64::{engine::general_purpose, Engine as _}; - general_purpose::STANDARD.encode(bytes) - }, - |encoded: String| { - use base64::{engine::general_purpose, Engine as _}; - general_purpose::STANDARD.decode(encoded) - } -); - -#[serde_as] -#[derive(Debug, Serialize, Deserialize)] -pub struct ProcessedBlock { - #[serde_as(as = "Vec")] - pub block_rlps: Vec>, - pub block_hashes: Vec, - pub prev_hash: H256, -} - -/// returns tuple of: -/// * vector of RLP bytes of each block -/// * tuple of -/// * parentHash (H256) -/// * endHash (H256) -/// * startBlockNumber (u32) -/// * endBlockNumber (u32) -/// * merkleRoots (Vec) -/// * where merkleRoots is a length `max_depth + 1` vector representing a merkle mountain range, ordered largest mountain first -// second tuple `instance` is only used for debugging now -pub fn get_blocks_input( - provider: &Provider

, - start_block_number: u32, - num_blocks: u32, - max_depth: usize, -) -> Vec> { - assert!(num_blocks <= (1 << max_depth)); - assert!(num_blocks > 0); - let chain_data_dir = PathBuf::from("data/chain"); - fs::create_dir_all(&chain_data_dir).unwrap(); - let end_block_number = start_block_number + num_blocks - 1; - let rt = Runtime::new().unwrap(); - let chain_id = rt.block_on(provider.get_chainid()).unwrap(); - let path = chain_data_dir - .join(format!("chainid{chain_id}_{start_block_number:06x}_{end_block_number:06x}.json")); - // block_hashes and prev_hash no longer used, but keeping this format for compatibility with old cached chaindata - let ProcessedBlock { mut block_rlps, block_hashes: _, prev_hash: _ } = - if let Ok(f) = File::open(&path) { - serde_json::from_reader(f).unwrap() - } else { - let blocks = get_blocks( - provider, - start_block_number as u64..(start_block_number + num_blocks) as u64, - ) - .unwrap_or_else(|e| panic!("get_blocks JSON-RPC call failed: {e}")); - let prev_hash = blocks[0].as_ref().expect("block not found").parent_hash; - let (block_rlps, block_hashes): (Vec<_>, Vec<_>) = blocks - .into_iter() - .map(|block| { - let block = block.expect("block not found"); - (get_block_rlp(&block), block.hash.unwrap()) - }) - .unzip(); - // write this to file - let file = File::create(&path).unwrap(); - let payload = ProcessedBlock { block_rlps, block_hashes, prev_hash }; - serde_json::to_writer(file, &payload).unwrap(); - payload - }; - // pad to correct length with dummies - let dummy_block_rlp = block_rlps[0].clone(); - block_rlps.resize(1 << max_depth, dummy_block_rlp); - - /*let end_hash = *block_hashes.last().unwrap(); - let mmr = get_merkle_mountain_range(&block_hashes, max_depth); - - let instance = EthBlockHeaderChainInstance::new( - prev_hash, - end_hash, - start_block_number, - end_block_number, - mmr, - );*/ - block_rlps -} - -#[cfg(test)] -mod tests { - use std::env::var; - - use ethers_providers::Http; - - use super::*; - - #[test] - fn test_provider() { - let provider_uri = var("JSON_RPC_URL").expect("JSON_RPC_URL not found"); - let provider = - Provider::::try_from(provider_uri).expect("could not instantiate HTTP Provider"); - - let rt = Runtime::new().unwrap(); - let block = rt.block_on(provider.get_block(17034973)).unwrap().unwrap(); - get_block_rlp(&block); - } - - #[test] - fn test_retry_provider() { - let provider_uri = var("JSON_RPC_URL").expect("JSON_RPC_URL not found"); - let provider = Provider::new_client(&provider_uri, 10, 500) - .expect("could not instantiate HTTP Provider"); - - let rt = Runtime::new().unwrap(); - let block = rt.block_on(provider.get_block(17034973)).unwrap().unwrap(); - get_block_rlp(&block); - } -} diff --git a/axiom-eth/src/providers/account.rs b/axiom-eth/src/providers/account.rs new file mode 100644 index 00000000..250aec20 --- /dev/null +++ b/axiom-eth/src/providers/account.rs @@ -0,0 +1,55 @@ +use ethers_core::types::{Address, EIP1186ProofResponse}; +use ethers_providers::{JsonRpcClient, Middleware, Provider}; +use futures::future::join_all; +use rlp::RlpStream; +use tokio::runtime::Runtime; + +use crate::storage::{circuit::EthStorageInput, ACCOUNT_STATE_FIELDS_MAX_BYTES}; + +use super::storage::json_to_mpt_input; + +async fn get_account_query( + provider: &Provider

, + block_number: u64, + addr: Address, + acct_pf_max_depth: usize, +) -> EthStorageInput { + let block = provider.get_block(block_number).await.unwrap().unwrap(); + let pf = provider.get_proof(addr, vec![], Some(block_number.into())).await.unwrap(); + + let mut input = json_to_mpt_input(pf, acct_pf_max_depth, 0); + input.acct_pf.root_hash = block.state_root; + input +} + +pub fn get_account_queries( + provider: &Provider

, + queries: Vec<(u64, Address)>, + acct_pf_max_depth: usize, +) -> Vec { + let rt = Runtime::new().unwrap(); + rt.block_on(join_all(queries.into_iter().map(|(block_number, addr)| { + get_account_query(provider, block_number, addr, acct_pf_max_depth) + }))) +} + +pub fn get_acct_rlp(pf: &EIP1186ProofResponse) -> Vec { + let mut rlp: RlpStream = RlpStream::new_list(4); + rlp.append(&pf.nonce); + rlp.append(&pf.balance); + rlp.append(&pf.storage_hash); + rlp.append(&pf.code_hash); + rlp.out().into() +} + +/// Format AccountState into list of fixed-length byte arrays +pub fn get_acct_list(pf: &EIP1186ProofResponse) -> Vec> { + let mut nonce_bytes = vec![0u8; 8]; + pf.nonce.to_big_endian(&mut nonce_bytes); + let mut balance_bytes = [0u8; 32]; + pf.balance.to_big_endian(&mut balance_bytes); + let balance_bytes = balance_bytes[32 - ACCOUNT_STATE_FIELDS_MAX_BYTES[1]..].to_vec(); + let storage_root = pf.storage_hash.as_bytes().to_vec(); + let code_hash = pf.code_hash.as_bytes().to_vec(); + vec![nonce_bytes, balance_bytes, storage_root, code_hash] +} diff --git a/axiom-eth/src/providers/block.rs b/axiom-eth/src/providers/block.rs new file mode 100644 index 00000000..7359d510 --- /dev/null +++ b/axiom-eth/src/providers/block.rs @@ -0,0 +1,99 @@ +use ethers_core::{ + types::{Block, H256}, + utils::keccak256, +}; +use ethers_providers::{JsonRpcClient, Middleware, Provider, ProviderError}; +use futures::future::join_all; +use rlp::RlpStream; +use tokio::runtime::Runtime; + +/// Makes concurrent JSON-RPC calls to get the blocks with the given block numbers. +pub fn get_blocks( + provider: &Provider

, + block_numbers: impl IntoIterator, +) -> Result>>, ProviderError> { + let rt = Runtime::new().unwrap(); + rt.block_on(join_all( + block_numbers.into_iter().map(|block_number| provider.get_block(block_number)), + )) + .into_iter() + .collect() +} + +pub fn get_block_rlp_from_num( + provider: &Provider

, + block_number: u32, +) -> Vec { + let rt = Runtime::new().unwrap(); + let block2 = rt + .block_on(provider.get_block(block_number as u64)) + .unwrap() + .unwrap_or_else(|| panic!("Block {block_number} not found")); + get_block_rlp(&block2) +} + +pub fn get_block_rlp(block: &Block) -> Vec { + let withdrawals_root: Option = block.withdrawals_root; + let base_fee = block.base_fee_per_gas; + let rlp_len = 15 + usize::from(base_fee.is_some()) + usize::from(withdrawals_root.is_some()); + let mut rlp = RlpStream::new_list(rlp_len); + rlp.append(&block.parent_hash); + rlp.append(&block.uncles_hash); + rlp.append(&block.author.unwrap()); + rlp.append(&block.state_root); + rlp.append(&block.transactions_root); + rlp.append(&block.receipts_root); + rlp.append(&block.logs_bloom.unwrap()); + rlp.append(&block.difficulty); + rlp.append(&block.number.unwrap()); + rlp.append(&block.gas_limit); + rlp.append(&block.gas_used); + rlp.append(&block.timestamp); + rlp.append(&block.extra_data.to_vec()); + rlp.append(&block.mix_hash.unwrap()); + rlp.append(&block.nonce.unwrap()); + base_fee.map(|base_fee| rlp.append(&base_fee)); + withdrawals_root.map(|withdrawals_root| rlp.append(&withdrawals_root)); + let encoding: Vec = rlp.out().into(); + assert_eq!(keccak256(&encoding), block.hash.unwrap().0); + encoding +} + +/// returns vector of RLP bytes of each block in [start_block_number, start_block_number + num_blocks) +pub fn get_blocks_input( + provider: &Provider

, + start_block_number: u32, + num_blocks: u32, +) -> Vec> { + let blocks = + get_blocks(provider, start_block_number as u64..(start_block_number + num_blocks) as u64) + .unwrap_or_else(|e| panic!("get_blocks JSON-RPC call failed: {e}")); + blocks + .into_iter() + .map(|block| { + let block = block.expect("block not found"); + get_block_rlp(&block) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use ethers_core::types::Chain; + use ethers_providers::Middleware; + + use crate::providers::setup_provider; + + use super::*; + + #[test] + fn test_retry_provider() { + let provider = setup_provider(Chain::Mainnet); + + let rt = Runtime::new().unwrap(); + for block_num in [5000050, 5000051, 17034973] { + let block = rt.block_on(provider.get_block(block_num)).unwrap().unwrap(); + get_block_rlp(&block); + } + } +} diff --git a/axiom-eth/src/providers/mod.rs b/axiom-eth/src/providers/mod.rs new file mode 100644 index 00000000..2580bc21 --- /dev/null +++ b/axiom-eth/src/providers/mod.rs @@ -0,0 +1,26 @@ +#![allow(clippy::too_many_arguments)] +use ethers_core::{types::Chain, utils::hex::FromHex}; +use ethers_providers::{Http, Provider, RetryClient}; + +use std::env::var; + +pub mod account; +pub mod block; +pub mod receipt; +pub mod storage; +pub mod transaction; + +pub fn get_provider_uri(chain: Chain) -> String { + let key = var("ALCHEMY_KEY").expect("ALCHEMY_KEY environmental variable not set"); + format!("https://eth-{chain}.g.alchemy.com/v2/{key}") +} + +pub fn setup_provider(chain: Chain) -> Provider> { + let provider_uri = get_provider_uri(chain); + Provider::new_client(&provider_uri, 10, 500).expect("could not instantiate HTTP Provider") +} + +pub fn from_hex(s: &str) -> Vec { + let s = if s.len() % 2 == 1 { format!("0{s}") } else { s.to_string() }; + Vec::from_hex(s).unwrap() +} diff --git a/axiom-eth/src/providers/receipt.rs b/axiom-eth/src/providers/receipt.rs new file mode 100644 index 00000000..eb2284e4 --- /dev/null +++ b/axiom-eth/src/providers/receipt.rs @@ -0,0 +1,239 @@ +use std::sync::Arc; + +use cita_trie::{MemoryDB, PatriciaTrie, Trie}; +use ethers_core::types::{TransactionReceipt, H256}; +use ethers_providers::{JsonRpcClient, Middleware, Provider}; +use hasher::HasherKeccak; +use rlp::RlpStream; +use tokio::runtime::Runtime; + +use crate::receipt::{calc_max_val_len as rc_calc_max_val_len, EthBlockReceiptInput}; +use crate::{mpt::MPTInput, providers::from_hex, receipt::EthReceiptInput}; + +use super::block::{get_block_rlp, get_blocks}; + +/// This is a fix to +pub fn rlp_bytes(receipt: TransactionReceipt) -> Vec { + let mut s = RlpStream::new(); + s.begin_list(4); + if let Some(post_state) = receipt.root { + s.append(&post_state); + } else { + s.append(&receipt.status.expect("No post-state or status in receipt")); + } + s.append(&receipt.cumulative_gas_used); + s.append(&receipt.logs_bloom); + s.append_list(&receipt.logs); + let bytesa = s.out(); + let mut rlp = bytesa.to_vec(); + if let Some(tx_type) = receipt.transaction_type { + if tx_type.as_u32() > 0 { + rlp = [vec![tx_type.as_u32() as u8], rlp].concat(); + } + } + rlp +} + +async fn get_receipt_query( + receipt_pf_max_depth: usize, + receipts: Vec, // all receipts in the block + tx_idx: usize, + receipts_root: H256, + max_data_byte_len: usize, + max_log_num: usize, + topic_num_bounds: (usize, usize), +) -> EthReceiptInput { + let memdb = Arc::new(MemoryDB::new(true)); + let hasher = Arc::new(HasherKeccak::new()); + let mut trie = PatriciaTrie::new(Arc::clone(&memdb), Arc::clone(&hasher)); + let num_rcs = receipts.len(); + let mut vals_cache = Vec::new(); + for (idx, receipt) in receipts.into_iter().enumerate() { + let mut rc_key = rlp::encode(&from_hex(&format!("{idx:x}"))).to_vec(); + if idx == 0 { + rc_key = vec![0x80]; + } + let rc_rlp = rlp_bytes(receipt); + trie.insert(rc_key, rc_rlp.clone()).unwrap(); + vals_cache.push(rc_rlp); + } + let root = trie.root().unwrap(); + trie = PatriciaTrie::from(Arc::clone(&memdb), Arc::clone(&hasher), &root).unwrap(); + let root = trie.root().unwrap(); + assert!(root == receipts_root.as_bytes().to_vec()); + + let mut rc_key = rlp::encode(&from_hex(&format!("{tx_idx:x}"))).to_vec(); + if tx_idx == 0 { + rc_key = vec![0x80]; + } + assert!(tx_idx < num_rcs, "Invalid transaction index"); + let rc_rlp = vals_cache[tx_idx].clone(); + let proof = MPTInput { + path: (&rc_key).into(), + value: rc_rlp, + root_hash: receipts_root, + proof: trie.get_proof(&rc_key).unwrap(), + value_max_byte_len: rc_calc_max_val_len(max_data_byte_len, max_log_num, topic_num_bounds), + max_depth: receipt_pf_max_depth, + slot_is_empty: false, + max_key_byte_len: 3, + key_byte_len: Some(rc_key.len()), + }; + EthReceiptInput { idx: tx_idx, proof } +} + +pub fn get_block_receipts( + provider: &Provider

, + block_number: u32, +) -> Vec { + let rt = Runtime::new().unwrap(); + let block2 = rt + .block_on(provider.get_block(block_number as u64)) + .unwrap() + .unwrap_or_else(|| panic!("Block {block_number} not found")); + let mut receipts = Vec::new(); + for transaction in block2.transactions { + let receipt = rt + .block_on(provider.get_transaction_receipt(transaction)) + .unwrap() + .unwrap_or_else(|| panic!("Transaction {transaction} not found")); + receipts.push(receipt); + } + receipts +} + +pub fn get_block_receipt_input( + provider: &Provider

, + tx_hash: H256, + receipt_pf_max_depth: usize, + max_data_byte_len: usize, + max_log_num: usize, + topic_num_bounds: (usize, usize), +) -> EthBlockReceiptInput { + let rt = Runtime::new().unwrap(); + let tx = rt + .block_on(provider.get_transaction(tx_hash)) + .unwrap() + .unwrap_or_else(|| panic!("Transaction {tx_hash} not found")); + let block_number = tx.block_number.unwrap().as_u32(); + let block = get_blocks(provider, [block_number as u64]).unwrap().pop().unwrap().unwrap(); + let block_hash = block.hash.unwrap(); + let block_header = get_block_rlp(&block); + let receipts = get_block_receipts(provider, block_number); + // requested receipt pf + let receipt = rt.block_on(get_receipt_query( + receipt_pf_max_depth, + receipts, + tx.transaction_index.unwrap().as_usize(), + block.receipts_root, + max_data_byte_len, + max_log_num, + topic_num_bounds, + )); + EthBlockReceiptInput { block_number, block_hash, block_header, receipt } +} + +#[cfg(test)] +mod tests { + use std::{env::var, sync::Arc}; + + use cita_trie::{MemoryDB, PatriciaTrie, Trie}; + use ethers_core::types::Chain; + use ethers_providers::Middleware; + use hasher::HasherKeccak; + use tokio::runtime::Runtime; + + use crate::providers::{from_hex, receipt::rlp_bytes, setup_provider}; + + #[test] + fn correct_root() { + let block_number = (var("BLOCK_NUM").expect("BLOCK_NUM environmental variable not set")) + .parse::() + .unwrap(); + let provider = setup_provider(Chain::Mainnet); + let rt = Runtime::new().unwrap(); + let block2 = rt + .block_on(provider.get_block(block_number as u64)) + .unwrap() + .unwrap_or_else(|| panic!("Block {block_number} not found")); + let mut receipts = Vec::new(); + for transaction in block2.transactions { + receipts.push(rt.block_on(provider.get_transaction_receipt(transaction)).unwrap()) + } + let receipts_root = block2.receipts_root; + let memdb = Arc::new(MemoryDB::new(true)); + let hasher = Arc::new(HasherKeccak::new()); + let mut trie = PatriciaTrie::new(Arc::clone(&memdb), Arc::clone(&hasher)); + let num_rcs = receipts.len(); + let mut vals_cache = Vec::new(); + #[allow(clippy::needless_range_loop)] + for idx in 0..num_rcs { + let rc = receipts[idx].clone(); + match rc { + None => {} + Some(rc) => { + let mut rc_key = + rlp::encode(&from_hex(&format!("{idx:x}").to_string())).to_vec(); + if idx == 0 { + rc_key = vec![0x80]; + } + let rc_rlp = rlp_bytes(rc); + println!("RC_RLP: {rc_rlp:02x?}"); + trie.insert(rc_key.clone(), rc_rlp.clone()).unwrap(); + vals_cache.push(rc_rlp); + } + } + } + let root = trie.root().unwrap(); + trie = PatriciaTrie::from(Arc::clone(&memdb), Arc::clone(&hasher), &root).unwrap(); + let root = trie.root().unwrap(); + assert!(root == receipts_root.as_bytes().to_vec()); + } +} + +#[cfg(test)] +mod data_analysis { + use std::{fs::File, io::Write}; + + use ethers_core::types::Chain; + + use crate::providers::setup_provider; + + use super::*; + #[test] + #[ignore] + pub fn find_receipt_lens() -> Result<(), Box> { + let provider = setup_provider(Chain::Mainnet); + + let mut data_file = File::create("data.txt").expect("creation failed"); + for i in 0..100 { + let receipts = get_block_receipts(&provider, 17578525 - i); + for (j, receipt) in receipts.into_iter().enumerate() { + let _len = { + let mut s = RlpStream::new(); + s.append_list(&receipt.logs); + let rlp_bytes: Vec = s.out().freeze().into(); + rlp_bytes.len() + }; + //let len = transaction.input.len(); + //let len = receipts[j].logs.len(); + for i in 0..receipt.logs.len() { + let len = receipt.logs[i].data.len(); + data_file + .write_all( + (len.to_string() + + ", " + + &j.to_string() + + ", " + + &(17578525 - i).to_string() + + ", " + + "\n") + .as_bytes(), + ) + .expect("write failed"); + } + } + } + Ok(()) + } +} diff --git a/axiom-eth/src/providers/storage.rs b/axiom-eth/src/providers/storage.rs new file mode 100644 index 00000000..bca855ba --- /dev/null +++ b/axiom-eth/src/providers/storage.rs @@ -0,0 +1,271 @@ +use ethers_core::{ + types::{Address, Bytes, EIP1186ProofResponse, H256}, + utils::keccak256, +}; +use ethers_providers::{JsonRpcClient, Middleware, Provider}; +use futures::future::join_all; +use rlp::{Encodable, Rlp, RlpStream}; +use tokio::runtime::Runtime; + +use crate::{ + mpt::{MPTInput, KECCAK_RLP_EMPTY_STRING}, + providers::account::get_acct_list, + storage::{ + circuit::{EthBlockStorageInput, EthStorageInput}, + ACCOUNT_PROOF_VALUE_MAX_BYTE_LEN, STORAGE_PROOF_VALUE_MAX_BYTE_LEN, + }, +}; + +use super::block::get_block_rlp; + +/// stateRoot is not provided and set to H256(0) +pub fn json_to_mpt_input( + pf: EIP1186ProofResponse, + acct_pf_max_depth: usize, + storage_pf_max_depth: usize, +) -> EthStorageInput { + let addr = pf.address; + let acct_key = H256(keccak256(addr)); + let slot_is_empty = !is_assigned_slot(&acct_key, &pf.account_proof); + log::debug!("address: {addr}, account is empty: {slot_is_empty}"); + let acct_state = get_acct_list(&pf); + // JSON-RPC Provider has been known to provide wrong codeHash vs MPT value, so we extract value from MPT proof itself + // If the proof ends with a branch node that contains the leaf node, we extract the + // leaf node and add it to the end of the proof so that our mpt implementation can + // handle it. + let get_new_proof_and_value = |key: &H256, proof: &[Bytes]| { + if proof.is_empty() { + return (vec![], vec![]); + } + let decode = Rlp::new(proof.last().unwrap()); + assert!(decode.is_list()); + let add_leaf = decode.item_count().unwrap() == 17; + let mut new_proof: Vec> = proof.iter().map(|x| x.to_vec()).collect(); + let value; + if add_leaf { + let last_nibble = last_nibble(key, proof); + assert!(last_nibble < 16); + let leaf = decode.at(last_nibble as usize).unwrap(); + if let Ok(leaf) = leaf.as_list::>() { + if leaf.is_empty() { + // this is a non-inclusion proof + value = vec![0x80]; + } else { + assert_eq!(leaf.len(), 2); + value = leaf.last().unwrap().to_vec(); + let leaf = rlp::encode_list::, Vec>(&leaf).to_vec(); + new_proof.push(leaf); + } + } else { + value = leaf.as_val().unwrap() + } + } else { + value = decode.val_at(1).unwrap() + } + (new_proof, value) + }; + let (new_acct_pf, mut acct_state_rlp) = get_new_proof_and_value(&acct_key, &pf.account_proof); + let mut storage_root = pf.storage_hash; + if slot_is_empty { + acct_state_rlp = EMPTY_ACCOUNT_RLP.clone(); + // If account is empty, then storage root should be + // - null root hash = keccak(rlp("")) = keccak(0x80) = 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421 + storage_root = H256::from_slice(&KECCAK_RLP_EMPTY_STRING); + } + let acct_pf = MPTInput { + path: acct_key.into(), + value: acct_state_rlp, + root_hash: H256([0u8; 32]), // STATE ROOT IS UNKNOWN IN THIS FUNCTION AND NOT SET + proof: new_acct_pf, + value_max_byte_len: ACCOUNT_PROOF_VALUE_MAX_BYTE_LEN, + max_depth: acct_pf_max_depth, + slot_is_empty, + max_key_byte_len: 32, + key_byte_len: None, + }; + let storage_pfs = pf + .storage_proof + .into_iter() + .map(|storage_pf| { + let path = H256(keccak256(storage_pf.key)); + let (new_proof, mut value) = get_new_proof_and_value(&path, &storage_pf.proof); + let new_proof_bytes: Vec = + new_proof.clone().into_iter().map(Bytes::from_iter).collect(); + let slot_is_empty = !is_assigned_slot(&path, &new_proof_bytes); + if slot_is_empty { + value = vec![0x80]; + } + assert_eq!(&value, storage_pf.value.rlp_bytes().as_ref()); + log::info!( + "address: {addr}, slot: {}, storage slot is empty: {slot_is_empty}", + storage_pf.key + ); + ( + storage_pf.key, + storage_pf.value, + MPTInput { + path: path.into(), + value, + root_hash: storage_root, + proof: new_proof, + value_max_byte_len: STORAGE_PROOF_VALUE_MAX_BYTE_LEN, + max_depth: storage_pf_max_depth, + slot_is_empty, + max_key_byte_len: 32, + key_byte_len: None, + }, + ) + }) + .collect(); + EthStorageInput { addr, acct_state, acct_pf, storage_pfs } +} + +/// Does not provide state root +async fn get_storage_query( + provider: &Provider

, + block_number: u64, + addr: Address, + slots: Vec, + acct_pf_max_depth: usize, + storage_pf_max_depth: usize, +) -> EthStorageInput { + log::debug!("block number: {block_number}"); + let pf = provider.get_proof(addr, slots, Some(block_number.into())).await.unwrap(); + json_to_mpt_input(pf, acct_pf_max_depth, storage_pf_max_depth) +} + +pub fn get_storage_queries( + provider: &Provider

, + queries: Vec<(u64, Address, H256)>, + acct_pf_max_depth: usize, + storage_pf_max_depth: usize, +) -> Vec { + let rt = Runtime::new().unwrap(); + rt.block_on(join_all(queries.into_iter().map(|(block_number, addr, slot)| { + get_storage_query( + provider, + block_number, + addr, + vec![slot], + acct_pf_max_depth, + storage_pf_max_depth, + ) + }))) +} + +pub fn get_block_storage_input( + provider: &Provider

, + block_number: u32, + addr: Address, + slots: Vec, + acct_pf_max_depth: usize, + storage_pf_max_depth: usize, +) -> EthBlockStorageInput { + let rt = Runtime::new().unwrap(); + let block = rt + .block_on(provider.get_block(block_number as u64)) + .unwrap() + .unwrap_or_else(|| panic!("Block {block_number} not found")); + let block_hash = block.hash.unwrap(); + let block_header = get_block_rlp(&block); + + let mut storage = rt.block_on(get_storage_query( + provider, + block_number as u64, + addr, + slots, + acct_pf_max_depth, + storage_pf_max_depth, + )); + storage.acct_pf.root_hash = block.state_root; + + EthBlockStorageInput { block, block_number, block_hash, block_header, storage } +} + +pub fn is_assigned_slot(key: &H256, proof: &[Bytes]) -> bool { + let mut key_nibbles = Vec::new(); + for &byte in key.as_bytes() { + key_nibbles.push(byte / 16); + key_nibbles.push(byte % 16); + } + let mut key_frags = Vec::new(); + let mut path_idx = 0; + for node in proof.iter() { + let rlp = Rlp::new(node); + if rlp.item_count().unwrap() == 2 { + let path = rlp.at(0).unwrap().data().unwrap(); + let is_odd = (path[0] / 16 == 1u8) || (path[0] / 16 == 3u8); + let mut frag = Vec::new(); + if is_odd { + frag.push(path[0] % 16); + path_idx += 1; + } + for byte in path.iter().skip(1) { + frag.push(*byte / 16); + frag.push(*byte % 16); + path_idx += 2; + } + key_frags.extend(frag); + } else { + key_frags.extend(vec![key_nibbles[path_idx]]); + path_idx += 1; + } + } + if path_idx == 64 { + for idx in 0..64 { + if key_nibbles[idx] != key_frags[idx] { + return false; + } + } + } else { + return false; + } + true +} + +/// In the case that we have a branch node at the end and we want to +/// read the next node, this tells us which entry to look at. +pub fn last_nibble(key: &H256, proof: &[Bytes]) -> u8 { + let mut key_nibbles = Vec::new(); + for &byte in key.as_bytes() { + key_nibbles.push(byte / 16); + key_nibbles.push(byte % 16); + } + let mut key_frags = Vec::new(); + let mut path_idx = 0; + for node in proof.iter() { + let rlp = Rlp::new(node); + if rlp.item_count().unwrap() == 2 { + let path = rlp.at(0).unwrap().data().unwrap(); + let is_odd = (path[0] / 16 == 1u8) || (path[0] / 16 == 3u8); + let mut frag = Vec::new(); + if is_odd { + frag.push(path[0] % 16); + path_idx += 1; + } + for byte in path.iter().skip(1) { + frag.push(*byte / 16); + frag.push(*byte % 16); + path_idx += 2; + } + key_frags.extend(frag); + } else { + key_frags.extend(vec![key_nibbles[path_idx]]); + path_idx += 1; + } + } + key_nibbles[path_idx - 1] +} + +pub fn empty_account_rlp() -> Vec { + let mut rlp = RlpStream::new_list(4); + rlp.append(&0u8); + rlp.append(&0u8); + rlp.append(&H256::from_slice(&KECCAK_RLP_EMPTY_STRING)); // null storageRoot + rlp.append(&H256::zero()); + rlp.out().to_vec() +} + +lazy_static::lazy_static! { + pub static ref EMPTY_ACCOUNT_RLP: Vec = empty_account_rlp(); +} diff --git a/axiom-eth/src/providers/transaction.rs b/axiom-eth/src/providers/transaction.rs new file mode 100644 index 00000000..50479da0 --- /dev/null +++ b/axiom-eth/src/providers/transaction.rs @@ -0,0 +1,322 @@ +use std::sync::Arc; + +use cita_trie::{MemoryDB, PatriciaTrie, Trie}; +use ethers_core::types::{Transaction, H256}; +use ethers_providers::{JsonRpcClient, Middleware, Provider}; +use hasher::HasherKeccak; +use tokio::runtime::Runtime; + +use crate::{ + mpt::MPTInput, + providers::from_hex, + transaction::{ + calc_max_val_len as tx_calc_max_val_len, EthBlockTransactionsInput, EthTransactionLenProof, + EthTransactionProof, + }, +}; + +use super::block::get_block_rlp; + +/// Creates transaction proof by reconstructing the transaction MPTrie. +/// `transactions` should be all transactions in the block +async fn get_transaction_query( + transaction_pf_max_depth: usize, + transactions: Vec, + idxs: Vec, + transactions_root: H256, + max_data_byte_len: usize, + max_access_list_len: usize, + enable_types: [bool; 3], + constrain_len: bool, +) -> (Vec, Option) { + let memdb = Arc::new(MemoryDB::new(true)); + let hasher = Arc::new(HasherKeccak::new()); + let mut trie = PatriciaTrie::new(Arc::clone(&memdb), Arc::clone(&hasher)); + let num_txs = transactions.len(); + let mut pfs = Vec::new(); + let mut vals_cache = Vec::new(); + #[allow(clippy::needless_range_loop)] + for idx in 0..num_txs { + let tx = transactions[idx].clone(); + let mut tx_key = rlp::encode(&from_hex(&format!("{idx:x}").to_string())).to_vec(); + if idx == 0 { + tx_key = vec![0x80]; + } + let tx_rlp = tx.rlp().to_vec(); + trie.insert(tx_key.clone(), tx_rlp.clone()).unwrap(); + vals_cache.push(tx_rlp); + } + let root = trie.root().unwrap(); + trie = PatriciaTrie::from(Arc::clone(&memdb), Arc::clone(&hasher), &root).unwrap(); + let root = trie.root().unwrap(); + assert_eq!(&root, transactions_root.as_bytes()); + + for idx in idxs { + let mut tx_key = rlp::encode(&from_hex(&format!("{idx:x}").to_string())).to_vec(); + if idx == 0 { + tx_key = vec![0x80]; + } + let tx_rlp = if idx < num_txs { vals_cache[idx].clone() } else { vec![0xba; 32] }; + let proof = MPTInput { + path: (&tx_key).into(), + value: tx_rlp, + root_hash: transactions_root, + proof: trie.get_proof(&tx_key).unwrap(), + value_max_byte_len: tx_calc_max_val_len( + max_data_byte_len, + max_access_list_len, + enable_types, + ), + max_depth: transaction_pf_max_depth, + slot_is_empty: idx >= num_txs, + max_key_byte_len: 3, + key_byte_len: Some(tx_key.len()), + }; + pfs.push(EthTransactionProof { tx_index: idx, proof }); + } + let pf0 = if num_txs > 0 { + let mut tx_key = rlp::encode(&from_hex(&format!("{:x}", num_txs - 1))).to_vec(); + if num_txs == 1 { + tx_key = vec![0x80]; + } + let tx_rlp = vals_cache[num_txs - 1].clone(); + EthTransactionProof { + tx_index: num_txs - 1, + proof: MPTInput { + path: (&tx_key).into(), + value: tx_rlp, + root_hash: transactions_root, + proof: trie.get_proof(&tx_key).unwrap(), + value_max_byte_len: tx_calc_max_val_len( + max_data_byte_len, + max_access_list_len, + enable_types, + ), + max_depth: transaction_pf_max_depth, + slot_is_empty: false, + max_key_byte_len: 3, + key_byte_len: Some(tx_key.len()), + }, + } + } else { + let tx_key = rlp::encode(&from_hex(&format!("{:x}", num_txs + 1))).to_vec(); + let tx_rlp = [0xba; 32].to_vec(); + EthTransactionProof { + tx_index: num_txs + 1, + proof: MPTInput { + path: (&tx_key).into(), + value: tx_rlp, + root_hash: transactions_root, + proof: trie.get_proof(&tx_key).unwrap(), + value_max_byte_len: tx_calc_max_val_len( + max_data_byte_len, + max_access_list_len, + enable_types, + ), + max_depth: transaction_pf_max_depth, + slot_is_empty: true, + max_key_byte_len: 3, + key_byte_len: Some(tx_key.len()), + }, + } + }; + + let mut tx_key = rlp::encode(&from_hex(&format!("{num_txs:x}"))).to_vec(); + if num_txs == 0 { + tx_key = vec![0x80]; + } + let tx_rlp = [0xba; 2].to_vec(); + let pf1 = EthTransactionProof { + tx_index: num_txs, + proof: MPTInput { + path: (&tx_key).into(), + value: tx_rlp, + root_hash: transactions_root, + proof: trie.get_proof(&tx_key).unwrap(), + value_max_byte_len: tx_calc_max_val_len( + max_data_byte_len, + max_access_list_len, + enable_types, + ), + max_depth: transaction_pf_max_depth, + slot_is_empty: true, + max_key_byte_len: 3, + key_byte_len: Some(tx_key.len()), + }, + }; + let len_proof = if constrain_len { + Some(EthTransactionLenProof { inclusion: pf0, noninclusion: pf1 }) + } else { + None + }; + (pfs, len_proof) +} + +pub fn get_block_transaction_input( + provider: &Provider

, + idxs: Vec, + block_number: u32, + transaction_pf_max_depth: usize, + max_data_byte_len: usize, + max_access_list_len: usize, + enable_types: [bool; 3], + constrain_len: bool, +) -> EthBlockTransactionsInput { + let rt = Runtime::new().unwrap(); + let block = rt + .block_on(provider.get_block_with_txs(block_number as u64)) + .unwrap() + .unwrap_or_else(|| panic!("Block {block_number} not found")); + let block2 = rt + .block_on(provider.get_block(block_number as u64)) + .unwrap() + .unwrap_or_else(|| panic!("Block {block_number} not found")); + + for i in 0..block.transactions.len() { + assert_eq!(block.transactions[i].hash(), block2.transactions[i]); + } + + let block_hash = block.hash.unwrap(); + let block_header = get_block_rlp(&block); + + let (tx_proofs, len_proof) = rt.block_on(get_transaction_query( + transaction_pf_max_depth, + block.clone().transactions, + idxs, + block.transactions_root, + max_data_byte_len, + max_access_list_len, + enable_types, + constrain_len, + )); + // println!("{block_header:?}"); + // println!("{tx_input:?}"); + EthBlockTransactionsInput { + block, + block_number, + block_hash, + block_header, + tx_proofs, + len_proof, + } +} + +pub fn get_block_transaction_len( + provider: &Provider

, + block_number: u32, +) -> usize { + let rt = Runtime::new().unwrap(); + let block2 = rt + .block_on(provider.get_block(block_number as u64)) + .unwrap() + .unwrap_or_else(|| panic!("Block {block_number} not found")); + block2.transactions.len() +} + +pub fn get_block_transactions( + provider: &Provider

, + block_number: u32, +) -> Vec { + let rt = Runtime::new().unwrap(); + let block2 = rt + .block_on(provider.get_block_with_txs(block_number as u64)) + .unwrap() + .unwrap_or_else(|| panic!("Block {block_number} not found")); + block2.transactions +} + +pub fn get_block_access_list_num( + provider: &Provider

, + block_number: u32, +) -> usize { + let rt = Runtime::new().unwrap(); + let block2 = rt + .block_on(provider.get_block_with_txs(block_number as u64)) + .unwrap() + .unwrap_or_else(|| panic!("Block {block_number} not found")); + let mut cnt: usize = 0; + for tx in block2.transactions { + match tx.access_list { + None => {} + Some(al) => { + cnt += !al.0.is_empty() as usize; + } + } + } + cnt +} + +#[cfg(test)] +mod data_analysis { + use std::{fs::File, io::Write}; + + use ethers_core::types::Chain; + use rlp::RlpStream; + + use crate::providers::setup_provider; + + use super::{get_block_access_list_num, get_block_transaction_len, get_block_transactions}; + + #[test] + #[ignore] + pub fn find_good_block256() { + let provider = setup_provider(Chain::Mainnet); + for block_number in 5000000..6000000 { + let num_tx = get_block_transaction_len(&provider, block_number.try_into().unwrap()); + if num_tx > 256 { + println!("Desired Block: {block_number:?}"); + } + } + } + + #[test] + #[ignore] + pub fn find_access_lists() { + let provider = setup_provider(Chain::Mainnet); + let mut trend = Vec::new(); + + let mut data_file = File::create("data.txt").expect("creation failed"); + for i in 0..100 { + let cnt = get_block_access_list_num(&provider, 17578525 - i); + trend.push((17578525 - i, cnt)); + data_file.write_all((cnt.to_string() + "\n").as_bytes()).expect("write failed"); + } + } + + #[test] + #[ignore] + pub fn find_transaction_lens() { + let provider = setup_provider(Chain::Mainnet); + let mut trend = Vec::new(); + + let mut data_file = File::create("data.txt").expect("creation failed"); + for i in 0..100 { + let transactions = get_block_transactions(&provider, 17578525 - i); + for (j, transaction) in transactions.into_iter().enumerate() { + trend.push((17578525 - i, transaction.input.len())); + let _len = match transaction.access_list { + Some(a_list) => { + let mut s = RlpStream::new(); + s.append(&a_list); + let rlp_bytes: Vec = s.out().freeze().into(); + rlp_bytes.len() + } + None => 0, + }; + let len = transaction.input.len(); + data_file + .write_all( + (len.to_string() + + ", " + + &j.to_string() + + ", " + + &(17578525 - i).to_string() + + ", " + + "\n") + .as_bytes(), + ) + .expect("write failed"); + } + } + } +} diff --git a/axiom-eth/src/receipt/mod.rs b/axiom-eth/src/receipt/mod.rs new file mode 100644 index 00000000..6be1ec72 --- /dev/null +++ b/axiom-eth/src/receipt/mod.rs @@ -0,0 +1,283 @@ +//! See https://hackmd.io/@axiom/H1TYkiBt2 for receipt data format + +use crate::{ + block_header::EthBlockHeaderChip, + keccak::KeccakChip, + mpt::MPTChip, + rlc::{ + chip::RlcChip, + circuit::builder::{RlcCircuitBuilder, RlcContextPair}, + }, + rlp::{evaluate_byte_array, max_rlp_len_len, RlpChip}, + transaction::TRANSACTION_IDX_MAX_LEN, + utils::circuit_utils::constrain_no_leading_zeros, +}; + +use ethers_core::types::Chain; +use halo2_base::{ + gates::{ + flex_gate::threads::parallelize_core, GateChip, GateInstructions, RangeChip, + RangeInstructions, + }, + AssignedValue, Context, + QuantumCell::{Constant, Existing}, +}; +use itertools::Itertools; + +//pub mod task; +#[cfg(all(test, feature = "providers"))] +pub mod tests; +mod types; +use crate::Field; +use serde::{Deserialize, Serialize}; +pub use types::*; + +pub const RECEIPT_NUM_FIELDS: usize = 4; +pub const RECEIPT_FIELDS_LOG_INDEX: usize = 3; + +pub fn calc_max_val_len( + max_data_byte_len: usize, + max_log_num: usize, + (_min_topic_num, max_topic_num): (usize, usize), +) -> usize { + let max_log_len = calc_max_log_len(max_data_byte_len, max_topic_num); + 4 + 33 + 33 + 259 + 4 + max_log_num * max_log_len +} + +fn calc_max_log_len(max_data_byte_len: usize, max_topic_num: usize) -> usize { + 3 + 21 + 3 + 33 * max_topic_num + 3 + max_data_byte_len + 1 +} + +/// Configuration parameters for [EthReceiptChip] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Hash, Default)] +pub struct EthReceiptChipParams { + pub max_data_byte_len: usize, + pub max_log_num: usize, + pub topic_num_bounds: (usize, usize), // min, max + /// Must be provided if using functions involving block header + pub network: Option, +} + +#[derive(Clone, Debug)] +pub struct EthReceiptChip<'chip, F: Field> { + pub mpt: &'chip MPTChip<'chip, F>, + pub params: EthReceiptChipParams, +} + +impl<'chip, F: Field> EthReceiptChip<'chip, F> { + pub fn new(mpt: &'chip MPTChip<'chip, F>, params: EthReceiptChipParams) -> Self { + Self { mpt, params } + } + pub fn gate(&self) -> &GateChip { + self.mpt.gate() + } + + pub fn range(&self) -> &RangeChip { + self.mpt.range() + } + + pub fn rlc(&self) -> &RlcChip { + self.mpt.rlc() + } + + pub fn rlp(&self) -> RlpChip { + self.mpt.rlp() + } + + pub fn keccak(&self) -> &KeccakChip { + self.mpt.keccak() + } + + pub fn mpt(&self) -> &'chip MPTChip<'chip, F> { + self.mpt + } + + pub fn network(&self) -> Option { + self.params.network + } + + pub fn block_header_chip(&self) -> EthBlockHeaderChip { + EthBlockHeaderChip::new_from_network( + self.rlp(), + self.network().expect("Must provide network to access block header chip"), + ) + } + + /// FirstPhase of proving the inclusion **or** exclusion of a transaction index within a receipt root. + /// In the case of + /// - inclusion: then parses the receipt + /// - exclusion: `input.proof.slot_is_empty` is true, and we return `tx_type = -1, value = rlp(0x00)`. + pub fn parse_receipt_proof_phase0( + &self, + ctx: &mut Context, + input: EthReceiptInputAssigned, + ) -> EthReceiptWitness { + let EthReceiptChipParams { max_data_byte_len, max_log_num, topic_num_bounds, .. } = + self.params; + let EthReceiptInputAssigned { tx_idx: transaction_index, proof } = input; + // Load value early to avoid borrow errors + let slot_is_empty = proof.slot_is_empty; + // check key is rlp(idx) + let idx_witness = self.rlp().decompose_rlp_field_phase0( + ctx, + proof.key_bytes.clone(), + TRANSACTION_IDX_MAX_LEN, + ); + let tx_idx = + evaluate_byte_array(ctx, self.gate(), &idx_witness.field_cells, idx_witness.field_len); + ctx.constrain_equal(&tx_idx, &transaction_index); + constrain_no_leading_zeros( + ctx, + self.gate(), + &idx_witness.field_cells, + idx_witness.field_len, + ); + + // check MPT inclusion + let mpt_witness = self.mpt.parse_mpt_inclusion_phase0(ctx, proof); + + // parse receipt + // when we disable all types, we use that as a flag to parse dummy values which are two bytes long + let type_is_not_zero = + self.range().is_less_than(ctx, mpt_witness.value_bytes[0], Constant(F::from(128)), 8); + // if the first byte is greater than 0xf7, the type is zero. Otherwise, the type is the first byte. + let mut tx_type = self.gate().mul(ctx, type_is_not_zero, mpt_witness.value_bytes[0]); + let max_val_len = calc_max_val_len(max_data_byte_len, max_log_num, topic_num_bounds); + let mut new_value_witness = Vec::with_capacity(max_val_len); + let slot_is_full = self.gate().not(ctx, slot_is_empty); + tx_type = self.gate().mul(ctx, tx_type, slot_is_full); + // tx_type = -1 if and only if the slot is empty, serves as a flag + tx_type = self.gate().sub(ctx, tx_type, slot_is_empty); + // parse the zeroes string if the slot is empty so that we don't run into errors + for i in 0..max_val_len { + let mut val_byte = self.gate().select( + ctx, + mpt_witness + .value_bytes + .get(i + 1) + .map(|a| Existing(*a)) + .unwrap_or(Constant(F::ZERO)), + mpt_witness.value_bytes[i], + type_is_not_zero, + ); + val_byte = if i == 0 { + // 0xc100 = rlp(0x00) + self.gate().select(ctx, val_byte, Constant(F::from(0xc1)), slot_is_full) + } else { + self.gate().mul(ctx, val_byte, slot_is_full) + }; + new_value_witness.push(val_byte); + } + // max byte length of each log + let max_log_len = calc_max_log_len(max_data_byte_len, topic_num_bounds.1); + // max byte length of rlp encoding of all logs + let max_logs_rlp_len = + 1 + max_rlp_len_len(max_log_len * max_log_num) + max_log_len * max_log_num; + let max_field_lens = [33, 33, 259, max_logs_rlp_len]; + let value = + self.rlp().decompose_rlp_array_phase0(ctx, new_value_witness, &max_field_lens, true); + let max_log_lens = vec![max_log_len; max_log_num]; + let logs = self.rlp().decompose_rlp_array_phase0( + ctx, + value.field_witness[RECEIPT_FIELDS_LOG_INDEX].encoded_item.clone(), + &max_log_lens, + true, + ); + + EthReceiptWitness { receipt_type: tx_type, tx_idx, idx_witness, value, logs, mpt_witness } + } + + /// SecondPhase of proving inclusion **or** exclusion of a transaction index in receipt root, and then parses + /// the receipt. See [`parse_receipt_proof_phase0`] for more details. + pub fn parse_receipt_proof_phase1( + &self, + (ctx_gate, ctx_rlc): RlcContextPair, + witness: EthReceiptWitness, + ) -> EthReceiptTrace { + // Comments below just to log what load_rlc_cache calls are done in the internal functions: + self.rlp().decompose_rlp_field_phase1((ctx_gate, ctx_rlc), witness.idx_witness); + // load_rlc_cache bit_length(2*mpt_witness.key_byte_len) + self.mpt.parse_mpt_inclusion_phase1((ctx_gate, ctx_rlc), witness.mpt_witness); + // load rlc_cache bit_length(value_witness.rlp_field.len()) + let trace = self.rlp().decompose_rlp_array_phase1((ctx_gate, ctx_rlc), witness.value, true); + let value_trace = trace.field_trace; + self.rlp().decompose_rlp_array_phase1((ctx_gate, ctx_rlc), witness.logs, true); + EthReceiptTrace { receipt_type: witness.receipt_type, value_trace } + } + + /// Parallelizes `parse_receipt_proof_phase0`. + pub fn parse_receipt_proofs_phase0( + &self, + builder: &mut RlcCircuitBuilder, + input: Vec>, + ) -> Vec> { + parallelize_core(builder.base.pool(0), input, |ctx, input| { + self.parse_receipt_proof_phase0(ctx, input) + }) + } + + /// Parallelizes `parse_receipt_proof_phase1` + pub fn parse_receipt_proofs_phase1( + &self, + builder: &mut RlcCircuitBuilder, + receipt_witness: Vec>, + ) -> Vec> { + // load rlc cache should be done globally; no longer done here + builder.parallelize_phase1(receipt_witness, |(ctx_gate, ctx_rlc), witness| { + self.parse_receipt_proof_phase1((ctx_gate, ctx_rlc), witness) + }) + } + + /// Extracts the field at `field_idx` from the given rlp decomposition of a receipt. + pub fn extract_receipt_field( + &self, + ctx: &mut Context, + witness: &EthReceiptWitness, + field_idx: AssignedValue, + ) -> EthReceiptFieldWitness { + let zero = ctx.load_zero(); + let field_witness = &witness.value.field_witness; + assert_eq!(field_witness.len(), RECEIPT_NUM_FIELDS); + let ans_len = field_witness.iter().map(|w| w.field_cells.len()).max().unwrap(); + let mut value_bytes = Vec::with_capacity(ans_len); + let indicator = self.gate().idx_to_indicator(ctx, field_idx, RECEIPT_NUM_FIELDS); + for i in 0..ans_len { + let entries = + field_witness.iter().map(|w| *w.field_cells.get(i).unwrap_or(&zero)).collect_vec(); + let byte = self.gate().select_by_indicator(ctx, entries, indicator.clone()); + value_bytes.push(byte); + } + let lens = field_witness.iter().map(|w| w.field_len).collect_vec(); + let value_len = self.gate().select_by_indicator(ctx, lens, indicator); + EthReceiptFieldWitness { field_idx, value_bytes, value_len } + } + + /// Extracts the log at `log_idx` from the given rlp decomposition of a receipt. + pub fn extract_receipt_log( + &self, + ctx: &mut Context, + witness: &EthReceiptWitness, + log_idx: AssignedValue, + ) -> EthReceiptLogWitness { + let zero = ctx.load_zero(); + let max_log_num = self.params.max_log_num; + // select log by log_idx + let log_ind = self.gate().idx_to_indicator(ctx, log_idx, max_log_num); + let logs = &witness.logs.field_witness; + assert_eq!(witness.logs.field_witness.len(), max_log_num); + let max_log_len = logs.iter().map(|w| w.max_field_len).max().unwrap(); + let mut log_bytes = Vec::with_capacity(max_log_len); + for i in 0..max_log_len { + let byte = self.gate().select_by_indicator( + ctx, + logs.iter().map(|w| *w.encoded_item.get(i).unwrap_or(&zero)), + log_ind.clone(), + ); + log_bytes.push(byte); + } + let log_len = + self.gate().select_by_indicator(ctx, logs.iter().map(|w| w.encoded_item_len), log_ind); + + EthReceiptLogWitness { log_idx, log_len, log_bytes } + } +} diff --git a/axiom-eth/src/receipt/tests/data/field/single_rc_pos_test_legacy.json b/axiom-eth/src/receipt/tests/data/field/single_rc_pos_test_legacy.json new file mode 100644 index 00000000..cb1d4084 --- /dev/null +++ b/axiom-eth/src/receipt/tests/data/field/single_rc_pos_test_legacy.json @@ -0,0 +1,5 @@ +{ + "tx_hash": "4174b713caa15ee44fbbb73e2990c5ab794fe79f448a711430f34acf41d4b17f", + "field_idx": 1, + "log_idx": 1 +} \ No newline at end of file diff --git a/axiom-eth/src/receipt/tests/data/field/single_rc_pos_test_new.json b/axiom-eth/src/receipt/tests/data/field/single_rc_pos_test_new.json new file mode 100644 index 00000000..088772d1 --- /dev/null +++ b/axiom-eth/src/receipt/tests/data/field/single_rc_pos_test_new.json @@ -0,0 +1,5 @@ +{ + "tx_hash": "da6310baf32ffeb16afb7c2f064a7b64d79beccb585082753c083da15b8c38f2", + "field_idx": 1, + "log_idx": 1 +} \ No newline at end of file diff --git a/axiom-eth/src/receipt/tests/data/multi_tx_pos_test_legacy.json b/axiom-eth/src/receipt/tests/data/multi_tx_pos_test_legacy.json new file mode 100644 index 00000000..a3cd191a --- /dev/null +++ b/axiom-eth/src/receipt/tests/data/multi_tx_pos_test_legacy.json @@ -0,0 +1,5 @@ +{ + "idxs": [0, 1, 130, 257], + "block_number": 5000050 +} + \ No newline at end of file diff --git a/axiom-eth/src/receipt/tests/data/multi_tx_pos_test_new.json b/axiom-eth/src/receipt/tests/data/multi_tx_pos_test_new.json new file mode 100644 index 00000000..be83a028 --- /dev/null +++ b/axiom-eth/src/receipt/tests/data/multi_tx_pos_test_new.json @@ -0,0 +1,5 @@ +{ + "idxs": [0, 1], + "block_number": 17578525 +} + \ No newline at end of file diff --git a/axiom-eth/src/receipt/tests/data/single_rc_pos_test_legacy.json b/axiom-eth/src/receipt/tests/data/single_rc_pos_test_legacy.json new file mode 100644 index 00000000..349cde13 --- /dev/null +++ b/axiom-eth/src/receipt/tests/data/single_rc_pos_test_legacy.json @@ -0,0 +1,3 @@ +{ + "tx_hash": "4174b713caa15ee44fbbb73e2990c5ab794fe79f448a711430f34acf41d4b17f" +} \ No newline at end of file diff --git a/axiom-eth/src/receipt/tests/data/single_rc_pos_test_new.json b/axiom-eth/src/receipt/tests/data/single_rc_pos_test_new.json new file mode 100644 index 00000000..174b3e2d --- /dev/null +++ b/axiom-eth/src/receipt/tests/data/single_rc_pos_test_new.json @@ -0,0 +1,3 @@ +{ + "tx_hash": "da6310baf32ffeb16afb7c2f064a7b64d79beccb585082753c083da15b8c38f2" +} \ No newline at end of file diff --git a/axiom-eth/src/receipt/tests/data/stress_test.json b/axiom-eth/src/receipt/tests/data/stress_test.json new file mode 100644 index 00000000..9a037142 --- /dev/null +++ b/axiom-eth/src/receipt/tests/data/stress_test.json @@ -0,0 +1 @@ +10 \ No newline at end of file diff --git a/axiom-eth/src/receipt/tests/data/zero_tx_pos_test_legacy.json b/axiom-eth/src/receipt/tests/data/zero_tx_pos_test_legacy.json new file mode 100644 index 00000000..11c50660 --- /dev/null +++ b/axiom-eth/src/receipt/tests/data/zero_tx_pos_test_legacy.json @@ -0,0 +1,5 @@ +{ + "idxs": [], + "block_number": 1000 +} + \ No newline at end of file diff --git a/axiom-eth/src/receipt/tests/data/zero_tx_pos_test_new.json b/axiom-eth/src/receipt/tests/data/zero_tx_pos_test_new.json new file mode 100644 index 00000000..11c50660 --- /dev/null +++ b/axiom-eth/src/receipt/tests/data/zero_tx_pos_test_new.json @@ -0,0 +1,5 @@ +{ + "idxs": [], + "block_number": 1000 +} + \ No newline at end of file diff --git a/axiom-eth/src/receipt/tests/field.rs b/axiom-eth/src/receipt/tests/field.rs new file mode 100644 index 00000000..1e11994f --- /dev/null +++ b/axiom-eth/src/receipt/tests/field.rs @@ -0,0 +1,215 @@ +#![cfg(feature = "providers")] +use super::*; +use crate::block_header::get_block_header_rlp_max_lens; +use crate::providers::setup_provider; +use ethers_core::types::H256; + +use halo2_base::halo2_proofs::dev::MockProver; + +use ethers_providers::{JsonRpcClient, Provider}; +use halo2_base::halo2_proofs::halo2curves::bn256::Fr; +use serde::{Deserialize, Serialize}; +use std::fs::File; +use test_log::test; + +//pub mod field; + +#[derive(Clone, Debug)] +pub struct EthBlockReceiptFieldCircuit { + pub inputs: EthBlockReceiptInput, // public and private inputs + pub field_idx: usize, + pub log_idx: usize, + pub params: EthReceiptChipParams, + _marker: PhantomData, +} + +impl EthBlockReceiptFieldCircuit { + #[allow(clippy::too_many_arguments)] + #[cfg(feature = "providers")] + pub fn from_provider( + provider: &Provider

, + tx_hash: H256, + field_idx: usize, + log_idx: usize, + receipt_pf_max_depth: usize, + network: Chain, + max_data_byte_len: usize, + max_log_num: usize, + topic_num_bounds: (usize, usize), + ) -> Self { + use crate::providers::receipt::get_block_receipt_input; + + let inputs = get_block_receipt_input( + provider, + tx_hash, + receipt_pf_max_depth, + max_data_byte_len, + max_log_num, + topic_num_bounds, + ); + let params = EthReceiptChipParams { + max_data_byte_len, + max_log_num, + topic_num_bounds, + network: Some(network), + }; + Self { inputs, field_idx, log_idx, params, _marker: PhantomData } + } +} + +impl EthCircuitInstructions for EthBlockReceiptFieldCircuit { + type FirstPhasePayload = (EthBlockHeaderWitness, EthReceiptWitness, EthReceiptChipParams); + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + ) -> Self::FirstPhasePayload { + let ctx = builder.base.main(FIRST_PHASE); + let chip = EthReceiptChip::new(mpt, self.params); + let max_header_len = get_block_header_rlp_max_lens(chip.network().unwrap()).0; + let field_idx = ctx.load_witness(F::from(self.field_idx as u64)); + let log_idx = ctx.load_witness(F::from(self.log_idx as u64)); + let block_header = assign_vec(ctx, self.inputs.block_header.clone(), max_header_len); + let block_witness = chip.block_header_chip().decompose_block_header_phase0( + ctx, + mpt.keccak(), + &block_header, + ); + let receipts_root = &block_witness.get_receipts_root().field_cells; + let tx_idx = ctx.load_witness(F::from(self.inputs.receipt.idx as u64)); + let proof = self.inputs.receipt.proof.clone().assign(ctx); + let rc_input = EthReceiptInputAssigned { tx_idx, proof }; + // check MPT root of transaction_witness is block_witness.transaction_root + let receipt_witness = { + let witness = chip.parse_receipt_proof_phase0(ctx, rc_input); + // check MPT root is transactions_root + for (pf_byte, byte) in + witness.mpt_witness.root_hash_bytes.iter().zip_eq(receipts_root.iter()) + { + ctx.constrain_equal(pf_byte, byte); + } + witness + }; + + let field_witness = chip.extract_receipt_field(ctx, &receipt_witness, field_idx); + let log_witness = chip.extract_receipt_log(ctx, &receipt_witness, log_idx); + println!("FIELD_LEN: {:?}", field_witness.value_len); + println!("FIELD_BYTES: {:?}", &field_witness.value_bytes[0..16]); + println!("LOG_LEN: {:?}", log_witness.log_len); + println!("LOG_BYTES: {:?}", &log_witness.log_bytes[0..16]); + (block_witness, receipt_witness, self.params) + } + + fn virtual_assign_phase1( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + (block_witness, receipt_witness, chip_params): Self::FirstPhasePayload, + ) { + let chip = EthReceiptChip::new(mpt, chip_params); + let (ctx_gate, ctx_rlc) = builder.rlc_ctx_pair(); + let _receipt_trace = chip.parse_receipt_proof_phase1((ctx_gate, ctx_rlc), receipt_witness); + let _block_trace = chip + .block_header_chip() + .decompose_block_header_phase1((ctx_gate, ctx_rlc), block_witness); + } +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize)] +pub struct ReceiptFieldProviderInput { + pub tx_hash: H256, + pub field_idx: usize, + pub log_idx: usize, +} + +fn get_test_circuit( + network: Chain, + tx_hash: H256, + field_idx: usize, + log_idx: usize, + max_data_byte_len: usize, + max_log_num: usize, + topic_num_bounds: (usize, usize), +) -> EthBlockReceiptFieldCircuit { + let provider = setup_provider(network); + + EthBlockReceiptFieldCircuit::from_provider( + &provider, + tx_hash, + field_idx, + log_idx, + 6, + network, + max_data_byte_len, + max_log_num, + topic_num_bounds, + ) +} + +pub fn test_valid_input_json( + path: String, + max_data_byte_len: usize, + max_log_num: usize, + topic_num_bounds: (usize, usize), +) { + let file_inputs: ReceiptFieldProviderInput = + serde_json::from_reader(File::open(path).expect("path does not exist")).unwrap(); + let tx_hash = file_inputs.tx_hash; + let field_idx = file_inputs.field_idx; + let log_idx = file_inputs.log_idx; + test_valid_input_direct( + tx_hash, + field_idx, + log_idx, + max_data_byte_len, + max_log_num, + topic_num_bounds, + ); +} + +pub fn test_valid_input_direct( + tx_hash: H256, + field_idx: usize, + log_idx: usize, + max_data_byte_len: usize, + max_log_num: usize, + topic_num_bounds: (usize, usize), +) { + let params = get_rlc_params("configs/tests/transaction.json"); + let k = params.base.k as u32; + + let input = get_test_circuit( + Chain::Mainnet, + tx_hash, + field_idx, + log_idx, + max_data_byte_len, + max_log_num, + topic_num_bounds, + ); + let mut circuit = create_circuit(CircuitBuilderStage::Mock, params, input); + circuit.mock_fulfill_keccak_promises(None); + circuit.calculate_params(); + let instances = circuit.instances(); + MockProver::run(k, &circuit, instances).unwrap().assert_satisfied(); +} + +#[test] +pub fn test_mock_single_rc_field_legacy() { + test_valid_input_json( + "src/receipt/tests/data/field/single_rc_pos_test_legacy.json".to_string(), + 256, + 8, + (0, 4), + ); +} + +#[test] +pub fn test_mock_single_rc_field_new() { + test_valid_input_json( + "src/receipt/tests/data/field/single_rc_pos_test_new.json".to_string(), + 256, + 8, + (0, 4), + ); +} diff --git a/axiom-eth/src/receipt/tests/mod.rs b/axiom-eth/src/receipt/tests/mod.rs new file mode 100644 index 00000000..ae92e19d --- /dev/null +++ b/axiom-eth/src/receipt/tests/mod.rs @@ -0,0 +1,185 @@ +#![cfg(feature = "providers")] +use super::*; +use crate::block_header::{get_block_header_rlp_max_lens, EthBlockHeaderWitness}; +use crate::providers::setup_provider; +use crate::rlc::tests::get_rlc_params; +use crate::rlc::FIRST_PHASE; +use crate::utils::assign_vec; +use crate::utils::eth_circuit::{create_circuit, EthCircuitInstructions}; +use ethers_core::types::H256; + +use halo2_base::gates::circuit::CircuitBuilderStage; +use halo2_base::halo2_proofs::dev::MockProver; + +use ethers_providers::{JsonRpcClient, Provider}; +use halo2_base::halo2_proofs::halo2curves::bn256::Fr; +use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::marker::PhantomData; +use test_log::test; + +pub mod field; + +#[derive(Clone, Debug)] +pub struct EthBlockReceiptCircuit { + pub inputs: EthBlockReceiptInput, // public and private inputs + pub params: EthReceiptChipParams, + _marker: PhantomData, +} + +impl EthBlockReceiptCircuit { + #[allow(clippy::too_many_arguments)] + #[cfg(feature = "providers")] + pub fn from_provider( + provider: &Provider

, + tx_hash: H256, + receipt_pf_max_depth: usize, + network: Chain, + max_data_byte_len: usize, + max_log_num: usize, + topic_num_bounds: (usize, usize), + ) -> Self { + use crate::providers::receipt::get_block_receipt_input; + + let inputs = get_block_receipt_input( + provider, + tx_hash, + receipt_pf_max_depth, + max_data_byte_len, + max_log_num, + topic_num_bounds, + ); + let params = EthReceiptChipParams { + max_data_byte_len, + max_log_num, + topic_num_bounds, + network: Some(network), + }; + Self { inputs, params, _marker: PhantomData } + } +} + +impl EthCircuitInstructions for EthBlockReceiptCircuit { + type FirstPhasePayload = (EthBlockHeaderWitness, EthReceiptWitness, EthReceiptChipParams); + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + ) -> Self::FirstPhasePayload { + let ctx = builder.base.main(FIRST_PHASE); + let chip = EthReceiptChip::new(mpt, self.params); + let max_header_len = get_block_header_rlp_max_lens(chip.network().unwrap()).0; + let block_header = assign_vec(ctx, self.inputs.block_header.clone(), max_header_len); + let block_witness = chip.block_header_chip().decompose_block_header_phase0( + ctx, + mpt.keccak(), + &block_header, + ); + let receipts_root = &block_witness.get_receipts_root().field_cells; + let tx_idx = ctx.load_witness(F::from(self.inputs.receipt.idx as u64)); + let proof = self.inputs.receipt.proof.clone().assign(ctx); + let rc_input = EthReceiptInputAssigned { tx_idx, proof }; + // check MPT root of transaction_witness is block_witness.transaction_root + let receipt_witness = { + let witness = chip.parse_receipt_proof_phase0(ctx, rc_input); + // check MPT root is transactions_root + for (pf_byte, byte) in + witness.mpt_witness.root_hash_bytes.iter().zip_eq(receipts_root.iter()) + { + ctx.constrain_equal(pf_byte, byte); + } + witness + }; + (block_witness, receipt_witness, self.params) + } + + fn virtual_assign_phase1( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + (block_witness, receipt_witness, chip_params): Self::FirstPhasePayload, + ) { + let chip = EthReceiptChip::new(mpt, chip_params); + let (ctx_gate, ctx_rlc) = builder.rlc_ctx_pair(); + let _receipt_trace = chip.parse_receipt_proof_phase1((ctx_gate, ctx_rlc), receipt_witness); + let _block_trace = chip + .block_header_chip() + .decompose_block_header_phase1((ctx_gate, ctx_rlc), block_witness); + } +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize)] +pub struct ReceiptProviderInput { + pub tx_hash: H256, +} + +fn get_test_circuit( + network: Chain, + tx_hash: H256, + max_data_byte_len: usize, + max_log_num: usize, + topic_num_bounds: (usize, usize), +) -> EthBlockReceiptCircuit { + let provider = setup_provider(network); + + EthBlockReceiptCircuit::from_provider( + &provider, + tx_hash, + 6, + network, + max_data_byte_len, + max_log_num, + topic_num_bounds, + ) +} + +pub fn test_valid_input_json( + path: String, + max_data_byte_len: usize, + max_log_num: usize, + topic_num_bounds: (usize, usize), +) { + let file_inputs: ReceiptProviderInput = + serde_json::from_reader(File::open(path).expect("path does not exist")).unwrap(); + let tx_hash = file_inputs.tx_hash; + + test_valid_input_direct(tx_hash, max_data_byte_len, max_log_num, topic_num_bounds); +} + +pub fn test_valid_input_direct( + tx_hash: H256, + max_data_byte_len: usize, + max_log_num: usize, + topic_num_bounds: (usize, usize), +) { + let params = get_rlc_params("configs/tests/transaction.json"); + let k = params.base.k as u32; + + let input = + get_test_circuit(Chain::Mainnet, tx_hash, max_data_byte_len, max_log_num, topic_num_bounds); + let mut circuit = create_circuit(CircuitBuilderStage::Mock, params, input); + circuit.mock_fulfill_keccak_promises(None); + circuit.calculate_params(); + let instances = circuit.instances(); + MockProver::run(k, &circuit, instances).unwrap().assert_satisfied(); +} + +#[test] +pub fn test_mock_single_rc_legacy() { + test_valid_input_json( + "src/receipt/tests/data/single_rc_pos_test_legacy.json".to_string(), + 256, + 8, + (0, 4), + ); +} + +#[test] +pub fn test_mock_single_rc_new() { + test_valid_input_json( + "src/receipt/tests/data/single_rc_pos_test_new.json".to_string(), + 256, + 8, + (0, 4), + ); +} diff --git a/axiom-eth/src/receipt/types.rs b/axiom-eth/src/receipt/types.rs new file mode 100644 index 00000000..365f38cb --- /dev/null +++ b/axiom-eth/src/receipt/types.rs @@ -0,0 +1,103 @@ +use crate::Field; +use crate::{ + mpt::{MPTInput, MPTProof, MPTProofWitness}, + rlp::types::{RlpArrayWitness, RlpFieldTrace, RlpFieldWitness}, +}; +use ethers_core::types::H256; +use getset::Getters; +use halo2_base::{AssignedValue, Context}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug)] +pub struct EthReceiptInputAssigned { + pub tx_idx: AssignedValue, + pub proof: MPTProof, +} + +#[derive(Clone, Debug, Getters)] +pub struct EthReceiptWitness { + pub receipt_type: AssignedValue, + pub tx_idx: AssignedValue, + pub idx_witness: RlpFieldWitness, + #[getset(get = "pub")] + pub(crate) value: RlpArrayWitness, + pub logs: RlpArrayWitness, + #[getset(get = "pub")] + pub(crate) mpt_witness: MPTProofWitness, +} + +#[derive(Clone, Debug)] +pub struct EthReceiptTrace { + pub receipt_type: AssignedValue, + pub value_trace: Vec>, +} + +#[derive(Clone, Debug)] +pub struct EthReceiptFieldWitness { + pub field_idx: AssignedValue, + /// Value of the field, right padded to some max length + pub value_bytes: Vec>, + pub value_len: AssignedValue, +} + +#[derive(Clone, Debug)] +pub struct EthReceiptLogWitness { + pub log_idx: AssignedValue, + /// Log in bytes, right padded to some max length + pub log_bytes: Vec>, + pub log_len: AssignedValue, +} + +#[derive(Clone, Debug)] +pub struct EthReceiptLogFieldWitness { + /// Witness for parsed log list + pub log_list: RlpArrayWitness, + /// Witness for parsed topics list + pub topics_list: RlpArrayWitness, +} + +impl EthReceiptLogFieldWitness { + /// Variable number of topics. + pub fn num_topics(&self) -> AssignedValue { + self.topics_list.list_len.unwrap() + } + /// List of topics, each topic as bytes. The list is padded to fixed length. + pub fn topics_bytes(&self) -> Vec>> { + self.topics_list.field_witness.iter().map(|h| h.field_cells.clone()).collect() + } + pub fn address(&self) -> &[AssignedValue] { + &self.log_list.field_witness[0].field_cells + } + pub fn data_bytes(&self) -> &[AssignedValue] { + &self.log_list.field_witness[2].field_cells + } + pub fn data_len(&self) -> AssignedValue { + self.log_list.field_witness[2].field_len + } +} + +// rust native types + +#[derive(Clone, Debug, Hash, Serialize, Deserialize)] +pub struct EthReceiptInput { + pub idx: usize, + pub proof: MPTInput, +} + +#[derive(Clone, Debug, Hash, Deserialize, Serialize)] +pub struct EthBlockReceiptInput { + pub block_number: u32, + pub block_hash: H256, // provided for convenience, actual block_hash is computed from block_header + pub block_header: Vec, + pub receipt: EthReceiptInput, +} + +impl EthReceiptInput { + pub fn assign(self, ctx: &mut Context) -> EthReceiptInputAssigned { + // let block_hash = encode_h256_to_field(&self.block_hash); + // let block_hash = block_hash.map(|block_hash| ctx.load_witness(block_hash)); + let tx_idx = ctx.load_witness(F::from(self.idx as u64)); + let proof = self.proof.assign(ctx); + EthReceiptInputAssigned { tx_idx, proof } + } +} diff --git a/axiom-eth/src/rlp/rlc.rs b/axiom-eth/src/rlc/chip.rs similarity index 61% rename from axiom-eth/src/rlp/rlc.rs rename to axiom-eth/src/rlc/chip.rs index 2815aeb2..6478cac9 100644 --- a/axiom-eth/src/rlp/rlc.rs +++ b/axiom-eth/src/rlc/chip.rs @@ -1,7 +1,3 @@ -use crate::halo2_proofs::{ - plonk::{Advice, Challenge, Column, ConstraintSystem, FirstPhase, SecondPhase, Selector}, - poly::Rotation, -}; use halo2_base::{ gates::GateInstructions, utils::{bit_length, ScalarField}, @@ -11,76 +7,13 @@ use halo2_base::{ use itertools::Itertools; use std::{ iter, - marker::PhantomData, sync::{RwLock, RwLockReadGuard}, }; -pub const FIRST_PHASE: usize = 0; -pub const RLC_PHASE: usize = 1; - -#[derive(Clone, Copy, Debug)] -/// RLC of a vector of `F` values of variable length but known maximum length -pub struct RlcTrace { - pub rlc_val: AssignedValue, // in SecondPhase - pub len: AssignedValue, // in FirstPhase - pub max_len: usize, - // We no longer store the input values as they should be exposed elsewhere - // pub values: Vec>, -} - -#[derive(Clone, Copy, Debug)] -/// RLC of a trace of known fixed length -pub struct RlcFixedTrace { - pub rlc_val: AssignedValue, // SecondPhase - // pub values: Vec>, // FirstPhase - pub len: usize, -} - -#[derive(Clone, Debug)] -/// This config consists of a variable number of advice columns, all in `SecondPhase`. -/// Each advice column has a selector column that enables a custom gate to aid RLC computation. -/// -/// The intention is that this chip is only used for the actual RLC computation. All other operations should use `GateInstructions` by advancing the phase to `SecondPhase`. -/// -/// Make sure that the `context_id` of `RlcChip` is different from that of any `FlexGateConfig` or `RangeConfig` you are using. -pub struct RlcConfig { - pub basic_gates: Vec<(Column, Selector)>, - pub gamma: Challenge, - _marker: PhantomData, -} - -impl RlcConfig { - pub fn configure(meta: &mut ConstraintSystem, num_advice: usize) -> Self { - let basic_gates = (0..num_advice) - .map(|_| { - let a = meta.advice_column_in(SecondPhase); - meta.enable_equality(a); - let q = meta.selector(); - (a, q) - }) - .collect_vec(); - - let gamma = meta.challenge_usable_after(FirstPhase); - - for &(rlc, q) in basic_gates.iter() { - meta.create_gate("RLC computation", |meta| { - let q = meta.query_selector(q); - let rlc_prev = meta.query_advice(rlc, Rotation::cur()); - let val = meta.query_advice(rlc, Rotation::next()); - let rlc_curr = meta.query_advice(rlc, Rotation(2)); - // TODO: see if reducing number of distinct rotation sets speeds up SHPLONK: - // Phantom query so rotation set is also size 4 to match `FlexGateConfig` - // meta.query_advice(rlc, Rotation(3)); - - let gamma = meta.query_challenge(gamma); - - vec![q * (rlc_prev * gamma + val - rlc_curr)] - }); - } - - Self { basic_gates, gamma, _marker: PhantomData } - } -} +use super::{ + circuit::builder::RlcContextPair, + types::{RlcFixedTrace, RlcTrace, RlcVar, RlcVarPtr}, +}; /// This chip provides functions related to computing Random Linear Combinations (RLCs) using powers of a random /// challenge value `gamma`. Throughout we assume that `gamma` is supplied through the Halo2 Challenge API. @@ -92,11 +25,6 @@ pub struct RlcChip { gamma: F, } -/// Wrapper so we don't need to pass around two contexts separately. The pair consists of `(ctx_gate, ctx_rlc)` where -/// * `ctx_gate` should be an `RLC_PHASE` context for use with `GateChip`. -/// * `ctx_rlc` should be a context for use with `RlcChip`. -pub(crate) type RlcContextPair<'a, F> = (&'a mut Context, &'a mut Context); - impl RlcChip { pub fn new(gamma: F) -> Self { Self { gamma_pow_cached: RwLock::new(vec![]), gamma } @@ -106,6 +34,7 @@ impl RlcChip { &self.gamma } + /// `gamma_pow_cached[i] = gamma^{2^i}` pub fn gamma_pow_cached(&self) -> RwLockReadGuard>> { self.gamma_pow_cached.read().unwrap() } @@ -126,11 +55,25 @@ impl RlcChip { gate: &impl GateInstructions, inputs: impl IntoIterator>, len: AssignedValue, + ) -> RlcTrace { + self.compute_rlc_with_min_len((ctx_gate, ctx_rlc), gate, inputs, len, 0) + } + + /// Same as `compute_rlc` but assumes `min_len <= len <= max_len` as an optimization. + /// The case `len = 0` is handled with some special treatment + pub fn compute_rlc_with_min_len( + &self, + (ctx_gate, ctx_rlc): RlcContextPair, + gate: &impl GateInstructions, + inputs: impl IntoIterator>, + len: AssignedValue, + min_len: usize, ) -> RlcTrace { let mut inputs = inputs.into_iter(); let is_zero = gate.is_zero(ctx_gate, len); - let len_minus_one = gate.sub(ctx_gate, len, Constant(F::one())); - let idx = gate.select(ctx_gate, Constant(F::zero()), len_minus_one, is_zero); + let shift_amt = if min_len != 0 { min_len } else { 1 }; + let shifted_len = gate.sub(ctx_gate, len, Constant(F::from(shift_amt as u64))); + let idx = gate.select(ctx_gate, Constant(F::ZERO), shifted_len, is_zero); let mut max_len: usize = 0; let row_offset = ctx_rlc.advice.len() as isize; @@ -149,14 +92,18 @@ impl RlcChip { ctx_rlc.assign_region(rlc_vals, (0..2 * max_len as isize - 2).step_by(2)); } } - + // ctx_rlc.advice looks like | rlc0=val0 | val1 | rlc1 | val2 | rlc2 | ... | rlc_{max_len - 1} | + // therefore we want to index into `2 * (len - 1)` unless len is 0, in which case we just return 0 + assert!(min_len <= max_len); let rlc_val = if max_len == 0 { ctx_gate.load_zero() + } else if shift_amt == max_len { + // same as ctx_rlc.get(-1): + ctx_rlc.get(row_offset + 2 * (max_len - 1) as isize) } else { gate.select_from_idx( ctx_gate, - // TODO: optimize this with iterator on ctx_rlc.advice - (0..2 * max_len as isize).step_by(2).map(|i| ctx_rlc.get(row_offset + i)), + (shift_amt - 1..max_len).map(|i| ctx_rlc.get(row_offset + 2 * i as isize)), idx, ) }; @@ -203,42 +150,23 @@ impl RlcChip { /// l = l_0 + ... + l_{k - 1}, /// then a = b^0 || ... || b^{k - 1}. /// * Pf: View both sides as polynomials in r. - // - /// Assumes: + /// + /// If `var_num_frags` is Some, then it concatenates the first `var_num_frags` inputs into the RLC. Otherwise it concatenates all inputs. + /// + /// # Assumptions /// * each tuple of the input is (RLC(a, l), l) for some sequence a_i of length l /// * all rlc_len values have been range checked + /// * `ctx_gate` should be in later phase than `inputs` + /// * `0 < var_num_frags <= inputs.len()` /// - /// `inputs[i] = (rlc_input, len, max_len)` - /// - /// `ctx_gate` should be in later phase than `inputs` - pub fn constrain_rlc_concat( - &self, - ctx_gate: &mut Context, // ctx_gate in SecondPhase - gate: &impl GateInstructions, - inputs: impl IntoIterator, AssignedValue, usize)>, - (concat_rlc, concat_len): (&AssignedValue, &AssignedValue), - ) { - let mut inputs = inputs.into_iter(); - - let (mut running_rlc, mut running_len, _) = inputs.next().unwrap(); - for (input, len, max_len) in inputs { - running_len = gate.add(ctx_gate, running_len, len); - let gamma_pow = self.rlc_pow(ctx_gate, gate, len, bit_length(max_len as u64)); - running_rlc = gate.mul_add(ctx_gate, running_rlc, gamma_pow, input); - } - ctx_gate.constrain_equal(&running_rlc, concat_rlc); - ctx_gate.constrain_equal(&running_len, concat_len); - } - - // returns (rlc, len) // if num_frags.value() = 0, then (rlc = 0, len = 0) because of how `select_from_idx` works (`num_frags_minus_1` will be very large) - fn rlc_concat_var( + pub fn rlc_concat( &self, ctx_gate: &mut Context, gate: &impl GateInstructions, - inputs: impl IntoIterator, AssignedValue, usize)>, - num_frags: AssignedValue, - ) -> (AssignedValue, AssignedValue) { + inputs: impl IntoIterator>, + var_num_frags: Option>, + ) -> RlcTrace { let mut inputs = inputs.into_iter(); let (size, hi) = inputs.size_hint(); // size only used for capacity estimation @@ -247,47 +175,53 @@ impl RlcChip { let mut partial_rlc = Vec::with_capacity(size); let mut partial_len = Vec::with_capacity(size); - let (mut running_rlc, mut running_len, _) = inputs.next().unwrap(); + let initial = inputs.next().unwrap(); + let mut running_rlc = initial.rlc_val; + let mut running_len = initial.len; + let mut running_max_len = initial.max_len; partial_rlc.push(running_rlc); partial_len.push(running_len); - for (input, len, max_len) in inputs { + for input in inputs { + let RlcTrace { rlc_val, len, max_len } = input; running_len = gate.add(ctx_gate, running_len, len); let gamma_pow = self.rlc_pow(ctx_gate, gate, len, bit_length(max_len as u64)); - running_rlc = gate.mul_add(ctx_gate, running_rlc, gamma_pow, input); + running_rlc = gate.mul_add(ctx_gate, running_rlc, gamma_pow, rlc_val); partial_len.push(running_len); partial_rlc.push(running_rlc); + running_max_len += max_len; + } + if let Some(num_frags) = var_num_frags { + let num_frags_minus_1 = gate.sub(ctx_gate, num_frags, Constant(F::ONE)); + let indicator = gate.idx_to_indicator(ctx_gate, num_frags_minus_1, partial_len.len()); + let total_len = gate.select_by_indicator(ctx_gate, partial_len, indicator.clone()); + let rlc_select = gate.select_by_indicator(ctx_gate, partial_rlc, indicator); + RlcTrace { rlc_val: rlc_select, len: total_len, max_len: running_max_len } + } else { + RlcTrace { + rlc_val: partial_rlc.pop().unwrap(), + len: partial_len.pop().unwrap(), + max_len: running_max_len, + } } - - let num_frags_minus_1 = gate.sub(ctx_gate, num_frags, Constant(F::one())); - let indicator = gate.idx_to_indicator(ctx_gate, num_frags_minus_1, partial_len.len()); - let total_len = gate.select_by_indicator(ctx_gate, partial_len, indicator.clone()); - let rlc_select = gate.select_by_indicator(ctx_gate, partial_rlc, indicator); - - (rlc_select, total_len) } - /// Same as `constrain_rlc_concat` but now the actual length of `inputs` to use is variable: - /// these are referred to as "fragments". - /// - /// Assumes 0 < num_frags <= max_num_frags. + /// We compute `rlc_concat` of `inputs` (the first `var_num_frags` if Some), and then constrain the result equals `concatenation`. /// /// `ctx_gate` and `ctx_rlc` should be in later phase than `inputs` - pub fn constrain_rlc_concat_var( + pub fn constrain_rlc_concat<'a>( &self, ctx_gate: &mut Context, gate: &impl GateInstructions, - inputs: impl IntoIterator, AssignedValue, usize)>, - (concat_rlc, concat_len): (&AssignedValue, &AssignedValue), - num_frags: AssignedValue, - // max_num_frags: usize, + inputs: impl IntoIterator>, + concatenation: impl Into>, + var_num_frags: Option>, ) { - let (rlc_select, total_len) = self.rlc_concat_var(ctx_gate, gate, inputs, num_frags); - ctx_gate.constrain_equal(&total_len, concat_len); - ctx_gate.constrain_equal(&rlc_select, concat_rlc); + let claimed_concat = self.rlc_concat(ctx_gate, gate, inputs, var_num_frags); + rlc_constrain_equal(ctx_gate, &claimed_concat, concatenation.into()); } fn load_gamma(&self, ctx_rlc: &mut Context, gamma: F) -> AssignedValue { - ctx_rlc.assign_region_last([Constant(F::one()), Constant(F::zero()), Witness(gamma)], [0]) + ctx_rlc.assign_region_last([Constant(F::ONE), Constant(F::ZERO), Witness(gamma)], [0]) } /// Updates `gamma_pow_cached` to contain assigned values for `gamma^{2^i}` for `i = 0,...,cache_bits - 1` where `gamma` is the challenge value @@ -337,7 +271,7 @@ impl RlcChip { let mut out = None; for (bit, &gamma_pow) in bits.into_iter().zip(self.gamma_pow_cached().iter()) { - let multiplier = gate.select(ctx_gate, gamma_pow, Constant(F::one()), bit); + let multiplier = gate.select(ctx_gate, gamma_pow, Constant(F::ONE), bit); out = Some(if let Some(prev) = out { gate.mul(ctx_gate, multiplier, prev) } else { @@ -346,37 +280,29 @@ impl RlcChip { } out.unwrap() } -} - -// to deal with selecting / comparing RLC of variable length strings - -#[derive(Clone, Copy, Debug)] -pub struct RlcVar { - pub rlc_val: AssignedValue, - pub len: AssignedValue, -} -impl From> for RlcVar { - fn from(trace: RlcTrace) -> Self { - RlcVar { rlc_val: trace.rlc_val, len: trace.len } - } -} - -#[derive(Clone, Copy, Debug)] -pub struct RlcVarPtr<'a, F: ScalarField> { - pub rlc_val: &'a AssignedValue, - pub len: &'a AssignedValue, -} - -impl<'a, F: ScalarField> From<&'a RlcTrace> for RlcVarPtr<'a, F> { - fn from(trace: &'a RlcTrace) -> Self { - RlcVarPtr { rlc_val: &trace.rlc_val, len: &trace.len } - } -} - -impl<'a, F: ScalarField> From<&'a RlcVar> for RlcVarPtr<'a, F> { - fn from(trace: &'a RlcVar) -> RlcVarPtr<'a, F> { - RlcVarPtr { rlc_val: &trace.rlc_val, len: &trace.len } + /// Computes `gamma^pow` where `gamma` is the challenge value. + pub fn rlc_pow_fixed( + &self, + ctx_gate: &mut Context, // ctx_gate in SecondPhase + gate: &impl GateInstructions, + pow: usize, + ) -> AssignedValue { + if pow == 0 { + return ctx_gate.load_constant(F::ONE); + } + let gamma_pow2 = self.gamma_pow_cached(); + let bits = bit_length(pow as u64); + assert!(bits <= gamma_pow2.len()); + let mut out = None; + for i in 0..bits { + if pow >> i & 1 == 1 { + let multiplier = gamma_pow2[i]; + out = + Some(out.map(|prev| gate.mul(ctx_gate, multiplier, prev)).unwrap_or(multiplier)) + } + } + out.unwrap() } } diff --git a/axiom-eth/src/rlc/circuit/builder.rs b/axiom-eth/src/rlc/circuit/builder.rs new file mode 100644 index 00000000..885ee344 --- /dev/null +++ b/axiom-eth/src/rlc/circuit/builder.rs @@ -0,0 +1,278 @@ +use halo2_base::{ + gates::{ + circuit::{ + builder::{BaseCircuitBuilder, RangeStatistics}, + CircuitBuilderStage, + }, + GateInstructions, RangeChip, + }, + halo2_proofs::plonk::Circuit, + utils::ScalarField, + virtual_region::copy_constraints::SharedCopyConstraintManager, + AssignedValue, Context, +}; +use itertools::Itertools; +use rayon::prelude::*; + +use crate::rlc::{ + chip::RlcChip, + virtual_region::{manager::RlcManager, RlcThreadBreakPoints}, + RLC_PHASE, +}; + +use super::RlcCircuitParams; + +/// Circuit builder that extends [BaseCircuitBuilder] with support for RLC gate +#[derive(Clone, Debug, Default)] +pub struct RlcCircuitBuilder { + pub base: BaseCircuitBuilder, + pub rlc_manager: RlcManager, + /// Number of advice columns for RLC gate + pub num_rlc_columns: usize, + /// The challenge value, will be set after FirstPhase + pub gamma: Option, + /// To avoid concurrency issues with `RlcChip::load_rlc_cache`, we will call it once + /// when `RlcChip` is first created. This means we compute `gamma^{2^i}` for `i=0..max_cache_bits`. + /// Concretely this means this circuit builder will only support concatenating strings where + /// the max length of a fragment is < 2^max_cache_bits. + max_cache_bits: usize, +} + +impl RlcCircuitBuilder { + pub fn new(witness_gen_only: bool, max_cache_bits: usize) -> Self { + let base = BaseCircuitBuilder::new(witness_gen_only); + let rlc_manager = RlcManager::new(witness_gen_only, base.core().copy_manager.clone()); + Self { base, rlc_manager, max_cache_bits, ..Default::default() } + } + + pub fn unknown(mut self, use_unknown: bool) -> Self { + self.base = self.base.unknown(use_unknown); + self.rlc_manager = self.rlc_manager.unknown(use_unknown); + self + } + + pub fn from_stage(stage: CircuitBuilderStage, max_cache_bits: usize) -> Self { + Self::new(stage.witness_gen_only(), max_cache_bits) + .unknown(stage == CircuitBuilderStage::Keygen) + } + + pub fn prover( + config_params: RlcCircuitParams, + break_points: RlcThreadBreakPoints, + max_cache_bits: usize, + ) -> Self { + Self::new(true, max_cache_bits).use_params(config_params).use_break_points(break_points) + } + + /// Returns global shared copy manager + pub fn copy_manager(&self) -> &SharedCopyConstraintManager { + &self.base.core().copy_manager + } + + /// Sets the copy manager to the given one in all shared references. + pub fn set_copy_manager(&mut self, copy_manager: SharedCopyConstraintManager) { + self.base.set_copy_manager(copy_manager.clone()); + self.rlc_manager.set_copy_manager(copy_manager); + } + + /// Returns `self` with a given copy manager + pub fn use_copy_manager(mut self, copy_manager: SharedCopyConstraintManager) -> Self { + self.set_copy_manager(copy_manager); + self + } + + pub fn set_max_cache_bits(&mut self, max_cache_bits: usize) { + self.max_cache_bits = max_cache_bits; + } + + pub fn use_max_cache_bits(mut self, max_cache_bits: usize) -> Self { + self.set_max_cache_bits(max_cache_bits); + self + } + + /// Deep clone of `self`, where the underlying object of shared references in [SharedCopyConstraintManager] and [LookupAnyManager] are cloned. + pub fn deep_clone(&self) -> Self { + let base = self.base.deep_clone(); + let rlc_manager = + self.rlc_manager.clone().use_copy_manager(base.core().copy_manager.clone()); + Self { + base, + rlc_manager, + num_rlc_columns: self.num_rlc_columns, + gamma: self.gamma, + max_cache_bits: self.max_cache_bits, + } + } + + pub fn clear(&mut self) { + self.base.clear(); + self.rlc_manager.clear(); + } + + /// Returns whether or not the circuit is only used for witness generation. + pub fn witness_gen_only(&self) -> bool { + assert_eq!(self.base.witness_gen_only(), self.rlc_manager.witness_gen_only()); + self.base.witness_gen_only() + } + + /// Circuit configuration parameters + pub fn params(&self) -> RlcCircuitParams { + RlcCircuitParams { base: self.base.params(), num_rlc_columns: self.num_rlc_columns } + } + + /// Set config params + pub fn set_params(&mut self, params: RlcCircuitParams) { + self.base.set_params(params.base); + self.num_rlc_columns = params.num_rlc_columns; + } + + /// Returns new with config params + pub fn use_params(mut self, params: RlcCircuitParams) -> Self { + self.set_params(params); + self + } + + /// The break points of the circuit. + pub fn break_points(&self) -> RlcThreadBreakPoints { + let base = self.base.break_points(); + let rlc = + self.rlc_manager.break_points.borrow().as_ref().expect("break points not set").clone(); + RlcThreadBreakPoints { base, rlc } + } + + /// Sets the break points of the circuit. + pub fn set_break_points(&mut self, break_points: RlcThreadBreakPoints) { + self.base.set_break_points(break_points.base); + *self.rlc_manager.break_points.borrow_mut() = Some(break_points.rlc); + } + + /// Returns new with break points + pub fn use_break_points(mut self, break_points: RlcThreadBreakPoints) -> Self { + self.set_break_points(break_points); + self + } + + /// Set lookup bits + pub fn set_lookup_bits(&mut self, lookup_bits: usize) { + self.base.config_params.lookup_bits = Some(lookup_bits); + } + + /// Returns new with lookup bits + pub fn use_lookup_bits(mut self, lookup_bits: usize) -> Self { + self.set_lookup_bits(lookup_bits); + self + } + + /// Set `k` = log2 of domain + pub fn set_k(&mut self, k: usize) { + self.base.config_params.k = k; + } + + /// Returns new with `k` set + pub fn use_k(mut self, k: usize) -> Self { + self.set_k(k); + self + } + + pub fn rlc_ctx_pair(&mut self) -> RlcContextPair { + (self.base.main(RLC_PHASE), self.rlc_manager.main()) + } + + /// Returns some statistics about the virtual region. + pub fn statistics(&self) -> RlcStatistics { + let base = self.base.statistics(); + let total_rlc_advice = self.rlc_manager.total_advice(); + RlcStatistics { base, total_rlc_advice } + } + + /// Virtual cells that will be copied to public instance columns + pub fn public_instances(&mut self) -> &mut [Vec>] { + &mut self.base.assigned_instances + } + + /// Auto-calculates configuration parameters for the circuit and sets them. + /// + /// * `minimum_rows`: The minimum number of rows in the circuit that cannot be used for witness assignments and contain random `blinding factors` to ensure zk property, defaults to 0. + pub fn calculate_params(&mut self, minimum_rows: Option) -> RlcCircuitParams { + let base = self.base.calculate_params(minimum_rows); + let total_rlc_advice = self.rlc_manager.total_advice(); + let max_rows = (1 << base.k) - minimum_rows.unwrap_or(0); + let num_rlc_columns = (total_rlc_advice + max_rows - 1) / max_rows; + self.num_rlc_columns = num_rlc_columns; + + let params = RlcCircuitParams { base, num_rlc_columns }; + #[cfg(feature = "display")] + { + println!("Total RLC advice cells: {total_rlc_advice}"); + log::info!("Auto-calculated config params:\n {params:#?}"); + } + params + } + + /// Creates a new [RangeChip] sharing the same [LookupAnyManager]s as `self`. + pub fn range_chip(&self) -> RangeChip { + self.base.range_chip() + } + + /// Returns [RlcChip] if challenge value `gamma` is available. Panics otherwise. + /// This should only be called in SecondPhase. + pub fn rlc_chip(&mut self, gate: &impl GateInstructions) -> RlcChip { + #[cfg(feature = "halo2-axiom")] + { + // safety check + assert!( + !self.witness_gen_only() || self.gamma.is_some(), + "Challenge value not available before SecondPhase" + ); + } + let gamma = self.gamma.unwrap_or(F::ZERO); + let rlc_chip = RlcChip::new(gamma); + // Precompute gamma^{2^i} to avoid concurrency issues + let cache_bits = self.max_cache_bits; + let (ctx_gate, ctx_rlc) = self.rlc_ctx_pair(); + rlc_chip.load_rlc_cache((ctx_gate, ctx_rlc), gate, cache_bits); + rlc_chip + } + + /// Utility function to parallelize an operation involving RLC. This should be called in SecondPhase. + // + // **Warning:** if `f` calls `rlc.load_rlc_cache`, then this call must be done *before* calling `parallelize_phase1`. + // Otherwise the cells where the rlc_cache gets stored will be different depending on which thread calls it first, + // leading to non-deterministic behavior. + pub fn parallelize_phase1(&mut self, input: Vec, f: FR) -> Vec + where + F: ScalarField, + T: Send, + R: Send, + FR: Fn(RlcContextPair, T) -> R + Send + Sync, + { + // to prevent concurrency issues with context id, we generate all the ids first + let core_thread_count = self.base.pool(RLC_PHASE).thread_count(); + let rlc_thread_count = self.rlc_manager.thread_count(); + let mut ctxs_gate = (0..input.len()) + .map(|i| self.base.pool(RLC_PHASE).new_context(core_thread_count + i)) + .collect_vec(); + let mut ctxs_rlc = (0..input.len()) + .map(|i| self.rlc_manager.new_context(rlc_thread_count + i)) + .collect_vec(); + let outputs: Vec<_> = input + .into_par_iter() + .zip((ctxs_gate.par_iter_mut()).zip(ctxs_rlc.par_iter_mut())) + .map(|(input, (ctx_gate, ctx_rlc))| f((ctx_gate, ctx_rlc), input)) + .collect(); + // we collect the new threads to ensure they are a FIXED order, otherwise the circuit will not be deterministic + self.base.pool(RLC_PHASE).threads.append(&mut ctxs_gate); + self.rlc_manager.threads.append(&mut ctxs_rlc); + outputs + } +} + +/// Wrapper so we don't need to pass around two contexts separately. The pair consists of `(ctx_gate, ctx_rlc)` where +/// * `ctx_gate` should be an `RLC_PHASE` context for use with `GateChip`. +/// * `ctx_rlc` should be a context for use with `RlcChip`. +pub type RlcContextPair<'a, F> = (&'a mut Context, &'a mut Context); + +pub struct RlcStatistics { + pub base: RangeStatistics, + pub total_rlc_advice: usize, +} diff --git a/axiom-eth/src/rlc/circuit/instructions.rs b/axiom-eth/src/rlc/circuit/instructions.rs new file mode 100644 index 00000000..e1d98ec3 --- /dev/null +++ b/axiom-eth/src/rlc/circuit/instructions.rs @@ -0,0 +1,103 @@ +use halo2_base::{ + gates::{circuit::MaybeRangeConfig, RangeChip}, + halo2_proofs::circuit::Layouter, + utils::ScalarField, + virtual_region::manager::VirtualRegionManager, +}; + +use crate::rlc::{ + chip::RlcChip, + circuit::{builder::RlcCircuitBuilder, RlcConfig}, +}; + +impl RlcCircuitBuilder { + /// Assigns the raw Halo2 cells for [RlcConfig] in FirstPhase. This should be the only time + /// FirstPhase cells are assigned to [RlcConfig]. + /// + /// This also imposes the equality constraints on + /// public instance columns (requires the copy manager to be assigned). + /// + /// (We are only imposing the copy constraints on the instance column, not assigning values to the instance column -- + /// The instance values are provided in `create_proof` as an argument, and that is what is used for Fiat-Shamir. + /// Therefore the equality constraints on instance columsn can also be done in SecondPhase instead. + /// We keep it in FirstPhase for logical clarity.) + pub fn raw_synthesize_phase0(&self, config: &RlcConfig, mut layouter: impl Layouter) { + let usable_rows = config.rlc.usable_rows; + layouter + .assign_region( + || "base phase 0", + |mut region| { + self.base.core().phase_manager[0] + .assign_raw(&(config.basic_gates(0), usable_rows), &mut region); + if let MaybeRangeConfig::WithRange(config) = &config.base.base { + self.base.assign_lookups_in_phase(config, &mut region, 0); + } + Ok(()) + }, + ) + .unwrap(); + self.base.assign_instances(&config.base.instance, layouter.namespace(|| "expose public")); + } + + /// Loads challenge value `gamma`, if after FirstPhase + pub fn load_challenge(&mut self, config: &RlcConfig, layouter: impl Layouter) { + let gamma = layouter.get_challenge(config.rlc.gamma); + gamma.map(|g| self.gamma = Some(g)); + log::info!("Challenge value: {gamma:?}"); + } + + /// Assigns the raw Halo2 cells for [RlcConfig] (which is [BaseConfig] and [PureRlcConfig]) + /// in SecondPhase. This should be the only time SecondPhase cells are assigned to [RlcConfig], + /// i.e., there is not shared ownership of some columns. + /// + /// If there is nothing after this that uses [CopyConstraintManager], then `enforce_copy_constraints` should + /// be set to true (this is usually the default). + pub fn raw_synthesize_phase1( + &self, + config: &RlcConfig, + mut layouter: impl Layouter, + enforce_copy_constraints: bool, + ) { + let usable_rows = config.rlc.usable_rows; + layouter + .assign_region( + || "base+rlc phase 1", + |mut region| { + let core = self.base.core(); + core.phase_manager[1] + .assign_raw(&(config.basic_gates(1), usable_rows), &mut region); + if let MaybeRangeConfig::WithRange(config) = &config.base.base { + self.base.assign_lookups_in_phase(config, &mut region, 1); + } + self.rlc_manager.assign_raw(&config.rlc, &mut region); + // Impose equality constraints + if enforce_copy_constraints && !core.witness_gen_only() { + core.copy_manager.assign_raw(config.base.constants(), &mut region); + } + Ok(()) + }, + ) + .unwrap(); + } +} + +/// Simple trait describing the FirstPhase and SecondPhase witness generation of a circuit +/// that only uses [RlcConfig]. This is mostly provided for convenience to use with +/// [RlcExecutor]; for more customization +/// you will have to implement [TwoPhaseInstructions] directly. +pub trait RlcCircuitInstructions { + type FirstPhasePayload; + + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + range: &RangeChip, + ) -> Self::FirstPhasePayload; + + fn virtual_assign_phase1( + builder: &mut RlcCircuitBuilder, + range: &RangeChip, + rlc: &RlcChip, + payload: Self::FirstPhasePayload, + ); +} diff --git a/axiom-eth/src/rlc/circuit/mod.rs b/axiom-eth/src/rlc/circuit/mod.rs new file mode 100644 index 00000000..ef2a6423 --- /dev/null +++ b/axiom-eth/src/rlc/circuit/mod.rs @@ -0,0 +1,105 @@ +use std::marker::PhantomData; + +use halo2_base::{ + gates::circuit::BaseCircuitParams, + gates::{circuit::BaseConfig, flex_gate::BasicGateConfig}, + halo2_proofs::{ + plonk::{Challenge, ConstraintSystem, FirstPhase, SecondPhase}, + poly::Rotation, + }, + utils::ScalarField, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +/// The circuit builder that coordinates all virtual region managers to support [BaseConfig] and [RlcConfig] +pub mod builder; +/// Module to help auto-implement [TwoPhaseCircuitInstructions] using [RlcCircuitBuilder] +pub mod instructions; + +/// This config consists of a variable number of advice columns, all in [SecondPhase]. +/// Each advice column has a selector column that enables a custom gate to aid RLC computation. +/// +/// The intention is that this chip is only used for the actual RLC computation. All other operations should use `GateInstructions` by advancing the phase to [SecondPhase]. +/// +/// Note: this uses a similar vertical gate structure as [FlexGateConfig] **however** the RLC gate uses only 3 contiguous rows instead of 4. +/// +/// We re-use the [BasicGateConfig] struct for the RLC gate, but do not call `BasicGateConfig::configure` because the custom gate we use here is different. +#[derive(Clone, Debug)] +pub struct PureRlcConfig { + pub basic_gates: Vec>, + pub gamma: Challenge, + /// Total number of usable (non-poisoned) rows in the circuit. + pub usable_rows: usize, + _marker: PhantomData, +} + +impl PureRlcConfig { + pub fn configure(meta: &mut ConstraintSystem, k: usize, num_advice_col: usize) -> Self { + let basic_gates = (0..num_advice_col) + .map(|_| { + let a = meta.advice_column_in(SecondPhase); + meta.enable_equality(a); + let q = meta.selector(); + BasicGateConfig::new(q, a) + }) + .collect_vec(); + + let gamma = meta.challenge_usable_after(FirstPhase); + + for gate in &basic_gates { + meta.create_gate("RLC computation", |meta| { + let q = meta.query_selector(gate.q_enable); + let rlc_prev = meta.query_advice(gate.value, Rotation::cur()); + let val = meta.query_advice(gate.value, Rotation::next()); + let rlc_curr = meta.query_advice(gate.value, Rotation(2)); + // TODO: see if reducing number of distinct rotation sets speeds up SHPLONK: + // Phantom query so rotation set is also size 4 to match `FlexGateConfig` + // meta.query_advice(rlc, Rotation(3)); + + let gamma = meta.query_challenge(gamma); + + vec![q * (rlc_prev * gamma + val - rlc_curr)] + }); + } + log::info!("Poisoned rows after RlcConfig::configure {}", meta.minimum_rows()); + // Warning: this needs to be updated if you create more advice columns after this `RlcConfig` is created + let usable_rows = (1usize << k) - meta.minimum_rows(); + + Self { basic_gates, gamma, usable_rows, _marker: PhantomData } + } +} + +/// Configuration parameters for [RlcConfig] +#[derive(Clone, Default, Hash, Debug, Serialize, Deserialize)] +pub struct RlcCircuitParams { + pub base: BaseCircuitParams, + pub num_rlc_columns: usize, +} + +/// Combination of [BaseConfig] and [PureRlcConfig]. +// We name this `RlcConfig` because we almost never use `PureRlcConfig` by itself. +#[derive(Clone, Debug)] +pub struct RlcConfig { + pub base: BaseConfig, + pub rlc: PureRlcConfig, +} + +impl RlcConfig { + pub fn configure(meta: &mut ConstraintSystem, params: RlcCircuitParams) -> Self { + let k = params.base.k; + let mut base = BaseConfig::configure(meta, params.base); + let rlc = PureRlcConfig::configure(meta, k, params.num_rlc_columns); + base.set_usable_rows(rlc.usable_rows); + RlcConfig { base, rlc } + } + + pub fn basic_gates(&self, phase: usize) -> Vec> { + self.base.gate().basic_gates[phase].clone() + } + + pub fn set_usable_rows(&mut self, usable_rows: usize) { + self.base.set_usable_rows(usable_rows); + self.rlc.usable_rows = usable_rows; + } +} diff --git a/axiom-eth/src/rlc/concat_array.rs b/axiom-eth/src/rlc/concat_array.rs new file mode 100644 index 00000000..71702d8e --- /dev/null +++ b/axiom-eth/src/rlc/concat_array.rs @@ -0,0 +1,61 @@ +use std::iter; + +use halo2_base::{ + gates::GateInstructions, utils::ScalarField, AssignedValue, Context, QuantumCell::Constant, +}; + +use super::{ + chip::RlcChip, + circuit::builder::RlcContextPair, + types::{AssignedVarLenVec, ConcatVarFixedArrayTrace, ConcatVarFixedArrayWitness}, +}; + +/// Both `prefix` and `suffix` are fixed length arrays, with length known at compile time. +/// However `prefix` is used to represent a variable length array, with variable length given +/// by `prefix_len`. +/// +/// This is the FirstPhase of computing `[&prefix[..prefix_len], &suffix[..]].concat()`. +/// This function **does not constrain anything**. It only computes the witness. +/// You **must** call [concat_var_fixed_array_phase1] to use RLC to constrain the concatenation. +pub fn concat_var_fixed_array_phase0( + ctx: &mut Context, + gate: &impl GateInstructions, + prefix: AssignedVarLenVec, + suffix: Vec>, +) -> ConcatVarFixedArrayWitness { + let concat_len = gate.add(ctx, prefix.len, Constant(F::from(suffix.len() as u64))); + let max_concat_len = prefix.max_len() + suffix.len(); + let prefix_len = prefix.len.value().get_lower_64() as usize; + assert!(prefix_len <= prefix.max_len()); + // Unsafe: unconstrained; to be constrained in phase1 + let concat_padded = ctx.assign_witnesses( + prefix.values[..prefix_len] + .iter() + .chain(suffix.iter()) + .map(|a| *a.value()) + .chain(iter::repeat(F::ZERO)) + .take(max_concat_len), + ); + let concat = AssignedVarLenVec { values: concat_padded, len: concat_len }; + + ConcatVarFixedArrayWitness { prefix, suffix, concat } +} + +pub fn concat_var_fixed_array_phase1( + (ctx_gate, ctx_rlc): RlcContextPair, + gate: &impl GateInstructions, + rlc: &RlcChip, + concat_witness: ConcatVarFixedArrayWitness, +) -> ConcatVarFixedArrayTrace { + let ConcatVarFixedArrayWitness { prefix, suffix, concat } = concat_witness; + let multiplier = rlc.rlc_pow_fixed(ctx_gate, gate, suffix.len()); + + let prefix_rlc = rlc.compute_rlc((ctx_gate, ctx_rlc), gate, prefix.values, prefix.len); + let suffix_rlc = rlc.compute_rlc_fixed_len(ctx_rlc, suffix); + let concat_rlc = rlc.compute_rlc((ctx_gate, ctx_rlc), gate, concat.values, concat.len); + + let claimed_concat = gate.mul_add(ctx_gate, prefix_rlc.rlc_val, multiplier, suffix_rlc.rlc_val); + ctx_gate.constrain_equal(&claimed_concat, &concat_rlc.rlc_val); + + ConcatVarFixedArrayTrace { prefix_rlc, suffix_rlc, concat_rlc } +} diff --git a/axiom-eth/src/rlc/mod.rs b/axiom-eth/src/rlc/mod.rs new file mode 100644 index 00000000..1cfd3951 --- /dev/null +++ b/axiom-eth/src/rlc/mod.rs @@ -0,0 +1,19 @@ +//! Custom gate, chip, and circuit builder for use with RLC computations + +/// Chip with functions using RLC +pub mod chip; +/// Circuit builder for RLC +pub mod circuit; +/// Utility functions for concatenating variable length arrays +pub mod concat_array; +#[cfg(test)] +pub mod tests; +/// Types +pub mod types; +/// Module for managing the virtual region corresponding to RLC columns +pub mod virtual_region; + +/// FirstPhase of challenge API +pub const FIRST_PHASE: usize = 0; +/// RLC is hard-coded to take place in SecondPhase +pub const RLC_PHASE: usize = 1; diff --git a/axiom-eth/src/rlc/tests/mod.rs b/axiom-eth/src/rlc/tests/mod.rs new file mode 100644 index 00000000..0739f7f4 --- /dev/null +++ b/axiom-eth/src/rlc/tests/mod.rs @@ -0,0 +1,160 @@ +use std::fs::File; + +use super::{ + chip::RlcChip, + circuit::{builder::RlcCircuitBuilder, instructions::RlcCircuitInstructions, RlcCircuitParams}, +}; +use ethers_core::k256::elliptic_curve::Field; +use halo2_base::{ + gates::{ + circuit::{BaseCircuitParams, CircuitBuilderStage}, + RangeChip, RangeInstructions, + }, + halo2_proofs::{ + dev::MockProver, + halo2curves::bn256::{Bn256, Fr}, + plonk::{keygen_pk, keygen_vk, Error}, + poly::kzg::commitment::ParamsKZG, + }, + utils::{ + testing::{check_proof, gen_proof}, + ScalarField, + }, + AssignedValue, +}; +use itertools::Itertools; +use rand::{rngs::StdRng, SeedableRng}; +use test_log::test; + +pub mod utils; +use utils::executor::{RlcCircuit, RlcExecutor}; + +const K: usize = 16; +fn test_params() -> RlcCircuitParams { + RlcCircuitParams { + base: BaseCircuitParams { + k: K, + num_advice_per_phase: vec![1, 1], + num_fixed: 1, + num_lookup_advice_per_phase: vec![], + lookup_bits: None, + num_instance_columns: 0, + }, + num_rlc_columns: 1, + } +} + +pub fn get_rlc_params(path: &str) -> RlcCircuitParams { + serde_json::from_reader(File::open(path).unwrap()).unwrap() +} + +struct Test { + padded_input: Vec, + len: usize, +} + +struct TestPayload { + true_input: Vec, + inputs: Vec>, + len: AssignedValue, +} + +impl RlcCircuitInstructions for Test { + type FirstPhasePayload = TestPayload; + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + _: &RangeChip, + ) -> Self::FirstPhasePayload { + let ctx = builder.base.main(0); + let true_input = self.padded_input[..self.len].to_vec(); + let inputs = ctx.assign_witnesses(self.padded_input.clone()); + let len = ctx.load_witness(F::from(self.len as u64)); + TestPayload { true_input, inputs, len } + } + + fn virtual_assign_phase1( + builder: &mut RlcCircuitBuilder, + range: &RangeChip, + rlc: &RlcChip, + payload: Self::FirstPhasePayload, + ) { + let TestPayload { true_input, inputs, len } = payload; + let (ctx_gate, ctx_rlc) = builder.rlc_ctx_pair(); + let gate = range.gate(); + let rlc_trace = rlc.compute_rlc((ctx_gate, ctx_rlc), gate, inputs, len); + let rlc_val = *rlc_trace.rlc_val.value(); + let real_rlc = compute_rlc_acc(&true_input, *rlc.gamma()); + assert_eq!(real_rlc, rlc_val); + } +} + +fn compute_rlc_acc(msg: &[F], r: F) -> F { + let mut rlc = msg[0]; + for val in msg.iter().skip(1) { + rlc = rlc * r + val; + } + rlc +} + +fn rlc_test_circuit( + stage: CircuitBuilderStage, + inputs: Vec, + len: usize, +) -> RlcCircuit> { + let params = test_params(); + let mut builder = RlcCircuitBuilder::from_stage(stage, 0).use_params(params); + builder.base.set_lookup_bits(8); // not used, just to create range chip + RlcExecutor::new(builder, Test { padded_input: inputs, len }) +} + +#[test] +pub fn test_mock_rlc() { + let input_bytes = vec![ + 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, + 7, 8, 0, 0, 0, 0, 0, 0, 0, 0, + ] + .into_iter() + .map(|x| Fr::from(x as u64)) + .collect_vec(); + let len = 32; + + let circuit = rlc_test_circuit(CircuitBuilderStage::Mock, input_bytes, len); + MockProver::run(K as u32, &circuit, vec![]).unwrap().assert_satisfied(); +} + +#[test] +pub fn test_rlc() -> Result<(), Error> { + let input_bytes = vec![ + 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, + 7, 8, 0, 0, 0, 0, 0, 0, 0, 0, + ] + .into_iter() + .map(|x| Fr::from(x as u64)) + .collect_vec(); + let len = 32; + + let mut rng = StdRng::from_seed([0u8; 32]); + let k = K as u32; + let params = ParamsKZG::::setup(k, &mut rng); + let circuit = + rlc_test_circuit(CircuitBuilderStage::Keygen, vec![Fr::ZERO; input_bytes.len()], 1); + + println!("vk gen started"); + let vk = keygen_vk(¶ms, &circuit)?; + println!("vk gen done"); + let pk = keygen_pk(¶ms, vk, &circuit)?; + println!("pk gen done"); + let break_points = circuit.0.builder.borrow().break_points(); + drop(circuit); + println!(); + println!("==============STARTING PROOF GEN==================="); + + let circuit = rlc_test_circuit(CircuitBuilderStage::Prover, input_bytes, len); + circuit.0.builder.borrow_mut().set_break_points(break_points); + let proof = gen_proof(¶ms, &pk, circuit); + println!("proof gen done"); + check_proof(¶ms, pk.get_vk(), &proof, true); + println!("verify done"); + Ok(()) +} diff --git a/axiom-eth/src/rlc/tests/utils/executor.rs b/axiom-eth/src/rlc/tests/utils/executor.rs new file mode 100644 index 00000000..6b976b39 --- /dev/null +++ b/axiom-eth/src/rlc/tests/utils/executor.rs @@ -0,0 +1,119 @@ +use std::cell::{OnceCell, RefCell}; + +use halo2_base::{ + gates::{RangeChip, RangeInstructions}, + halo2_proofs::{circuit::Layouter, plonk::ConstraintSystem}, + utils::ScalarField, +}; + +use crate::rlc::circuit::{ + builder::RlcCircuitBuilder, instructions::RlcCircuitInstructions, RlcCircuitParams, RlcConfig, +}; + +use super::two_phase::{TwoPhaseCircuit, TwoPhaseCircuitInstructions}; + +/// This struct provides a quick way to create a circuit that only uses [RlcConfig] that +/// is less verbose than implementing [TwoPhaseCircuitInstructions] directly. +/// +/// If additional in-circuit logic is required or columns not managed by [RlcConfig] are necessary, +/// then one needs to create a new struct (often a circuit builder) that implements +/// [TwoPhaseCircuitInstructions] directly. +pub struct RlcExecutor> { + pub logic_inputs: I, + pub range_chip: RangeChip, + pub builder: RefCell>, + /// The FirstPhasePayload is set after FirstPhase witness generation. + /// This is used both to pass payload between phases and also to detect if `generate_witnesses_phase0` + /// was already run outside of `synthesize` (e.g., to determine public instances) + pub payload: RefCell>, +} + +pub type RlcCircuit = TwoPhaseCircuit>; + +impl RlcExecutor +where + I: RlcCircuitInstructions, +{ + pub fn new(builder: RlcCircuitBuilder, logic_inputs: I) -> RlcCircuit { + let range_chip = builder.base.range_chip(); + let ex = Self { + logic_inputs, + range_chip, + builder: RefCell::new(builder), + payload: RefCell::new(OnceCell::new()), + }; + TwoPhaseCircuit::new(ex) + } + + pub fn calculate_params(&self, minimum_rows: Option) -> RlcCircuitParams { + let mut builder = self.builder.borrow().deep_clone(); + let range_chip = builder.base.range_chip(); + let payload = I::virtual_assign_phase0(&self.logic_inputs, &mut builder, &range_chip); + // as long as not in Prover stage, this will just set challenge = 0 + let rlc_chip = builder.rlc_chip(range_chip.gate()); + I::virtual_assign_phase1(&mut builder, &range_chip, &rlc_chip, payload); + let params = builder.calculate_params(minimum_rows); + builder.clear(); // clear so dropping copy manager doesn't complain + self.builder.borrow_mut().set_params(params.clone()); + params + } +} + +impl TwoPhaseCircuitInstructions for RlcExecutor +where + F: ScalarField, + I: RlcCircuitInstructions, +{ + type Config = RlcConfig; + type Params = RlcCircuitParams; + + fn params(&self) -> Self::Params { + self.builder.borrow().params() + } + + fn configure_with_params(meta: &mut ConstraintSystem, params: Self::Params) -> Self::Config { + RlcConfig::configure(meta, params) + } + + /// We never clear if running a real proof for "halo2-axiom". + fn clear(&self) { + if self.builder.borrow().witness_gen_only() { + return; + } + self.builder.borrow_mut().clear(); + self.payload.borrow_mut().take(); + } + + fn initialize(&self, config: &Self::Config, mut layouter: impl Layouter) { + config.base.initialize(&mut layouter); + } + + fn virtual_assign_phase0(&self) { + if self.payload.borrow().get().is_some() { + return; + } + let mut builder = self.builder.borrow_mut(); + let payload = I::virtual_assign_phase0(&self.logic_inputs, &mut builder, &self.range_chip); + let _ = self.payload.borrow().set(payload); + } + + fn raw_synthesize_phase0(&self, config: &Self::Config, layouter: impl Layouter) { + self.builder.borrow().raw_synthesize_phase0(config, layouter); + } + + fn load_challenges(&self, config: &Self::Config, layouter: impl Layouter) { + self.builder.borrow_mut().load_challenge(config, layouter); + } + + fn virtual_assign_phase1(&self) { + let payload = + self.payload.borrow_mut().take().expect("FirstPhase witness generation was not run"); + let mut builder = self.builder.borrow_mut(); + let rlc_chip = builder.rlc_chip(self.range_chip.gate()); + I::virtual_assign_phase1(&mut builder, &self.range_chip, &rlc_chip, payload) + } + + fn raw_synthesize_phase1(&self, config: &Self::Config, layouter: impl Layouter) { + self.builder.borrow().raw_synthesize_phase1(config, layouter, true); + } +} diff --git a/axiom-eth/src/rlc/tests/utils/mod.rs b/axiom-eth/src/rlc/tests/utils/mod.rs new file mode 100644 index 00000000..712ef75d --- /dev/null +++ b/axiom-eth/src/rlc/tests/utils/mod.rs @@ -0,0 +1,3 @@ +/// Helper struct to automatically implement the `TwoPhaseCircuitInstructions` trait for fast testing. +pub mod executor; +pub mod two_phase; diff --git a/axiom-eth/src/rlc/tests/utils/two_phase.rs b/axiom-eth/src/rlc/tests/utils/two_phase.rs new file mode 100644 index 00000000..9c68bf29 --- /dev/null +++ b/axiom-eth/src/rlc/tests/utils/two_phase.rs @@ -0,0 +1,89 @@ +//! Template for functions a circuit should implement to work with two challenge phases, +//! with particular attention to + +use std::marker::PhantomData; + +use halo2_base::{ + halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner}, + plonk::{Circuit, ConstraintSystem, Error}, + }, + utils::ScalarField, +}; + +/// Interface for what functions need to be supplied to write a circuit that +/// uses two challenge phases. +pub trait TwoPhaseCircuitInstructions { + type Config: Clone; + type Params: Clone + Default; + + fn configure_with_params(meta: &mut ConstraintSystem, params: Self::Params) -> Self::Config; + fn params(&self) -> Self::Params; + /// The multi-phase challenge API requires `Circuit::synthesize` to be called multiple times unless in `create_proof` mode and using `halo2-axiom`. To prevent issues with + /// the multiple calls, we require a function to clear the state of any circuit builders. + fn clear(&self); + // Instructions are listed in order they will be run + fn initialize(&self, _config: &Self::Config, _layouter: impl Layouter) {} + /// Phase0 assign to virtual regions. Any data passing from phase0 to phase1 will be done internally within the circuit and stored in `OnceCell` or `RefCell`. + fn virtual_assign_phase0(&self); + fn raw_synthesize_phase0(&self, config: &Self::Config, layouter: impl Layouter); + fn load_challenges(&self, config: &Self::Config, layouter: impl Layouter); + fn virtual_assign_phase1(&self); + fn raw_synthesize_phase1(&self, config: &Self::Config, layouter: impl Layouter); +} + +// Rust does not like blanket implementations of `Circuit` for multiple other traits. +// To get around this, we will wrap `TwoPhaseCircuitInstructions` +#[derive(Clone, Debug)] +pub struct TwoPhaseCircuit>( + pub CI, + PhantomData, +); + +impl> TwoPhaseCircuit { + pub fn new(instructions: CI) -> Self { + Self(instructions, PhantomData) + } +} + +impl> Circuit for TwoPhaseCircuit { + type Config = CI::Config; + type FloorPlanner = SimpleFloorPlanner; + type Params = CI::Params; + + fn params(&self) -> Self::Params { + self.0.params() + } + + fn without_witnesses(&self) -> Self { + unimplemented!() + } + + fn configure_with_params(meta: &mut ConstraintSystem, params: Self::Params) -> Self::Config { + CI::configure_with_params(meta, params) + } + + fn configure(_: &mut ConstraintSystem) -> Self::Config { + unreachable!() + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + // clear in case synthesize is called multiple times + self.0.clear(); + self.0.initialize(&config, layouter.namespace(|| "initialize")); + self.0.virtual_assign_phase0(); + self.0.raw_synthesize_phase0(&config, layouter.namespace(|| "raw synthesize phase0")); + #[cfg(feature = "halo2-axiom")] + { + layouter.next_phase(); + } + self.0.load_challenges(&config, layouter.namespace(|| "load challenges")); + self.0.virtual_assign_phase1(); + self.0.raw_synthesize_phase1(&config, layouter.namespace(|| "raw synthesize phase1")); + Ok(()) + } +} diff --git a/axiom-eth/src/rlc/types.rs b/axiom-eth/src/rlc/types.rs new file mode 100644 index 00000000..462b0898 --- /dev/null +++ b/axiom-eth/src/rlc/types.rs @@ -0,0 +1,102 @@ +use halo2_base::{safe_types::VarLenBytesVec, utils::ScalarField, AssignedValue, Context}; + +#[derive(Clone, Copy, Debug)] +/// RLC of a vector of `F` values of variable length but known maximum length +pub struct RlcTrace { + pub rlc_val: AssignedValue, // in SecondPhase + pub len: AssignedValue, // in FirstPhase + pub max_len: usize, + // We no longer store the input values as they should be exposed elsewhere + // pub values: Vec>, +} + +impl RlcTrace { + pub fn new(rlc_val: AssignedValue, len: AssignedValue, max_len: usize) -> Self { + Self { rlc_val, len, max_len } + } + + pub fn from_fixed(ctx: &mut Context, trace: RlcFixedTrace) -> Self { + let len = ctx.load_constant(F::from(trace.len as u64)); + Self { rlc_val: trace.rlc_val, len, max_len: trace.len } + } +} + +#[derive(Clone, Copy, Debug)] +/// RLC of a trace of known fixed length +pub struct RlcFixedTrace { + pub rlc_val: AssignedValue, // SecondPhase + // pub values: Vec>, // FirstPhase + pub len: usize, +} + +// to deal with selecting / comparing RLC of variable length strings + +#[derive(Clone, Copy, Debug)] +pub struct RlcVar { + pub rlc_val: AssignedValue, + pub len: AssignedValue, +} + +impl From> for RlcVar { + fn from(trace: RlcTrace) -> Self { + RlcVar { rlc_val: trace.rlc_val, len: trace.len } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct RlcVarPtr<'a, F: ScalarField> { + pub rlc_val: &'a AssignedValue, + pub len: &'a AssignedValue, +} + +impl<'a, F: ScalarField> From<&'a RlcTrace> for RlcVarPtr<'a, F> { + fn from(trace: &'a RlcTrace) -> Self { + RlcVarPtr { rlc_val: &trace.rlc_val, len: &trace.len } + } +} + +impl<'a, F: ScalarField> From<&'a RlcVar> for RlcVarPtr<'a, F> { + fn from(trace: &'a RlcVar) -> RlcVarPtr<'a, F> { + RlcVarPtr { rlc_val: &trace.rlc_val, len: &trace.len } + } +} + +/// Length of `values` known at compile time. +/// Represents a variable length array with length given by `len`. +/// +/// Construction of this struct assumes you have checked `len < values.len()`. +#[derive(Clone, Debug)] +pub struct AssignedVarLenVec { + pub values: Vec>, + pub len: AssignedValue, +} + +impl AssignedVarLenVec { + pub fn max_len(&self) -> usize { + self.values.len() + } +} + +impl From> for AssignedVarLenVec { + fn from(array: VarLenBytesVec) -> Self { + let values = array.bytes().iter().map(|b| *b.as_ref()).collect(); + let len = *array.len(); + Self { values, len } + } +} + +#[derive(Clone, Debug)] +pub struct ConcatVarFixedArrayWitness { + pub prefix: AssignedVarLenVec, + pub suffix: Vec>, + pub concat: AssignedVarLenVec, +} + +/// Rlc traces of the rlc concatenation used to constrain the concatenation +/// of variable length `prefix` and fixed length `suffix` arrays. +#[derive(Debug, Clone)] +pub struct ConcatVarFixedArrayTrace { + pub prefix_rlc: RlcTrace, + pub suffix_rlc: RlcFixedTrace, + pub concat_rlc: RlcTrace, +} diff --git a/axiom-eth/src/rlc/virtual_region/manager.rs b/axiom-eth/src/rlc/virtual_region/manager.rs new file mode 100644 index 00000000..ef1b033c --- /dev/null +++ b/axiom-eth/src/rlc/virtual_region/manager.rs @@ -0,0 +1,165 @@ +use std::cell::RefCell; + +use crate::rlc::{circuit::PureRlcConfig, RLC_PHASE}; +use getset::CopyGetters; +use halo2_base::{ + gates::{ + circuit::CircuitBuilderStage, + flex_gate::{ + threads::single_phase::{assign_with_constraints, assign_witnesses}, + ThreadBreakPoints, + }, + }, + halo2_proofs::circuit::Region, + utils::ScalarField, + virtual_region::{ + copy_constraints::SharedCopyConstraintManager, manager::VirtualRegionManager, + }, + Context, +}; + +/// Virtual region manager for managing virtual columns ([Context]s) in [RLC_PHASE] corresponding to [RlcConfig]. +/// +/// Note: this uses a similar vertical gate structure as [FlexGateConfig] **however** the RLC gate uses only 3 contiguous rows instead of 4. +/// +/// The implementation and functionality of this manager is very similar to `SinglePhaseCoreManager` for [FlexGateConfig] except the aforementioned 3 rows vs 4 rows gate. +#[derive(Clone, Debug, Default, CopyGetters)] +pub struct RlcManager { + /// Virtual columns. These cannot be shared across CPU threads while keeping the circuit deterministic. + pub threads: Vec>, + /// Global shared copy manager + pub copy_manager: SharedCopyConstraintManager, + /// Flag for witness generation. If true, the gate thread builder is used for witness generation only. + #[getset(get_copy = "pub")] + witness_gen_only: bool, + /// The `unknown` flag is used during key generation. If true, during key generation witness [Value]s are replaced with Value::unknown() for safety. + #[getset(get_copy = "pub")] + pub(crate) use_unknown: bool, + /// A very simple computation graph for the basic vertical RLC gate. Must be provided as a "pinning" + /// when running the production prover. + pub break_points: RefCell>, +} + +// Copied impl from `SinglePhaseCoreManager`, modified so TypeId is of RlcManager, and phase is always RLC_PHASE = 1 (SecondPhase) +impl RlcManager { + /// Creates a new [RlcManager] and spawns a main thread. + /// * `witness_gen_only`: If true, the [RlcManager] is used for witness generation only and does not impose any constraints. + pub fn new(witness_gen_only: bool, copy_manager: SharedCopyConstraintManager) -> Self { + Self { + threads: vec![], + witness_gen_only, + use_unknown: false, + copy_manager, + ..Default::default() + } + } + + /// The phase of this manager is always [RLC_PHASE] + pub fn phase(&self) -> usize { + RLC_PHASE + } + + /// Creates a new [GateThreadBuilder] depending on the stage of circuit building. If the stage is [CircuitBuilderStage::Prover], the [GateThreadBuilder] is used for witness generation only. + pub fn from_stage( + stage: CircuitBuilderStage, + copy_manager: SharedCopyConstraintManager, + ) -> Self { + Self::new(stage.witness_gen_only(), copy_manager) + .unknown(stage == CircuitBuilderStage::Keygen) + } + + /// Creates a new [RlcManager] with `use_unknown` flag set. + /// * `use_unknown`: If true, during key generation witness [Value]s are replaced with Value::unknown() for safety. + pub fn unknown(self, use_unknown: bool) -> Self { + Self { use_unknown, ..self } + } + + /// Sets the copy manager to the given one in all shared references. + pub fn set_copy_manager(&mut self, copy_manager: SharedCopyConstraintManager) { + for ctx in &mut self.threads { + ctx.copy_manager = copy_manager.clone(); + } + self.copy_manager = copy_manager; + } + + /// Returns `self` with a given copy manager + pub fn use_copy_manager(mut self, copy_manager: SharedCopyConstraintManager) -> Self { + self.set_copy_manager(copy_manager); + self + } + + pub fn clear(&mut self) { + self.threads.clear(); + self.copy_manager.lock().unwrap().clear(); + } + + /// Returns a mutable reference to the [Context] of a gate thread. Spawns a new thread for the given phase, if none exists. + pub fn main(&mut self) -> &mut Context { + if self.threads.is_empty() { + self.new_thread() + } else { + self.threads.last_mut().unwrap() + } + } + + /// Returns the number of threads + pub fn thread_count(&self) -> usize { + self.threads.len() + } + + /// Creates new context but does not append to `self.threads` + pub(crate) fn new_context(&self, context_id: usize) -> Context { + Context::new( + self.witness_gen_only, + RLC_PHASE, + "axiom-eth:RlcManager:SecondPhase", + context_id, + self.copy_manager.clone(), + ) + } + + /// Spawns a new thread for a new given `phase`. Returns a mutable reference to the [Context] of the new thread. + /// * `phase`: The phase (index) of the gate thread. + pub fn new_thread(&mut self) -> &mut Context { + let context_id = self.thread_count(); + self.threads.push(self.new_context(context_id)); + self.threads.last_mut().unwrap() + } + + /// Returns total advice cells + pub fn total_advice(&self) -> usize { + self.threads.iter().map(|ctx| ctx.advice.len()).sum::() + } +} + +impl VirtualRegionManager for RlcManager { + type Config = PureRlcConfig; + + fn assign_raw(&self, rlc_config: &Self::Config, region: &mut Region) { + if self.witness_gen_only { + let binding = self.break_points.borrow(); + let break_points = binding.as_ref().expect("break points not set"); + assign_witnesses(&self.threads, &rlc_config.basic_gates, region, break_points); + } else { + let mut copy_manager = self.copy_manager.lock().unwrap(); + assert!( + self.threads.iter().all(|ctx| ctx.phase() == RLC_PHASE), + "all threads must be in RLC_PHASE" + ); + let break_points = assign_with_constraints::( + &self.threads, + &rlc_config.basic_gates, + region, + &mut copy_manager, + rlc_config.usable_rows, + self.use_unknown, + ); + let mut bp = self.break_points.borrow_mut(); + if let Some(bp) = bp.as_ref() { + assert_eq!(bp, &break_points, "break points don't match"); + } else { + *bp = Some(break_points); + } + } + } +} diff --git a/axiom-eth/src/rlc/virtual_region/mod.rs b/axiom-eth/src/rlc/virtual_region/mod.rs new file mode 100644 index 00000000..f5ef9a3c --- /dev/null +++ b/axiom-eth/src/rlc/virtual_region/mod.rs @@ -0,0 +1,10 @@ +use halo2_base::gates::flex_gate::{MultiPhaseThreadBreakPoints, ThreadBreakPoints}; +use serde::{Deserialize, Serialize}; + +pub mod manager; + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct RlcThreadBreakPoints { + pub base: MultiPhaseThreadBreakPoints, + pub rlc: ThreadBreakPoints, +} diff --git a/axiom-eth/src/rlp/builder.rs b/axiom-eth/src/rlp/builder.rs deleted file mode 100644 index cc14f000..00000000 --- a/axiom-eth/src/rlp/builder.rs +++ /dev/null @@ -1,398 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - env::var, - iter, mem, -}; - -use halo2_base::{ - gates::{ - builder::{ - assign_threads_in, CircuitBuilderStage, FlexGateConfigParams, GateThreadBuilder, - KeygenAssignments as GateKeygenAssignments, MultiPhaseThreadBreakPoints, - ThreadBreakPoints, - }, - flex_gate::FlexGateConfig, - }, - halo2_proofs::{ - circuit::{self, Region, Value}, - plonk::{Advice, Column, Selector}, - }, - utils::ScalarField, - Context, -}; -use itertools::Itertools; -use rayon::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::rlc::{RlcChip, RlcConfig, RlcContextPair, FIRST_PHASE, RLC_PHASE}; -use crate::util::EthConfigParams; - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct RlcThreadBreakPoints { - pub gate: MultiPhaseThreadBreakPoints, - pub rlc: ThreadBreakPoints, -} - -#[derive(Clone, Debug, Default)] -pub struct KeygenAssignments { - pub assigned_advices: HashMap<(usize, usize), (circuit::Cell, usize)>, - pub assigned_constants: HashMap, - pub break_points: RlcThreadBreakPoints, -} - -#[derive(Clone, Debug, Default)] -pub struct RlcThreadBuilder { - /// Threads for RLC assignment, assume only in `RLC_PHASE` for now - pub threads_rlc: Vec>, - /// [`GateThreadBuilder`] with threads for basic gate; also in charge of thread IDs - pub gate_builder: GateThreadBuilder, -} - -impl RlcThreadBuilder { - // re-expose some methods from [`GateThreadBuilder`] for convenience - #[allow(unused_mut)] - pub fn new(mut witness_gen_only: bool) -> Self { - // in non halo2-axiom, the prover calls `synthesize` twice: first just to get FirstPhase advice columns, commit, and then generate challenge value; then the second time to actually compute SecondPhase advice - // our "Prover" implementation (`witness_gen_only = true`) is heavily optimized for the Axiom version, which only calls `synthesize` once - #[cfg(not(feature = "halo2-axiom"))] - { - witness_gen_only = false; - } - Self { threads_rlc: Vec::new(), gate_builder: GateThreadBuilder::new(witness_gen_only) } - } - - pub fn from_stage(stage: CircuitBuilderStage) -> Self { - Self::new(stage == CircuitBuilderStage::Prover) - } - - pub fn mock() -> Self { - Self::new(false) - } - - pub fn keygen() -> Self { - Self::new(false).unknown(true) - } - - pub fn prover() -> Self { - Self::new(true) - } - - pub fn unknown(mut self, use_unknown: bool) -> Self { - self.gate_builder = self.gate_builder.unknown(use_unknown); - self - } - - pub fn rlc_ctx_pair(&mut self) -> RlcContextPair { - if self.threads_rlc.is_empty() { - self.new_thread_rlc(); - } - (self.gate_builder.main(RLC_PHASE), self.threads_rlc.last_mut().unwrap()) - } - - pub fn witness_gen_only(&self) -> bool { - self.gate_builder.witness_gen_only() - } - - pub fn use_unknown(&self) -> bool { - self.gate_builder.use_unknown() - } - - pub fn thread_count(&self) -> usize { - self.gate_builder.thread_count() - } - - pub fn get_new_thread_id(&mut self) -> usize { - self.gate_builder.get_new_thread_id() - } - - pub fn new_thread_rlc(&mut self) -> &mut Context { - let thread_id = self.get_new_thread_id(); - self.threads_rlc.push(Context::new(self.witness_gen_only(), thread_id)); - self.threads_rlc.last_mut().unwrap() - } - - /// Auto-calculate configuration parameters for the circuit - pub fn config(&self, k: usize, minimum_rows: Option) -> EthConfigParams { - // first auto-configure the basic gates and lookup advice columns - let FlexGateConfigParams { - strategy: _, - num_advice_per_phase, - num_lookup_advice_per_phase, - num_fixed: _, - k, - } = self.gate_builder.config(k, minimum_rows); - // now calculate how many RLC advice columns are needed - let max_rows = (1 << k) - minimum_rows.unwrap_or(0); - let total_rlc_advice = self.threads_rlc.iter().map(|ctx| ctx.advice.len()).sum::(); - // we do a rough estimate by taking ceil(advice_cells_per_phase / 2^k ) - // if there is an edge case, `minimum_rows` will need to be manually adjusted - let num_rlc_columns = (total_rlc_advice + max_rows - 1) / max_rows; - // total fixed is the total number of constants used in both gate_builder and RLC so we need to re-calculate: - let total_fixed: usize = HashSet::::from_iter( - self.gate_builder - .threads - .iter() - .flatten() - .chain(self.threads_rlc.iter()) - .flat_map(|ctx| ctx.constant_equality_constraints.iter().map(|(c, _)| *c)), - ) - .len(); - let num_fixed = (total_fixed + (1 << k) - 1) >> k; - // assemble into new config params - let params = EthConfigParams { - degree: k as u32, - num_rlc_columns, - num_range_advice: num_advice_per_phase, - num_lookup_advice: num_lookup_advice_per_phase, - num_fixed, - unusable_rows: minimum_rows.unwrap_or(0), - keccak_rows_per_round: 0, - lookup_bits: var("LOOKUP_BITS").map(|s| s.parse().ok()).unwrap_or(None), - }; - #[cfg(feature = "display")] - { - println!("RLC Chip | {total_rlc_advice} advice cells"); - log::info!("RlcThreadBuilder auto-calculated config params:\n {params:#?}"); - } - std::env::set_var("ETH_CONFIG_PARAMS", serde_json::to_string(¶ms).unwrap()); - params - } - - /// Assigns all advice and fixed cells, turns on selectors, imposes equality constraints. - /// This should only be called during keygen. - pub fn assign_all( - &mut self, - gate: &FlexGateConfig, - lookup_advice: &[Vec>], - q_lookup: &[Option], - rlc: &RlcConfig, - region: &mut Region, - KeygenAssignments { - mut assigned_advices, - assigned_constants, - mut break_points, - }: KeygenAssignments, - ) -> KeygenAssignments { - assert!(!self.witness_gen_only()); - let use_unknown = self.use_unknown(); - let max_rows = gate.max_rows; - - // first we assign all RLC contexts, basically copying gate::builder::assign_all except that the length of the RLC vertical gate is 3 instead of 4 (which was length of basic gate) - let mut gate_index = 0; - let mut row_offset = 0; - let mut basic_gate = None; - for ctx in self.threads_rlc.iter() { - // TODO: if we have more similar vertical gates this should be refactored into a general function - for (i, (&advice, &q)) in - ctx.advice.iter().zip(ctx.selector.iter().chain(iter::repeat(&false))).enumerate() - { - let (mut column, mut q_rlc) = basic_gate.unwrap_or(rlc.basic_gates[gate_index]); - let value = if use_unknown { Value::unknown() } else { Value::known(advice) }; - #[cfg(feature = "halo2-axiom")] - let cell = *region.assign_advice(column, row_offset, value).cell(); - #[cfg(not(feature = "halo2-axiom"))] - let cell = - region.assign_advice(|| "", column, row_offset, || value).unwrap().cell(); - assigned_advices.insert((ctx.context_id, i), (cell, row_offset)); - - if (q && row_offset + 3 > max_rows) || row_offset >= max_rows - 1 { - break_points.rlc.push(row_offset); - row_offset = 0; - gate_index += 1; - // when there is a break point, because we may have two gates that overlap at the current cell, we must copy the current cell to the next column for safety - basic_gate = Some(*rlc - .basic_gates - .get(gate_index) - .unwrap_or_else(|| panic!("NOT ENOUGH RLC ADVICE COLUMNS. Perhaps blinding factors were not taken into account. The max non-poisoned rows is {max_rows}"))); - (column, q_rlc) = basic_gate.unwrap(); - - #[cfg(feature = "halo2-axiom")] - { - let ncell = region.assign_advice(column, row_offset, value); - region.constrain_equal(ncell.cell(), &cell); - } - #[cfg(not(feature = "halo2-axiom"))] - { - let ncell = region - .assign_advice(|| "", column, row_offset, || value) - .unwrap() - .cell(); - region.constrain_equal(ncell, cell).unwrap(); - } - } - - if q { - q_rlc.enable(region, row_offset).expect("enable selector should not fail"); - } - row_offset += 1; - } - } - // in order to constrain equalities and assign constants, we copy the RLC equality constraints into the gate builder (it doesn't matter which context the equalities are in), so `GateThreadBuilder::assign_all` can take care of it - // the phase doesn't matter for equality constraints, so we use phase 0 since we're sure there's a main context there - let main_ctx = self.gate_builder.main(FIRST_PHASE); - for ctx in self.threads_rlc.iter_mut() { - main_ctx.advice_equality_constraints.append(&mut ctx.advice_equality_constraints); - main_ctx.constant_equality_constraints.append(&mut ctx.constant_equality_constraints); - } - let assignments = self.gate_builder.assign_all( - gate, - lookup_advice, - q_lookup, - region, - GateKeygenAssignments { - assigned_advices, - assigned_constants, - break_points: break_points.gate, - }, - ); - - KeygenAssignments { - assigned_advices: assignments.assigned_advices, - assigned_constants: assignments.assigned_constants, - break_points: RlcThreadBreakPoints { - gate: assignments.break_points, - rlc: break_points.rlc, - }, - } - } -} - -/// Pure advice witness assignment in a single phase. Uses preprocessed `break_points` to determine when -/// to split a thread into a new column. -pub fn assign_threads_rlc( - threads_rlc: Vec>, - rlc: &RlcConfig, - region: &mut Region, - break_points: ThreadBreakPoints, -) { - if rlc.basic_gates.is_empty() { - assert!(threads_rlc.is_empty(), "Trying to assign threads in a phase with no columns"); - return; - } - let mut break_points = break_points.into_iter(); - let mut break_point = break_points.next(); - - let mut gate_index = 0; - let (mut column, _) = rlc.basic_gates[gate_index]; - let mut row_offset = 0; - - for ctx in threads_rlc { - for advice in ctx.advice { - #[cfg(feature = "halo2-axiom")] - region.assign_advice(column, row_offset, Value::known(advice)); - #[cfg(not(feature = "halo2-axiom"))] - region.assign_advice(|| "", column, row_offset, || Value::known(advice)).unwrap(); - - if break_point == Some(row_offset) { - break_point = break_points.next(); - row_offset = 0; - gate_index += 1; - (column, _) = rlc.basic_gates[gate_index]; - - #[cfg(feature = "halo2-axiom")] - region.assign_advice(column, row_offset, Value::known(advice)); - #[cfg(not(feature = "halo2-axiom"))] - region.assign_advice(|| "", column, row_offset, || Value::known(advice)).unwrap(); - } - - row_offset += 1; - } - } -} - -pub trait FnSynthesize = FnOnce(&mut RlcThreadBuilder, &RlcChip) + Clone; // `Clone` because we may run synthesize multiple times on the same circuit during keygen or mock stages - -// re-usable function for phase 0 synthesize in prover mode -pub fn assign_prover_phase0( - region: &mut Region, - gate: &FlexGateConfig, - lookup_advice: &[Vec>], - builder: &mut RlcThreadBuilder, - break_points: &mut RlcThreadBreakPoints, -) { - let break_points_gate = mem::take(&mut break_points.gate[FIRST_PHASE]); - // warning: we currently take all contexts from phase 0, which means you can't read the values - // from these contexts later in phase 1. If we want to read, should clone here - let threads = mem::take(&mut builder.gate_builder.threads[FIRST_PHASE]); - // assign phase 0 - assign_threads_in( - FIRST_PHASE, - threads, - gate, - &lookup_advice[FIRST_PHASE], - region, - break_points_gate, - ); - log::info!("End of FirstPhase"); -} - -// re-usable function for phase 1 synthesize in prover mode -#[allow(clippy::too_many_arguments)] -pub fn assign_prover_phase1( - region: &mut Region, - gate: &FlexGateConfig, - lookup_advice: &[Vec>], - rlc_config: &RlcConfig, - rlc_chip: &RlcChip, - builder: &mut RlcThreadBuilder, - break_points: &mut RlcThreadBreakPoints, - f: impl FnSynthesize, -) { - let break_points_gate = mem::take(&mut break_points.gate[RLC_PHASE]); - let break_points_rlc = mem::take(&mut break_points.rlc); - - // generate witnesses depending on challenge - f(builder, rlc_chip); - - let threads = mem::take(&mut builder.gate_builder.threads[RLC_PHASE]); - // assign phase 1 - assign_threads_in( - RLC_PHASE, - threads, - gate, - &lookup_advice[RLC_PHASE], - region, - break_points_gate, - ); - - let threads_rlc = mem::take(&mut builder.threads_rlc); - assign_threads_rlc(threads_rlc, rlc_config, region, break_points_rlc); -} - -/// Utility function to parallelize an operation involving RLC. This should be called in SecondPhase. -/// -/// **Warning:** if `f` calls `rlc.load_rlc_cache`, then this call must be done *before* calling `parallelize_phase1`. -/// Otherwise the cells where the rlc_cache gets stored will be different depending on which thread calls it first, -/// leading to non-deterministic behavior. -pub fn parallelize_phase1( - thread_pool: &mut RlcThreadBuilder, - input: Vec, - f: FR, -) -> Vec -where - F: ScalarField, - T: Send, - R: Send, - FR: Fn(RlcContextPair, T) -> R + Send + Sync, -{ - let witness_gen_only = thread_pool.witness_gen_only(); - let ctx_ids = input - .iter() - .map(|_| (thread_pool.get_new_thread_id(), thread_pool.get_new_thread_id())) - .collect_vec(); - let (trace, ctxs): (Vec<_>, Vec<_>) = input - .into_par_iter() - .zip(ctx_ids.into_par_iter()) - .map(|(input, (gate_id, rlc_id))| { - let mut ctx_gate = Context::new(witness_gen_only, gate_id); - let mut ctx_rlc = Context::new(witness_gen_only, rlc_id); - let trace = f((&mut ctx_gate, &mut ctx_rlc), input); - (trace, (ctx_gate, ctx_rlc)) - }) - .unzip(); - let (mut ctxs_gate, mut ctxs_rlc): (Vec<_>, Vec<_>) = ctxs.into_iter().unzip(); - thread_pool.gate_builder.threads[RLC_PHASE].append(&mut ctxs_gate); - thread_pool.threads_rlc.append(&mut ctxs_rlc); - - trace -} diff --git a/axiom-eth/src/rlp/mod.rs b/axiom-eth/src/rlp/mod.rs index c4fadd06..cea07c11 100644 --- a/axiom-eth/src/rlp/mod.rs +++ b/axiom-eth/src/rlp/mod.rs @@ -1,26 +1,27 @@ -use crate::halo2_proofs::plonk::ConstraintSystem; +use std::iter; + +use crate::{ + rlc::{chip::RlcChip, circuit::builder::RlcContextPair, types::RlcTrace}, + utils::circuit_utils::constrain_no_leading_zeros, +}; + use halo2_base::{ - gates::{ - flex_gate::FlexGateConfig, - range::{RangeConfig, RangeStrategy}, - GateChip, GateInstructions, RangeChip, RangeInstructions, - }, + gates::{GateChip, GateInstructions, RangeChip, RangeInstructions}, utils::{bit_length, ScalarField}, AssignedValue, Context, QuantumCell::{Constant, Existing}, }; -use std::iter; -pub mod builder; -pub mod rlc; +use self::types::{ + RlpArrayPrefixParsed, RlpArrayTrace, RlpArrayWitness, RlpFieldPrefixParsed, RlpFieldTrace, + RlpFieldWitness, RlpPrefixParsed, +}; + #[cfg(test)] mod tests; +pub mod types; -use rlc::{RlcChip, RlcConfig, RlcTrace}; - -use self::rlc::RlcContextPair; - -pub fn max_rlp_len_len(max_len: usize) -> usize { +pub const fn max_rlp_len_len(max_len: usize) -> usize { if max_len > 55 { (bit_length(max_len as u64) + 7) / 8 } else { @@ -28,6 +29,10 @@ pub fn max_rlp_len_len(max_len: usize) -> usize { } } +pub const fn max_rlp_encoding_len(payload_len: usize) -> usize { + 1 + max_rlp_len_len(payload_len) + payload_len +} + /// Returns array whose first `sub_len` cells are /// `array[start_idx..start_idx + sub_len]` /// and whose last cells are `0`. @@ -41,13 +46,13 @@ pub fn witness_subarray( max_len: usize, ) -> Vec> { // `u32` should be enough for array indices - let [start_id, sub_len] = [start_id, sub_len].map(|fe| fe.get_lower_32() as usize); - debug_assert!(sub_len <= max_len); + let [start_id, sub_len] = [start_id, sub_len].map(|fe| fe.get_lower_64() as usize); + debug_assert!(sub_len <= max_len, "{sub_len} > {max_len}"); ctx.assign_witnesses( array[start_id..start_id + sub_len] .iter() .map(|a| *a.value()) - .chain(iter::repeat(F::zero())) + .chain(iter::repeat(F::ZERO)) .take(max_len), ) } @@ -59,11 +64,13 @@ pub fn evaluate_byte_array( array: &[AssignedValue], len: AssignedValue, ) -> AssignedValue { - let f_256 = gate.get_field_element(256); if !array.is_empty() { - let incremental_evals = - gate.accumulated_product(ctx, iter::repeat(Constant(f_256)), array.iter().copied()); - let len_minus_one = gate.sub(ctx, len, Constant(F::one())); + let incremental_evals = gate.accumulated_product( + ctx, + iter::repeat(Constant(F::from(256))), + array.iter().copied(), + ); + let len_minus_one = gate.sub(ctx, len, Constant(F::ONE)); // if `len = 0` then `len_minus_one` will be very large, so `select_from_idx` will return 0. gate.select_from_idx(ctx, incremental_evals.iter().copied(), len_minus_one) } else { @@ -71,118 +78,8 @@ pub fn evaluate_byte_array( } } -#[derive(Clone, Debug)] -pub struct RlpFieldPrefixParsed { - is_not_literal: AssignedValue, - is_big: AssignedValue, - - next_len: AssignedValue, - len_len: AssignedValue, -} - -#[derive(Clone, Debug)] -pub struct RlpArrayPrefixParsed { - // is_empty: AssignedValue, - is_big: AssignedValue, - - next_len: AssignedValue, - len_len: AssignedValue, -} - -#[derive(Clone, Debug)] -pub struct RlpFieldWitness { - prefix: AssignedValue, // value of the prefix - prefix_len: AssignedValue, - len_len: AssignedValue, - len_cells: Vec>, - max_len_len: usize, - - pub field_len: AssignedValue, - pub field_cells: Vec>, - pub max_field_len: usize, -} - -#[derive(Clone, Debug)] -/// All witnesses involved in the RLP decoding of a byte string (non-list). -pub struct RlpFieldTraceWitness { - /// This contains the assigned witnesses of the raw byte string after RLP decoding. - pub witness: RlpFieldWitness, - /// This is the original raw RLP encoding byte string, padded with zeros to a known fixed maximum length - pub rlp_field: Vec>, - /// This is length of `rlp_field` in bytes, which can be variable. - pub rlp_len: AssignedValue, - // We need to keep the raw `rlp_field, rlp_len` because we still need to compute their variable length RLC in SecondPhase -} - -#[derive(Clone, Debug)] -/// The outputed values after RLP decoding a byte string (non-list) in SecondPhase. -/// This contains RLC of substrings from the decomposition. -pub struct RlpFieldTrace { - pub prefix: AssignedValue, // value of the prefix - pub prefix_len: AssignedValue, - pub len_trace: RlcTrace, - pub field_trace: RlcTrace, - // to save memory maybe we don't need this - // pub rlp_trace: RlcTrace, -} - -#[derive(Clone, Debug)] -pub struct RlpArrayTraceWitness { - pub field_witness: Vec>, - - pub len_len: AssignedValue, - pub len_cells: Vec>, - - pub rlp_len: AssignedValue, - pub rlp_array: Vec>, -} - -#[derive(Clone, Debug)] -pub struct RlpArrayTrace { - pub len_trace: RlcTrace, - pub field_trace: Vec>, - // to save memory we don't need this - // pub array_trace: RlcTrace, -} - -#[derive(Clone, Debug)] -pub struct RlcGateConfig { - pub rlc: RlcConfig, - pub gate: FlexGateConfig, -} - -#[derive(Clone, Debug)] -pub struct RlpConfig { - pub rlc: RlcConfig, - pub range: RangeConfig, -} - -impl RlpConfig { - pub fn configure( - meta: &mut ConstraintSystem, - num_rlc_columns: usize, - num_advice: &[usize], - num_lookup_advice: &[usize], - num_fixed: usize, - lookup_bits: usize, - circuit_degree: usize, - ) -> Self { - let mut range = RangeConfig::configure( - meta, - RangeStrategy::Vertical, - num_advice, - num_lookup_advice, - num_fixed, - lookup_bits, - circuit_degree, - ); - let rlc = RlcConfig::configure(meta, num_rlc_columns); - // blinding factors may have changed - range.gate.max_rows = (1 << circuit_degree) - meta.minimum_rows(); - Self { rlc, range } - } -} - +/// Chip for proving RLP decoding. This only contains references to other chips, so it can be freely copied. +/// This will only contain [RlcChip] after [SecondPhase]. #[derive(Clone, Copy, Debug)] pub struct RlpChip<'range, F: ScalarField> { pub rlc: Option<&'range RlcChip>, // We use this chip in FirstPhase when there is no RlcChip @@ -206,11 +103,6 @@ impl<'range, F: ScalarField> RlpChip<'range, F> { self.rlc.as_ref().expect("RlcChip should be constructed and used only in SecondPhase") } - /// Gets field element either from cache (for speed) or recalculates. - pub fn field_element(&self, n: u64) -> F { - self.gate().get_field_element(n) - } - /// Parse a byte by interpreting it as the first byte in the RLP encoding of a byte string. /// Constrains that the byte is valid for RLP encoding of byte strings (not a list). /// @@ -220,26 +112,27 @@ impl<'range, F: ScalarField> RlpChip<'range, F> { /// * For a single byte whose value is in the [0x00, 0x7f] (decimal [0, 127]) range, that byte is its own RLP encoding. /// * Otherwise, if a string is 0-55 bytes long, the RLP encoding consists of a single byte with value 0x80 (dec. 128) plus the length of the string followed by the string. The range of the first byte is thus [0x80, 0xb7] (dec. [128, 183]). /// * If a string is more than 55 bytes long, the RLP encoding consists of a single byte with value 0xb7 (dec. 183) plus the length in bytes of the length of the string in binary form, followed by the length of the string, followed by the string. - pub fn parse_rlp_field_prefix( + /// + /// ## Warning + /// This function does not constrain that if `is_big == true` then the string length must be greater than 55 bytes. This is done separately in `parse_rlp_len`. + fn parse_rlp_field_prefix( &self, ctx: &mut Context, prefix: AssignedValue, ) -> RlpFieldPrefixParsed { - let is_not_literal = - self.range.is_less_than(ctx, Constant(self.field_element(127)), prefix, 8); - let is_len_or_literal = - self.range.is_less_than(ctx, prefix, Constant(self.field_element(184)), 8); + let is_not_literal = self.range.is_less_than(ctx, Constant(F::from(127)), prefix, 8); + let is_len_or_literal = self.range.is_less_than(ctx, prefix, Constant(F::from(184)), 8); // is valid - self.range.check_less_than(ctx, prefix, Constant(self.field_element(192)), 8); + self.range.check_less_than(ctx, prefix, Constant(F::from(192)), 8); - let field_len = self.gate().sub(ctx, prefix, Constant(self.field_element(128))); - let len_len = self.gate().sub(ctx, prefix, Constant(self.field_element(183))); + let field_len = self.gate().sub(ctx, prefix, Constant(F::from(128))); + let len_len = self.gate().sub(ctx, prefix, Constant(F::from(183))); let is_big = self.gate().not(ctx, is_len_or_literal); // length of the next RLP field let next_len = self.gate().select(ctx, len_len, field_len, is_big); - let next_len = self.gate().select(ctx, next_len, Constant(F::one()), is_not_literal); + let next_len = self.gate().select(ctx, next_len, Constant(F::ONE), is_not_literal); let len_len = self.gate().mul(ctx, len_len, is_big); let len_len = self.gate().mul(ctx, is_not_literal, len_len); RlpFieldPrefixParsed { is_not_literal, is_big, next_len, len_len } @@ -253,44 +146,103 @@ impl<'range, F: ScalarField> RlpChip<'range, F> { /// https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/ /// * If the total payload of a list (i.e. the combined length of all its items being RLP encoded) is 0-55 bytes long, the RLP encoding consists of a single byte with value 0xc0 plus the length of the list followed by the concatenation of the RLP encodings of the items. The range of the first byte is thus [0xc0, 0xf7] (dec. [192, 247]). /// * If the total payload of a list is more than 55 bytes long, the RLP encoding consists of a single byte with value 0xf7 plus the length in bytes of the length of the payload in binary form, followed by the length of the payload, followed by the concatenation of the RLP encodings of the items. The range of the first byte is thus [0xf8, 0xff] (dec. [248, 255]). - pub fn parse_rlp_array_prefix( + /// + /// ## Warning + /// This function does not constrain that if `is_big == true` then the total payload length of the list must be greater than 55 bytes. This is done separately in `parse_rlp_len`. + fn parse_rlp_array_prefix( &self, ctx: &mut Context, prefix: AssignedValue, ) -> RlpArrayPrefixParsed { // is valid - self.range.check_less_than(ctx, Constant(self.field_element(191)), prefix, 8); + self.range.check_less_than(ctx, Constant(F::from(191)), prefix, 8); - let is_big = self.range.is_less_than(ctx, Constant(self.field_element(247)), prefix, 8); + let is_big = self.range.is_less_than(ctx, Constant(F::from(247)), prefix, 8); - let array_len = self.gate().sub(ctx, prefix, Constant(self.field_element(192))); - let len_len = self.gate().sub(ctx, prefix, Constant(self.field_element(247))); + let array_len = self.gate().sub(ctx, prefix, Constant(F::from(192))); + let len_len = self.gate().sub(ctx, prefix, Constant(F::from(247))); let next_len = self.gate().select(ctx, len_len, array_len, is_big); let len_len = self.gate().mul(ctx, len_len, is_big); RlpArrayPrefixParsed { is_big, next_len, len_len } } + /// Parse a byte by interpreting it as the first byte in the RLP encoding (either a byte string or a list). + /// Output should be identical to `parse_rlp_field_prefix` when `prefix` is a byte string. + /// Output should be identical to `parse_rlp_array_prefix` when `prefix` is a list. + /// + /// Assumes that `prefix` has already been range checked to be a byte + /// + /// ## Warning + /// This function does not constrain that if `is_big == true` then the total payload length must be greater than 55 bytes. This is done separately in `parse_rlp_len`. + fn parse_rlp_prefix( + &self, + ctx: &mut Context, + prefix: AssignedValue, + ) -> RlpPrefixParsed { + let is_not_literal = self.range.is_less_than(ctx, Constant(F::from(127)), prefix, 8); + let is_field = self.range.is_less_than(ctx, prefix, Constant(F::from(192)), 8); + + let is_big_if_field = self.range.is_less_than(ctx, Constant(F::from(183)), prefix, 8); + let is_big_if_array = self.range.is_less_than(ctx, Constant(F::from(247)), prefix, 8); + + let is_big = self.gate().select(ctx, is_big_if_field, is_big_if_array, is_field); + + let field_len = self.gate().sub(ctx, prefix, Constant(F::from(128))); + let field_len_len = self.gate().sub(ctx, prefix, Constant(F::from(183))); + let next_field_len = self.gate().select(ctx, field_len_len, field_len, is_big_if_field); + let next_field_len = + self.gate().select(ctx, next_field_len, Constant(F::ONE), is_not_literal); + + let array_len = self.gate().sub(ctx, prefix, Constant(F::from(192))); + let array_len_len = self.gate().sub(ctx, prefix, Constant(F::from(247))); + let next_array_len = self.gate().select(ctx, array_len_len, array_len, is_big_if_array); + + let next_len = self.gate().select(ctx, next_field_len, next_array_len, is_field); + let len_len = self.gate().select(ctx, field_len_len, array_len_len, is_field); + let len_len = self.gate().mul(ctx, len_len, is_big); + + RlpPrefixParsed { is_not_literal, is_big, next_len, len_len } + } + /// Given a full RLP encoding `rlp_cells` string, and the length in bytes of the length of the payload, `len_len`, parse the length of the payload. /// /// Assumes that it is known that `len_len <= max_len_len`. /// /// Returns the *witness* for the length of the payload in bytes, together with the BigInt value of this length, which is constrained assuming that the witness is valid. (The witness for the length as byte string is checked later in an RLC concatenation.) + /// + /// The BigInt value of the length is returned as `len_val`. + /// We constrain that `len_val > 55` if and only if `is_big == true`. + /// + /// ## Assumptions + /// * `rlp_cells` have already been constrained to be bytes. fn parse_rlp_len( &self, ctx: &mut Context, rlp_cells: &[AssignedValue], len_len: AssignedValue, max_len_len: usize, + is_big: AssignedValue, ) -> (Vec>, AssignedValue) { let len_cells = witness_subarray( ctx, rlp_cells, - &F::one(), // the 0th index is the prefix byte, and is skipped + &F::ONE, // the 0th index is the prefix byte, and is skipped len_len.value(), max_len_len, ); + // The conversion from length as BigInt to bytes must have no leading zeros + constrain_no_leading_zeros(ctx, self.gate(), &len_cells, len_len); let len_val = evaluate_byte_array(ctx, self.gate(), &len_cells, len_len); + // Constrain that `len_val > 55` if and only if `is_big == true`. + // `len_val` has at most `max_len_len * 8` bits, and `55` is 6 bits. + let len_is_big = self.range.is_less_than( + ctx, + Constant(F::from(55)), + len_val, + std::cmp::max(8 * max_len_len, 6), + ); + ctx.constrain_equal(&len_is_big, &is_big); (len_cells, len_val) } @@ -300,12 +252,16 @@ impl<'range, F: ScalarField> RlpChip<'range, F> { /// In the present function, the witnesses for the RLP decoding are computed and assigned. This decomposition is NOT yet constrained. /// /// Witnesses MUST be generated in `FirstPhase` to be able to compute RLC of them in `SecondPhase` + /// + /// # Assumptions + /// - `rlp_field` elements are already range checked to be bytes + // TODO: use SafeByte pub fn decompose_rlp_field_phase0( &self, ctx: &mut Context, // context for GateChip rlp_field: Vec>, max_field_len: usize, - ) -> RlpFieldTraceWitness { + ) -> RlpFieldWitness { let max_len_len = max_rlp_len_len(max_field_len); debug_assert_eq!(rlp_field.len(), 1 + max_len_len + max_field_len); @@ -335,7 +291,8 @@ impl<'range, F: ScalarField> RlpChip<'range, F> { let len_len = prefix_parsed.len_len; self.range.check_less_than_safe(ctx, len_len, (max_len_len + 1) as u64); - let (len_cells, len_byte_val) = self.parse_rlp_len(ctx, &rlp_field, len_len, max_len_len); + let (len_cells, len_byte_val) = + self.parse_rlp_len(ctx, &rlp_field, len_len, max_len_len, prefix_parsed.is_big); let field_len = self.gate().select(ctx, len_byte_val, prefix_parsed.next_len, prefix_parsed.is_big); @@ -351,61 +308,63 @@ impl<'range, F: ScalarField> RlpChip<'range, F> { let rlp_len = self.gate().sum(ctx, [prefix_parsed.is_not_literal, len_len, field_len]); - RlpFieldTraceWitness { - witness: RlpFieldWitness { - prefix, - prefix_len: prefix_parsed.is_not_literal, - len_len, - len_cells, - max_len_len, - field_len, - field_cells, - max_field_len, - }, - rlp_len, - rlp_field, + RlpFieldWitness { + prefix, + prefix_len: prefix_parsed.is_not_literal, + len_len, + len_cells, + field_len, + field_cells, + max_field_len, + encoded_item: rlp_field, + encoded_item_len: rlp_len, } } /// Use RLC to constrain the parsed RLP field witness. This MUST be done in `SecondPhase`. - /// - /// WARNING: this is not thread-safe if `load_rlc_cache` needs to be updated. pub fn decompose_rlp_field_phase1( &self, (ctx_gate, ctx_rlc): RlcContextPair, - rlp_field_witness: RlpFieldTraceWitness, + witness: RlpFieldWitness, ) -> RlpFieldTrace { - let RlpFieldTraceWitness { witness, rlp_len, rlp_field } = rlp_field_witness; let RlpFieldWitness { prefix, prefix_len, len_len, len_cells, - max_len_len, field_len, field_cells, max_field_len, + encoded_item: rlp_field, + encoded_item_len: rlp_field_len, } = witness; - let rlc = self.rlc(); + assert_eq!(max_rlp_len_len(max_field_len), len_cells.len()); + assert_eq!(max_field_len, field_cells.len()); - rlc.load_rlc_cache((ctx_gate, ctx_rlc), self.gate(), bit_length(rlp_field.len() as u64)); + let rlc = self.rlc(); + // Disabling as it should be called globally to avoid concurrency issues: + // rlc.load_rlc_cache((ctx_gate, ctx_rlc), self.gate(), bit_length(rlp_field.len() as u64)); let len_rlc = rlc.compute_rlc((ctx_gate, ctx_rlc), self.gate(), len_cells, len_len); let field_rlc = rlc.compute_rlc((ctx_gate, ctx_rlc), self.gate(), field_cells, field_len); - let rlp_field_rlc = rlc.compute_rlc((ctx_gate, ctx_rlc), self.gate(), rlp_field, rlp_len); + let rlp_field_rlc = + rlc.compute_rlc((ctx_gate, ctx_rlc), self.gate(), rlp_field, rlp_field_len); rlc.constrain_rlc_concat( ctx_gate, self.gate(), - [ - (prefix, prefix_len, 1), - (len_rlc.rlc_val, len_rlc.len, max_len_len), - (field_rlc.rlc_val, field_rlc.len, max_field_len), - ], - (&rlp_field_rlc.rlc_val, &rlp_field_rlc.len), + [RlcTrace::new(prefix, prefix_len, 1), len_rlc, field_rlc], + &rlp_field_rlc, + None, ); - RlpFieldTrace { prefix, prefix_len, len_trace: len_rlc, field_trace: field_rlc } + RlpFieldTrace { + prefix, + prefix_len, + len_trace: len_rlc, + field_trace: field_rlc, + rlp_trace: rlp_field_rlc, + } } /// Compute and assign witnesses for deserializing an RLP list of byte strings. Does not support nested lists. @@ -417,14 +376,19 @@ impl<'range, F: ScalarField> RlpChip<'range, F> { /// * Otherwise if `is_variable_len = true`, then `max_field_lens.len()` is assumed to be the maximum number of items of any list represented by this RLP encoding. /// * `max_field_lens` is the maximum length of each field in the list. /// - /// In order for the circuit to pass, the excess witness values in `rlp_array` beyond the actual RLP sequence should all be `0`s. + /// # Assumptions + /// * In order for the circuit to pass, the excess witness values in `rlp_array` beyond the actual RLP sequence should all be `0`s. + /// * `rlp_array` should be an array of `AssignedValue`s that are range checked to be bytes + /// + /// For each item in the array, we must decompose its prefix and length to determine how long the item is. + /// pub fn decompose_rlp_array_phase0( &self, ctx: &mut Context, // context for GateChip rlp_array: Vec>, max_field_lens: &[usize], is_variable_len: bool, - ) -> RlpArrayTraceWitness { + ) -> RlpArrayWitness { let max_rlp_array_len = rlp_array.len(); let max_len_len = max_rlp_len_len(max_rlp_array_len); @@ -450,15 +414,18 @@ impl<'range, F: ScalarField> RlpChip<'range, F> { // (field_rlc.rlc_val, field_rlc.rlc_len)]) let prefix = rlp_array[0]; - let prefix_parsed = self.parse_rlp_array_prefix(ctx, prefix); + let prefix_parsed = self.parse_rlp_array_prefix(ctx, prefix); let len_len = prefix_parsed.len_len; + let next_len = prefix_parsed.next_len; + let is_big = prefix_parsed.is_big; + self.range.check_less_than_safe(ctx, len_len, (max_len_len + 1) as u64); - let (len_cells, len_byte_val) = self.parse_rlp_len(ctx, &rlp_array, len_len, max_len_len); + let (len_cells, len_byte_val) = + self.parse_rlp_len(ctx, &rlp_array, len_len, max_len_len, is_big); - let list_payload_len = - self.gate().select(ctx, len_byte_val, prefix_parsed.next_len, prefix_parsed.is_big); + let list_payload_len = self.gate().select(ctx, len_byte_val, next_len, is_big); self.range.check_less_than_safe( ctx, list_payload_len, @@ -466,15 +433,16 @@ impl<'range, F: ScalarField> RlpChip<'range, F> { ); // this is automatically <= max_rlp_array_len - let rlp_len = self - .gate() - .sum(ctx, [Constant(F::one()), Existing(len_len), Existing(list_payload_len)]); + let rlp_len = + self.gate().sum(ctx, [Constant(F::ONE), Existing(len_len), Existing(list_payload_len)]); let mut field_witness = Vec::with_capacity(max_field_lens.len()); - let mut prefix_idx = self.gate().add(ctx, Constant(F::one()), Existing(len_len)); + let mut prefix_idx = self.gate().add(ctx, Constant(F::ONE), len_len); let mut running_max_len = max_len_len + 1; + let mut list_len = ctx.load_zero(); + for &max_field_len in max_field_lens { let mut prefix = self.gate().select_from_idx( ctx, @@ -482,37 +450,34 @@ impl<'range, F: ScalarField> RlpChip<'range, F> { rlp_array.iter().copied().take(running_max_len + 1), prefix_idx, ); - let prefix_parsed = self.parse_rlp_field_prefix(ctx, prefix); + let prefix_parsed = self.parse_rlp_prefix(ctx, prefix); let mut len_len = prefix_parsed.len_len; + let max_field_len_len = max_rlp_len_len(max_field_len); - self.range.check_less_than_safe(ctx, len_len, (max_field_len_len + 1) as u64); + self.range.check_less_than_safe( + ctx, + prefix_parsed.len_len, + (max_field_len_len + 1) as u64, + ); let len_start_id = *prefix_parsed.is_not_literal.value() + prefix_idx.value(); let len_cells = witness_subarray( ctx, &rlp_array, &len_start_id, - len_len.value(), + prefix_parsed.len_len.value(), max_field_len_len, ); - let field_byte_val = evaluate_byte_array(ctx, self.gate(), &len_cells, len_len); + let field_len_byte_val = evaluate_byte_array(ctx, self.gate(), &len_cells, len_len); let mut field_len = self.gate().select( ctx, - field_byte_val, + field_len_byte_val, prefix_parsed.next_len, prefix_parsed.is_big, ); - self.range.check_less_than_safe(ctx, field_len, (max_field_len + 1) as u64); - let field_cells = witness_subarray( - ctx, - &rlp_array, - &(len_start_id + len_len.value()), - field_len.value(), - max_field_len, - ); running_max_len += 1 + max_field_len_len + max_field_len; // prefix_len is either 0 or 1 @@ -525,103 +490,114 @@ impl<'range, F: ScalarField> RlpChip<'range, F> { rlp_len, bit_length(max_rlp_array_len as u64), ); + list_len = self.gate().add(ctx, list_len, field_in_list); // In cases where the RLP sequence is a list of unknown variable length, we keep track // of whether the corresponding index actually is a list item by constraining that // all of `prefix_len, len_len, field_len` are 0 when the current field should be treated // as a dummy and not actually in the list prefix_len = self.gate().mul(ctx, prefix_len, field_in_list); - len_len = self.gate().mul(ctx, len_len, field_in_list); + len_len = self.gate().mul(ctx, prefix_parsed.len_len, field_in_list); field_len = self.gate().mul(ctx, field_len, field_in_list); } + + self.range.check_less_than_safe(ctx, field_len, (max_field_len + 1) as u64); + + let field_cells = witness_subarray( + ctx, + &rlp_array, + &(len_start_id + len_len.value()), + field_len.value(), + max_field_len, + ); + + let encoded_item_len = + self.gate().sum(ctx, [prefix_parsed.is_not_literal, len_len, field_len]); + let encoded_item = witness_subarray( + ctx, + &rlp_array, + prefix_idx.value(), + encoded_item_len.value(), + max_rlp_encoding_len(max_field_len), + ); // *** unconstrained + prefix = self.gate().mul(ctx, prefix, prefix_len); - prefix_idx = self.gate().sum(ctx, [prefix_idx, prefix_len, len_len, field_len]); + prefix_idx = + self.gate().sum(ctx, [prefix_idx, prefix_len, prefix_parsed.len_len, field_len]); let witness = RlpFieldWitness { - prefix, - prefix_len, + prefix, // 0 if phantom or literal, 1st byte otherwise + prefix_len, // 0 if phantom or literal, 1 otherwise len_len, - len_cells, - max_len_len: max_field_len_len, - field_len, - field_cells, + len_cells, // have not constrained this subarray + field_len, // have not constrained the copy used to make this + field_cells, // have not constrained this subarray max_field_len, + encoded_item, // have not constrained this subarray + encoded_item_len, }; field_witness.push(witness); } - RlpArrayTraceWitness { field_witness, len_len, len_cells, rlp_len, rlp_array } + let list_len = is_variable_len.then_some(list_len); + RlpArrayWitness { field_witness, len_len, len_cells, rlp_len, rlp_array, list_len } } /// Use RLC to constrain the parsed RLP array witness. This MUST be done in `SecondPhase`. /// /// We do not make any guarantees on the values in the original RLP sequence beyond the parsed length for the total payload - /// - /// WARNING: this is not thread-safe if `load_rlc_cache` needs to be updated pub fn decompose_rlp_array_phase1( &self, (ctx_gate, ctx_rlc): RlcContextPair, - rlp_array_witness: RlpArrayTraceWitness, + rlp_array_witness: RlpArrayWitness, _is_variable_len: bool, ) -> RlpArrayTrace { - let RlpArrayTraceWitness { field_witness, len_len, len_cells, rlp_len, rlp_array } = + let RlpArrayWitness { field_witness, len_len, len_cells, rlp_len, rlp_array, .. } = rlp_array_witness; let rlc = self.rlc(); + // we only need rlc_pow up to the maximum length in a fragment of `constrain_rlc_concat` + // let max_item_rlp_len = + // field_witness.iter().map(|w| max_rlp_encoding_len(w.max_field_len)).max().unwrap(); + // Disabling this as it should be called once globally: + // rlc.load_rlc_cache((ctx_gate, ctx_rlc), self.gate(), bit_length(max_item_rlp_len as u64)); + let len_trace = rlc.compute_rlc((ctx_gate, ctx_rlc), self.gate(), len_cells, len_len); let mut field_trace = Vec::with_capacity(field_witness.len()); - for field_witness in field_witness { - let len_rlc = rlc.compute_rlc( + for w in field_witness { + let len_rlc = rlc.compute_rlc((ctx_gate, ctx_rlc), self.gate(), w.len_cells, w.len_len); + let field_rlc = + rlc.compute_rlc((ctx_gate, ctx_rlc), self.gate(), w.field_cells, w.field_len); + // RLC of the entire RLP encoded item + let rlp_trace = rlc.compute_rlc( (ctx_gate, ctx_rlc), self.gate(), - field_witness.len_cells, - field_witness.len_len, + w.encoded_item, + w.encoded_item_len, ); - let field_rlc = rlc.compute_rlc( - (ctx_gate, ctx_rlc), + // We need to constrain that the `encoded_item` is the concatenation of the prefix, the length, and the payload + rlc.constrain_rlc_concat( + ctx_gate, self.gate(), - field_witness.field_cells, - field_witness.field_len, + [RlcTrace::new(w.prefix, w.prefix_len, 1), len_rlc, field_rlc], + &rlp_trace, + None, ); field_trace.push(RlpFieldTrace { - prefix: field_witness.prefix, - prefix_len: field_witness.prefix_len, + prefix: w.prefix, + prefix_len: w.prefix_len, len_trace: len_rlc, field_trace: field_rlc, + rlp_trace, }); } - rlc.load_rlc_cache((ctx_gate, ctx_rlc), self.gate(), bit_length(rlp_array.len() as u64)); - let prefix = rlp_array[0]; - let one = ctx_gate.load_constant(F::one()); + let one = ctx_gate.load_constant(F::ONE); let rlp_rlc = rlc.compute_rlc((ctx_gate, ctx_rlc), self.gate(), rlp_array, rlp_len); - let inputs = iter::empty() - .chain([(prefix, one, 1), (len_trace.rlc_val, len_trace.len, len_trace.max_len)]) - .chain(field_trace.iter().flat_map(|trace| { - [ - (trace.prefix, trace.prefix_len, 1), - (trace.len_trace.rlc_val, trace.len_trace.len, trace.len_trace.max_len), - (trace.field_trace.rlc_val, trace.field_trace.len, trace.field_trace.max_len), - ] - })); - - rlc.constrain_rlc_concat(ctx_gate, self.gate(), inputs, (&rlp_rlc.rlc_val, &rlp_rlc.len)); - - // We do not constrain the witness values of trailing elements in `rlp_array` beyond `rlp_len`. To do so, uncomment below: - /* - let unused_array_len = - self.gate().sub(ctx, Constant(F::from(rlp_array.len() as u64)), Existing(&rlp_rlc.len)); - let suffix_pow = self.rlc.rlc_pow( - ctx, - self.gate(), - &unused_array_len, - bit_length(rlp_rlc.values.len() as u64), - ); - let suffix_check = self.gate().mul(ctx, - Existing(&suffix_pow), - Existing(&rlp_rlc.rlc_val)); - ctx.region.constrain_equal(suffix_check.cell(), rlp_rlc.rlc_max.cell()); - */ + .chain([RlcTrace::new(prefix, one, 1), len_trace]) + .chain(field_trace.iter().map(|trace| trace.rlp_trace)); + + rlc.constrain_rlc_concat(ctx_gate, self.gate(), inputs, &rlp_rlc, None); RlpArrayTrace { len_trace, field_trace } } diff --git a/axiom-eth/src/rlp/test_data/array_of_literals_big.json b/axiom-eth/src/rlp/test_data/array_of_literals_big.json new file mode 100644 index 00000000..89bfdade --- /dev/null +++ b/axiom-eth/src/rlp/test_data/array_of_literals_big.json @@ -0,0 +1,18 @@ +{ + "input": "f8408d123000000000000000000000028824232222222222238b32222222222222222412528a04233333333333332322912323333333333333333333333333333333", + "max_field_lens": [ + 20, + 20, + 20, + 20, + 20 + ], + "is_var_len": true, + "parsed": [ + "8d12300000000000000000000002", + "882423222222222223", + "8b3222222222222222241252", + "8a04233333333333332322", + "912323333333333333333333333333333333" + ] +} \ No newline at end of file diff --git a/axiom-eth/src/rlp/test_data/arrays_and_fields.json b/axiom-eth/src/rlp/test_data/arrays_and_fields.json new file mode 100644 index 00000000..6da44ded --- /dev/null +++ b/axiom-eth/src/rlp/test_data/arrays_and_fields.json @@ -0,0 +1,42 @@ +{ + "input": "d5808080c2340280c236038080808080808080808001", + "max_field_lens": [ + 2, + 2, + 2, + 2, + 2, + 8, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "is_var_len": true, + "parsed": [ + "80", + "80", + "80", + "c23402", + "80", + "c23603", + "80", + "80", + "80", + "80", + "80", + "80", + "80", + "80", + "80", + "80", + "01" + ] +} \ No newline at end of file diff --git a/axiom-eth/src/rlp/test_data/list_of_strings.json b/axiom-eth/src/rlp/test_data/list_of_strings.json new file mode 100644 index 00000000..4fafd4af --- /dev/null +++ b/axiom-eth/src/rlp/test_data/list_of_strings.json @@ -0,0 +1,12 @@ +{ + "input": "c88363617483646f67", + "max_field_lens": [ + 4, + 4 + ], + "is_var_len": true, + "parsed": [ + "83636174", + "83646f67" + ] +} \ No newline at end of file diff --git a/axiom-eth/src/rlp/test_data/nested_arrays.json b/axiom-eth/src/rlp/test_data/nested_arrays.json new file mode 100644 index 00000000..be7b3478 --- /dev/null +++ b/axiom-eth/src/rlp/test_data/nested_arrays.json @@ -0,0 +1,12 @@ +{ + "input": "d9820012d5808080c2340280c236038080808080808080808001", + "max_field_lens": [ + 5, + 25 + ], + "is_var_len": true, + "parsed": [ + "820012", + "d5808080c2340280c236038080808080808080808001" + ] +} \ No newline at end of file diff --git a/axiom-eth/src/rlp/test_data/nested_arrays_big.json b/axiom-eth/src/rlp/test_data/nested_arrays_big.json new file mode 100644 index 00000000..5954b5a4 --- /dev/null +++ b/axiom-eth/src/rlp/test_data/nested_arrays_big.json @@ -0,0 +1,16 @@ +{ + "input": "f872820012d5808080c2340280c236038080808080808080808001d5808080c2340280c236038080808080808080808001f841808080c2340280c236038080808080808080808001d5808080c2340280c236038080808080808080808001d5808080c2340280c236038080808080808080808001", + "max_field_lens": [ + 100, + 100, + 100, + 100 + ], + "is_var_len": true, + "parsed": [ + "820012", + "d5808080c2340280c236038080808080808080808001", + "d5808080c2340280c236038080808080808080808001", + "f841808080c2340280c236038080808080808080808001d5808080c2340280c236038080808080808080808001d5808080c2340280c236038080808080808080808001" + ] +} \ No newline at end of file diff --git a/axiom-eth/src/rlp/tests.rs b/axiom-eth/src/rlp/tests.rs deleted file mode 100644 index 131bc536..00000000 --- a/axiom-eth/src/rlp/tests.rs +++ /dev/null @@ -1,556 +0,0 @@ -use std::cell::RefCell; - -use crate::util::EthConfigParams; - -use super::{ - builder::*, - rlc::{RlcChip, RlcConfig}, - RlcGateConfig, RlpConfig, -}; -use halo2_base::{ - gates::flex_gate::{FlexGateConfig, GateStrategy}, - halo2_proofs::{ - circuit::{Layouter, SimpleFloorPlanner}, - plonk::{Advice, Circuit, Column, ConstraintSystem, Error, Selector}, - }, - utils::ScalarField, - SKIP_FIRST_PASS, -}; - -mod rlc { - use super::RlcCircuitBuilder; - use halo2_base::{ - gates::GateChip, - halo2_proofs::{ - dev::MockProver, - halo2curves::bn256::{Bn256, Fr, G1Affine}, - plonk::{create_proof, keygen_pk, keygen_vk, verify_proof, Error}, - poly::{ - commitment::ParamsProver, - kzg::{ - commitment::{KZGCommitmentScheme, ParamsKZG}, - multiopen::{ProverSHPLONK, VerifierSHPLONK}, - strategy::SingleStrategy, - }, - }, - transcript::{ - Blake2bRead, Blake2bWrite, Challenge255, TranscriptReadBuffer, - TranscriptWriterBuffer, - }, - }, - utils::ScalarField, - }; - use itertools::Itertools; - use rand::{rngs::StdRng, SeedableRng}; - use test_log::test; - - use crate::rlp::{ - builder::{FnSynthesize, RlcThreadBuilder}, - rlc::RlcChip, - }; - - const DEGREE: u32 = 10; - - fn rlc_test_circuit( - mut builder: RlcThreadBuilder, - _inputs: Vec, - _len: usize, - ) -> RlcCircuitBuilder> { - let ctx = builder.gate_builder.main(0); - let inputs = ctx.assign_witnesses(_inputs.clone()); - let len = ctx.load_witness(F::from(_len as u64)); - - let synthesize_phase1 = move |builder: &mut RlcThreadBuilder, rlc: &RlcChip| { - // the closure captures the `inputs` variable - log::info!("phase 1 synthesize begin"); - let gate = GateChip::default(); - - let (ctx_gate, ctx_rlc) = builder.rlc_ctx_pair(); - let rlc_trace = rlc.compute_rlc((ctx_gate, ctx_rlc), &gate, inputs, len); - let rlc_val = *rlc_trace.rlc_val.value(); - let real_rlc = compute_rlc_acc(&_inputs[.._len], *rlc.gamma()); - assert_eq!(real_rlc, rlc_val); - }; - - RlcCircuitBuilder::new(builder, None, synthesize_phase1) - } - - fn compute_rlc_acc(msg: &[F], r: F) -> F { - let mut rlc = msg[0]; - for val in msg.iter().skip(1) { - rlc = rlc * r + val; - } - rlc - } - - #[test] - pub fn test_mock_rlc() { - let k = DEGREE; - let input_bytes = vec![ - 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, - 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0, - ] - .into_iter() - .map(|x| Fr::from(x as u64)) - .collect_vec(); - let len = 32; - - let circuit = rlc_test_circuit(RlcThreadBuilder::mock(), input_bytes, len); - - circuit.config(k as usize, Some(6)); - MockProver::run(k, &circuit, vec![]).unwrap().assert_satisfied(); - } - - #[test] - pub fn test_rlc() -> Result<(), Error> { - let k = DEGREE; - let input_bytes = vec![ - 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, - 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0, - ] - .into_iter() - .map(|x| Fr::from(x as u64)) - .collect_vec(); - let len = 32; - - let mut rng = StdRng::from_seed([0u8; 32]); - let params = ParamsKZG::::setup(k, &mut rng); - let circuit = rlc_test_circuit(RlcThreadBuilder::keygen(), input_bytes.clone(), len); - circuit.config(k as usize, Some(6)); - - println!("vk gen started"); - let vk = keygen_vk(¶ms, &circuit)?; - println!("vk gen done"); - let pk = keygen_pk(¶ms, vk, &circuit)?; - println!("pk gen done"); - println!(); - println!("==============STARTING PROOF GEN==================="); - let break_points = circuit.break_points.take(); - drop(circuit); - let circuit = rlc_test_circuit(RlcThreadBuilder::prover(), input_bytes, len); - *circuit.break_points.borrow_mut() = break_points; - - let mut transcript = Blake2bWrite::<_, _, Challenge255<_>>::init(vec![]); - create_proof::< - KZGCommitmentScheme, - ProverSHPLONK<'_, Bn256>, - Challenge255, - _, - Blake2bWrite, G1Affine, Challenge255>, - _, - >(¶ms, &pk, &[circuit], &[&[]], rng, &mut transcript)?; - let proof = transcript.finalize(); - println!("proof gen done"); - let verifier_params = params.verifier_params(); - let strategy = SingleStrategy::new(verifier_params); - let mut transcript = Blake2bRead::<_, _, Challenge255<_>>::init(&proof[..]); - verify_proof::< - KZGCommitmentScheme, - VerifierSHPLONK<'_, Bn256>, - Challenge255, - Blake2bRead<&[u8], G1Affine, Challenge255>, - SingleStrategy<'_, Bn256>, - >(verifier_params, pk.get_vk(), strategy, &[&[]], &mut transcript) - .unwrap(); - println!("verify done"); - Ok(()) - } -} - -mod rlp { - use super::RlpCircuitBuilder; - use crate::rlp::{ - builder::{FnSynthesize, RlcThreadBuilder}, - *, - }; - use halo2_base::halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr}; - use hex::FromHex; - use std::env::set_var; - use test_log::test; - - const DEGREE: u32 = 18; - - fn rlp_string_circuit( - mut builder: RlcThreadBuilder, - encoded: Vec, - max_len: usize, - ) -> RlpCircuitBuilder> { - let prover = builder.witness_gen_only(); - let ctx = builder.gate_builder.main(0); - let inputs = ctx.assign_witnesses(encoded.iter().map(|x| F::from(*x as u64))); - set_var("LOOKUP_BITS", "8"); - let range = RangeChip::default(8); - let chip = RlpChip::new(&range, None); - let witness = chip.decompose_rlp_field_phase0(ctx, inputs, max_len); - - let f = move |b: &mut RlcThreadBuilder, rlc: &RlcChip| { - let chip = RlpChip::new(&range, Some(rlc)); - // closure captures `witness` variable - log::info!("phase 1 synthesize begin"); - let (ctx_gate, ctx_rlc) = b.rlc_ctx_pair(); - chip.decompose_rlp_field_phase1((ctx_gate, ctx_rlc), witness); - }; - let circuit = RlpCircuitBuilder::new(builder, None, f); - // auto-configure circuit if not in prover mode for convenience - if !prover { - circuit.config(DEGREE as usize, Some(6)); - } - circuit - } - - fn rlp_list_circuit( - mut builder: RlcThreadBuilder, - encoded: Vec, - max_field_lens: &[usize], - is_var_len: bool, - ) -> RlpCircuitBuilder> { - let prover = builder.witness_gen_only(); - let ctx = builder.gate_builder.main(0); - let inputs = ctx.assign_witnesses(encoded.iter().map(|x| F::from(*x as u64))); - set_var("LOOKUP_BITS", "8"); - let range = RangeChip::default(8); - let chip = RlpChip::new(&range, None); - let witness = chip.decompose_rlp_array_phase0(ctx, inputs, max_field_lens, is_var_len); - - let circuit = RlpCircuitBuilder::new( - builder, - None, - move |builder: &mut RlcThreadBuilder, rlc: &RlcChip| { - let chip = RlpChip::new(&range, Some(rlc)); - // closure captures `witness` variable - log::info!("phase 1 synthesize begin"); - chip.decompose_rlp_array_phase1(builder.rlc_ctx_pair(), witness, is_var_len); - }, - ); - if !prover { - circuit.config(DEGREE as usize, Some(6)); - } - circuit - } - - #[test] - pub fn test_mock_rlp_array() { - let k = DEGREE; - // the list [ "cat", "dog" ] = [ 0xc8, 0x83, 'c', 'a', 't', 0x83, 'd', 'o', 'g' ] - let cat_dog: Vec = vec![0xc8, 0x83, b'c', b'a', b't', 0x83, b'd', b'o', b'g']; - // the empty list = [ 0xc0 ] - let empty_list: Vec = vec![0xc0]; - let input_bytes: Vec = Vec::from_hex("f8408d123000000000000000000000028824232222222222238b32222222222222222412528a04233333333333332322912323333333333333333333333333333333000000").unwrap(); - - for mut test_input in [cat_dog, empty_list, input_bytes] { - test_input.append(&mut vec![0; 69 - test_input.len()]); - let circuit = rlp_list_circuit( - RlcThreadBuilder::::mock(), - test_input, - &[15, 9, 11, 10, 17], - true, - ); - MockProver::run(k, &circuit, vec![]).unwrap().assert_satisfied(); - } - } - - #[test] - pub fn test_mock_rlp_field() { - let k = DEGREE; - let input_bytes: Vec = - Vec::from_hex("a012341234123412341234123412341234123412341234123412341234123412340000") - .unwrap(); - let circuit = rlp_string_circuit(RlcThreadBuilder::::mock(), input_bytes, 34); - MockProver::run(k, &circuit, vec![]).unwrap().assert_satisfied(); - } - - #[test] - pub fn test_mock_rlp_short_field() { - let k = DEGREE; - let mut input_bytes: Vec = vec![127]; - input_bytes.resize(35, 0); - - let circuit = rlp_string_circuit(RlcThreadBuilder::::mock(), input_bytes, 34); - MockProver::run(k, &circuit, vec![]).unwrap().assert_satisfied(); - } - - #[test] - pub fn test_mock_rlp_literal() { - let k = DEGREE; - let mut input_bytes: Vec = vec![0]; - input_bytes.resize(33, 0); - let circuit = rlp_string_circuit(RlcThreadBuilder::::mock(), input_bytes, 32); - MockProver::run(k, &circuit, vec![]).unwrap().assert_satisfied(); - } - - #[test] - pub fn test_mock_rlp_long_field() { - let k = DEGREE; - let input_bytes: Vec = Vec::from_hex("a09bdb004d9b1e7f3e5f86fbdc9856f21f9dcb07a44c42f5de8eec178514d279df0000000000000000000000000000000000000000000000000000000000").unwrap(); - - let circuit = rlp_string_circuit(RlcThreadBuilder::::mock(), input_bytes, 60); - MockProver::run(k, &circuit, vec![]).unwrap().assert_satisfied(); - } - - #[test] - pub fn test_mock_rlp_long_long_field() { - let k = DEGREE; - let input_bytes: Vec = Vec::from_hex("b83adb004d9b1e7f3e5f86fbdc9856f21f9dcb07a44c42f5de8eec178514d279df0000000000000000000000000000000000000000000000000000000000").unwrap(); - - let circuit = rlp_string_circuit(RlcThreadBuilder::::mock(), input_bytes, 60); - MockProver::run(k, &circuit, vec![]).unwrap().assert_satisfied(); - } -} - -// The circuits below are mostly used for testing. -// Unfortunately for `KeccakCircuitBuilder` we still need to do some more custom stuff beyond what's in this circuit -// due to the intricacies of 2-phase challenge API. - -/// A wrapper struct to auto-build a circuit from a `RlcThreadBuilder`. -/// -/// This struct is trickier because it uses the Multi-phase Challenge API. The intended use is as follows: -/// * The user can run phase 0 calculations on `builder` outside of the circuit (as usual) and supply the builder to construct the circuit. -/// * The user also specifies a closure `synthesize_phase1(builder, challenge)` that specifies all calculations that should be done in phase 1. -/// The builder will then handle the process of assigning all advice cells in phase 1, squeezing a challenge value `challenge` from the backend API, and then using that value to do all phase 1 witness generation. -pub struct RlcCircuitBuilder -where - FnPhase1: FnSynthesize, -{ - pub builder: RefCell>, - pub break_points: RefCell, // `RefCell` allows the circuit to record break points in a keygen call of `synthesize` for use in later witness gen - // we guarantee that `synthesize_phase1` is called *exactly once* during the proving stage, but since `Circuit::synthesize` takes `&self`, and `assign_region` takes a `Fn` instead of `FnOnce`, we need some extra engineering: - pub synthesize_phase1: RefCell>, -} - -impl RlcCircuitBuilder -where - FnPhase1: FnSynthesize, -{ - pub fn new( - builder: RlcThreadBuilder, - break_points: Option, - synthesize_phase1: FnPhase1, - ) -> Self { - Self { - builder: RefCell::new(builder), - break_points: RefCell::new(break_points.unwrap_or_default()), - synthesize_phase1: RefCell::new(Some(synthesize_phase1)), - } - } - - pub fn config(&self, k: usize, minimum_rows: Option) -> EthConfigParams { - // clone everything so we don't alter the circuit in any way for later calls - let mut builder = self.builder.borrow().clone(); - let f = self.synthesize_phase1.borrow().clone().expect("synthesize_phase1 should exist"); - f(&mut builder, &RlcChip::new(F::zero())); - builder.config(k, minimum_rows) - } - - // re-usable function for synthesize - pub fn two_phase_synthesize( - &self, - gate: &FlexGateConfig, - lookup_advice: &[Vec>], - q_lookup: &[Option], - rlc: &RlcConfig, - layouter: &mut impl Layouter, - ) { - let mut first_pass = SKIP_FIRST_PASS; - #[cfg(feature = "halo2-axiom")] - let witness_gen_only = self.builder.borrow().witness_gen_only(); - // in non halo2-axiom, the prover calls `synthesize` twice: first just to get FirstPhase advice columns, commit, and then generate challenge value; then the second time to actually compute SecondPhase advice - // our "Prover" implementation is heavily optimized for the Axiom version, which only calls `synthesize` once - #[cfg(not(feature = "halo2-axiom"))] - let witness_gen_only = false; - - let mut gamma = None; - if !witness_gen_only { - // in these cases, synthesize is called twice, and challenge can be gotten after the first time, or we use dummy value 0 - layouter.get_challenge(rlc.gamma).map(|gamma_| gamma = Some(gamma_)); - } - - layouter - .assign_region( - || "RlcCircuitBuilder generated circuit", - |mut region| { - if first_pass { - first_pass = false; - return Ok(()); - } - if !witness_gen_only { - let mut builder = self.builder.borrow().clone(); - let f = self - .synthesize_phase1 - .borrow() - .clone() - .expect("synthesize_phase1 should exist"); - // call the actual synthesize function - let rlc_chip = RlcChip::new(gamma.unwrap_or_else(|| F::zero())); - f(&mut builder, &rlc_chip); - let KeygenAssignments { - assigned_advices: _, - assigned_constants: _, - break_points, - } = builder.assign_all( - gate, - lookup_advice, - q_lookup, - rlc, - &mut region, - Default::default(), - ); - *self.break_points.borrow_mut() = break_points; - } else { - let builder = &mut self.builder.borrow_mut(); - let break_points = &mut self.break_points.borrow_mut(); - assign_prover_phase0( - &mut region, - gate, - lookup_advice, - builder, - break_points, - ); - // this is a special backend API function (in halo2-axiom only) that computes the KZG commitments for all columns in FirstPhase and performs Fiat-Shamir on them to return the challenge value - #[cfg(feature = "halo2-axiom")] - region.next_phase(); - // get challenge value - let mut gamma = None; - #[cfg(feature = "halo2-axiom")] - region.get_challenge(rlc.gamma).map(|gamma_| { - log::info!("gamma: {gamma_:?}"); - gamma = Some(gamma_); - }); - let rlc_chip = - RlcChip::new(gamma.expect("Could not get challenge in second phase")); - let f = RefCell::take(&self.synthesize_phase1) - .expect("synthesize_phase1 should exist"); // we `take` the closure during proving to avoid cloning captured variables (the captured variables would be the AssignedValue payload sent from FirstPhase to SecondPhase) - assign_prover_phase1( - &mut region, - gate, - lookup_advice, - rlc, - &rlc_chip, - builder, - break_points, - f, - ); - } - Ok(()) - }, - ) - .unwrap(); - } -} - -impl Circuit for RlcCircuitBuilder -where - FnPhase1: FnSynthesize, -{ - type Config = RlcGateConfig; - type FloorPlanner = SimpleFloorPlanner; - - fn without_witnesses(&self) -> Self { - unimplemented!() - } - - fn configure(meta: &mut ConstraintSystem) -> RlcGateConfig { - let EthConfigParams { - degree, - num_rlc_columns, - num_range_advice, - num_lookup_advice: _, - num_fixed, - unusable_rows: _, - keccak_rows_per_round: _, - lookup_bits: _, - } = serde_json::from_str(&std::env::var("ETH_CONFIG_PARAMS").unwrap()).unwrap(); - let mut gate = FlexGateConfig::configure( - meta, - GateStrategy::Vertical, - &num_range_advice, - num_fixed, - degree as usize, - ); - let rlc = RlcConfig::configure(meta, num_rlc_columns); - // number of blinding factors may have changed due to introduction of new RLC gate - gate.max_rows = (1 << degree) - meta.minimum_rows(); - RlcGateConfig { gate, rlc } - } - - fn synthesize( - &self, - config: Self::Config, - mut layouter: impl Layouter, - ) -> Result<(), Error> { - self.two_phase_synthesize(&config.gate, &[], &[], &config.rlc, &mut layouter); - Ok(()) - } -} - -/// A wrapper around RlcCircuitBuilder where Gate is replaced by Range in the circuit -pub struct RlpCircuitBuilder(RlcCircuitBuilder) -where - FnPhase1: FnSynthesize; - -impl RlpCircuitBuilder -where - FnPhase1: FnSynthesize, -{ - pub fn new( - builder: RlcThreadBuilder, - break_points: Option, - synthesize_phase1: FnPhase1, - ) -> Self { - Self(RlcCircuitBuilder::new(builder, break_points, synthesize_phase1)) - } - - pub fn config(&self, k: usize, minimum_rows: Option) -> EthConfigParams { - self.0.config(k, minimum_rows) - } -} - -impl Circuit for RlpCircuitBuilder -where - FnPhase1: FnSynthesize, -{ - type Config = RlpConfig; - type FloorPlanner = SimpleFloorPlanner; - - fn without_witnesses(&self) -> Self { - unimplemented!() - } - - fn configure(meta: &mut ConstraintSystem) -> RlpConfig { - let EthConfigParams { - degree, - num_rlc_columns, - num_range_advice, - num_lookup_advice, - num_fixed, - unusable_rows: _, - keccak_rows_per_round: _, - lookup_bits: _, - } = serde_json::from_str(&std::env::var("ETH_CONFIG_PARAMS").unwrap()).unwrap(); - let lookup_bits = std::env::var("LOOKUP_BITS").unwrap().parse().unwrap(); - RlpConfig::configure( - meta, - num_rlc_columns, - &num_range_advice, - &num_lookup_advice, - num_fixed, - lookup_bits, - degree as usize, - ) - } - - fn synthesize( - &self, - config: Self::Config, - mut layouter: impl Layouter, - ) -> Result<(), Error> { - config.range.load_lookup_table(&mut layouter)?; - self.0.two_phase_synthesize( - &config.range.gate, - &config.range.lookup_advice, - &config.range.q_lookup, - &config.rlc, - &mut layouter, - ); - Ok(()) - } -} diff --git a/axiom-eth/src/rlp/tests/combo.rs b/axiom-eth/src/rlp/tests/combo.rs new file mode 100644 index 00000000..666653b6 --- /dev/null +++ b/axiom-eth/src/rlp/tests/combo.rs @@ -0,0 +1,74 @@ +use std::fs::File; + +use super::*; + +use serde::{Deserialize, Serialize}; +use test_case::test_case; + +#[derive(Default, Clone, Debug, Serialize, Deserialize)] +struct RlpComboTestInput { + pub input: String, + pub max_field_lens: Vec, + pub is_var_len: bool, + pub parsed: Vec, +} + +impl RlcCircuitInstructions for RlpComboTestInput { + type FirstPhasePayload = (RlpArrayWitness, bool); + + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + range: &RangeChip, + ) -> Self::FirstPhasePayload { + let ctx = builder.base.main(0); + let mut input_bytes: Vec = Vec::from_hex(&self.input).unwrap(); + let input_size = self.max_field_lens.iter().sum::() * 2; + input_bytes.resize(input_size, 0u8); + let inputs = ctx.assign_witnesses(input_bytes.iter().map(|x| Fr::from(*x as u64))); + let chip = RlpChip::new(range, None); + let witness = + chip.decompose_rlp_array_phase0(ctx, inputs, &self.max_field_lens, self.is_var_len); + assert_eq!(witness.field_witness.len(), self.parsed.len()); + for (item_witness, parsed) in witness.field_witness.iter().zip(self.parsed.iter()) { + let parsed_bytes = Vec::from_hex(parsed).unwrap(); + + let field = &item_witness.encoded_item; + let parsed_bytes = parsed_bytes.iter().map(|x| Fr::from(*x as u64)); + for (a, b) in field.iter().zip(parsed_bytes) { + assert_eq!(a.value(), &b); + } + } + (witness, self.is_var_len) + } + + fn virtual_assign_phase1( + builder: &mut RlcCircuitBuilder, + range: &RangeChip, + rlc: &RlcChip, + (witness, is_var_len): Self::FirstPhasePayload, + ) { + let chip = RlpChip::new(range, Some(rlc)); + chip.decompose_rlp_array_phase1(builder.rlc_ctx_pair(), witness, is_var_len); + } +} + +#[test_case("src/rlp/test_data/list_of_strings.json" ; "fields")] +#[test_case("src/rlp/test_data/arrays_and_fields.json" ; "arrays and fields")] +#[test_case("src/rlp/test_data/nested_arrays.json" ; "nested arrays")] +#[test_case("src/rlp/test_data/array_of_literals_big.json" ; "big array of literals")] +#[test_case("src/rlp/test_data/nested_arrays_big.json" ; "big nested arrays")] + +pub fn test_mock_rlp_combo(path: &str) { + let k = DEGREE; + let input: RlpComboTestInput = + serde_json::from_reader(File::open(path).expect("path does not exist")).unwrap(); + + let builder = RlcCircuitBuilder::from_stage(CircuitBuilderStage::Mock, 7) + .use_k(DEGREE as usize) + .use_lookup_bits(8); + let circuit = RlcExecutor::new(builder, input); + circuit.0.calculate_params(Some(20)); + + MockProver::run(k, &circuit, vec![]).unwrap().assert_satisfied(); +} diff --git a/axiom-eth/src/rlp/tests/list.rs b/axiom-eth/src/rlp/tests/list.rs new file mode 100644 index 00000000..5a63fcf1 --- /dev/null +++ b/axiom-eth/src/rlp/tests/list.rs @@ -0,0 +1,177 @@ +use super::*; +use test_log::test; + +// Both positive and negative tests for RLP decoding a byte string +struct RlpListTest { + encoded: Vec, + max_field_lens: Vec, + is_var_len: bool, + prank_idx: Option, + prank_component: Option, + _marker: PhantomData, +} + +impl RlcCircuitInstructions for RlpListTest { + type FirstPhasePayload = (RlpArrayWitness, bool); + + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + range: &RangeChip, + ) -> Self::FirstPhasePayload { + let ctx = builder.base.main(0); + let inputs = ctx.assign_witnesses(self.encoded.iter().map(|x| F::from(*x as u64))); + let chip = RlpChip::new(range, None); + let mut witness = + chip.decompose_rlp_array_phase0(ctx, inputs, &self.max_field_lens, self.is_var_len); + if let Some(prank_component) = self.prank_component { + let (prank_len, prank_comp) = match prank_component { + 0 => (witness.rlp_len, &mut witness.rlp_array), + 1 => (witness.len_len, &mut witness.len_cells), + 2 => ( + witness.field_witness[0].encoded_item_len, + &mut witness.field_witness[0].encoded_item, + ), + _ => ( + witness.field_witness[0].encoded_item_len, + &mut witness.field_witness[0].encoded_item, + ), + }; + if let Some(prank_idx) = self.prank_idx { + if prank_component < 3 && prank_idx < prank_comp.len() { + let prankval = range.gate().add(ctx, prank_comp[prank_idx], Constant(F::ONE)); + prank_comp[prank_idx].debug_prank(ctx, *prankval.value()); + } + let bad_idx = range.is_less_than_safe(ctx, prank_len, prank_idx as u64 + 1); + let zero = ctx.load_zero(); + ctx.constrain_equal(&bad_idx, &zero); + } + } + (witness, self.is_var_len) + } + + fn virtual_assign_phase1( + builder: &mut RlcCircuitBuilder, + range: &RangeChip, + rlc: &RlcChip, + (witness, is_var_len): (RlpArrayWitness, bool), + ) { + let chip = RlpChip::new(range, Some(rlc)); + let (ctx_gate, ctx_rlc) = builder.rlc_ctx_pair(); + chip.decompose_rlp_array_phase1((ctx_gate, ctx_rlc), witness, is_var_len); + } +} + +fn rlp_list_circuit( + stage: CircuitBuilderStage, + encoded: Vec, + max_field_lens: &[usize], + is_var_len: bool, + prank_idx: Option, + prank_component: Option, +) -> RlcCircuit> { + let input = RlpListTest { + encoded, + max_field_lens: max_field_lens.to_vec(), + is_var_len, + prank_idx, + prank_component, + _marker: PhantomData, + }; + let mut builder = RlcCircuitBuilder::from_stage(stage, 10).use_k(DEGREE as usize); + builder.base.set_lookup_bits(8); + let circuit = RlcExecutor::new(builder, input); + if !stage.witness_gen_only() { + circuit.0.calculate_params(Some(9)); + } + circuit +} + +#[test] +pub fn test_mock_rlp_array() { + let k = DEGREE; + // the list [ "cat", "dog" ] = [ 0xc8, 0x83, 'c', 'a', 't', 0x83, 'd', 'o', 'g' ] + let cat_dog: Vec = vec![0xc8, 0x83, b'c', b'a', b't', 0x83, b'd', b'o', b'g']; + // the empty list = [ 0xc0 ] + let empty_list: Vec = vec![0xc0]; + let input_bytes: Vec = Vec::from_hex("f8408d123000000000000000000000028824232222222222238b32222222222222222412528a04233333333333332322912323333333333333333333333333333333000000").unwrap(); + + for mut test_input in [cat_dog, empty_list, input_bytes] { + test_input.resize(69, 0); + let circuit = rlp_list_circuit::( + CircuitBuilderStage::Mock, + test_input, + &[15, 9, 11, 10, 17], + true, + None, + None, + ); + MockProver::run(k, &circuit, vec![]).unwrap().assert_satisfied(); + } +} + +#[test] +pub fn prank_test_mock_rlp_array() { + let k = DEGREE; + // the list [ "cat", "dog" ] = [ 0xc8, 0x83, 'c', 'a', 't', 0x83, 'd', 'o', 'g' ] + let cat_dog: Vec = vec![0xc8, 0x83, b'c', b'a', b't', 0x83, b'd', b'o', b'g']; + // the empty list = [ 0xc0 ] + let empty_list: Vec = vec![0xc0]; + let input_bytes: Vec = Vec::from_hex("f8408d123000000000000000000000028824232222222222238b32222222222222222412528a04233333333333332322912323333333333333333333333333333333000000").unwrap(); + + for mut test_input in [cat_dog, empty_list, input_bytes] { + let prank_lens = [test_input.len(), 3, test_input.len()]; + for (j, prank_len) in prank_lens.into_iter().enumerate() { + test_input.resize(69, 0); + for i in 0..prank_len { + let circuit = rlp_list_circuit::( + CircuitBuilderStage::Mock, + test_input.clone(), + &[15, 9, 11, 10, 17], + true, + Some(i), + Some(j), + ); + let prover = MockProver::run(k, &circuit, vec![]).unwrap(); + assert!(prover.verify().is_err(), "Unconstrained at {i}. Should not have verified"); + } + } + } +} + +#[test] +fn test_list_len_not_big_fail() { + let k = DEGREE; + // the list [ "cat", "dog" ] = [ 0xc8, 0x83, 'c', 'a', 't', 0x83, 'd', 'o', 'g' ] + // try to do RLP encoding where length is_big = true (even though it's not) + // 0x08 is the length of the payload + let mut attack: Vec = vec![0xf8, 0x08, 0x83, b'c', b'a', b't', 0x83, b'd', b'o', b'g']; + attack.resize(69, 0); + let circuit = rlp_list_circuit::( + CircuitBuilderStage::Mock, + attack, + &[15, 9, 11, 10, 17], + true, + None, + None, + ); + assert!(MockProver::run(k, &circuit, vec![]).unwrap().verify().is_err()); +} + +#[test] +fn test_list_len_leading_zeros_fail() { + let k = DEGREE; + // original: + // let input_bytes: Vec = Vec::from_hex("f8408d123000000000000000000000028824232222222222238b32222222222222222412528a04233333333333332322912323333333333333333333333333333333000000").unwrap(); + let mut attack: Vec = Vec::from_hex("f900408d123000000000000000000000028824232222222222238b32222222222222222412528a04233333333333332322912323333333333333333333333333333333000000").unwrap(); + attack.resize(310, 0); + let circuit = rlp_list_circuit::( + CircuitBuilderStage::Mock, + attack, + &[256, 9, 11, 10, 17], // make max payload length > 256 so we allow 2 bytes for len_len + true, + None, + None, + ); + assert!(MockProver::run(k, &circuit, vec![]).unwrap().verify().is_err()); +} diff --git a/axiom-eth/src/rlp/tests/mod.rs b/axiom-eth/src/rlp/tests/mod.rs new file mode 100644 index 00000000..43d1cb1e --- /dev/null +++ b/axiom-eth/src/rlp/tests/mod.rs @@ -0,0 +1,24 @@ +use std::marker::PhantomData; + +use crate::rlc::{ + chip::RlcChip, + circuit::{builder::RlcCircuitBuilder, instructions::RlcCircuitInstructions}, + tests::utils::executor::{RlcCircuit, RlcExecutor}, +}; + +use halo2_base::{ + gates::{circuit::CircuitBuilderStage, GateInstructions, RangeChip, RangeInstructions}, + utils::ScalarField, + QuantumCell::Constant, +}; + +use halo2_base::halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr}; +use hex::FromHex; + +use super::{max_rlp_encoding_len, types::RlpArrayWitness, RlpChip}; + +mod combo; +mod list; +mod string; + +const DEGREE: u32 = 10; diff --git a/axiom-eth/src/rlp/tests/string.rs b/axiom-eth/src/rlp/tests/string.rs new file mode 100644 index 00000000..07b87825 --- /dev/null +++ b/axiom-eth/src/rlp/tests/string.rs @@ -0,0 +1,122 @@ +use crate::rlp::types::RlpFieldWitness; + +use super::*; +use test_case::test_case; + +// Both positive and negative tests for RLP decoding a byte string +struct RlpStringTest { + encoded: Vec, + max_len: usize, + prank_idx: Option, + prank_component: Option, + _marker: PhantomData, +} + +impl RlcCircuitInstructions for RlpStringTest { + type FirstPhasePayload = RlpFieldWitness; + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + range: &RangeChip, + ) -> Self::FirstPhasePayload { + let ctx = builder.base.main(0); + let inputs = ctx.assign_witnesses(self.encoded.iter().map(|x| F::from(*x as u64))); + let chip = RlpChip::new(range, None); + let mut witness = chip.decompose_rlp_field_phase0(ctx, inputs, self.max_len); + // pranking for negative tests + if let Some(prank_component) = self.prank_component { + let (prank_comp, mut prank_len) = match prank_component { + 0 => (&mut witness.encoded_item, &witness.encoded_item_len), + 1 => (&mut witness.len_cells, &witness.len_len), + 2 => (&mut witness.field_cells, &witness.field_len), + _ => (&mut witness.field_cells, &witness.field_len), + }; + if let Some(prank_idx) = self.prank_idx { + if prank_component < 3 { + if prank_idx < prank_comp.len() { + let prankval = *prank_comp[prank_idx].value() + F::ONE; + prank_comp[prank_idx].debug_prank(ctx, prankval); + } + } else { + prank_len = &witness.prefix_len; + if prank_idx == 0 { + let prankval = *prank_comp[prank_idx].value() + F::ONE; + witness.prefix.debug_prank(ctx, prankval); + } + } + let bad_idx = range.is_less_than_safe(ctx, *prank_len, prank_idx as u64 + 1); + println!("{:?}", *prank_len); + println!("{:?}", prank_idx); + let zero = ctx.load_zero(); + ctx.constrain_equal(&bad_idx, &zero); + } + } + witness + } + fn virtual_assign_phase1( + builder: &mut RlcCircuitBuilder, + range: &RangeChip, + rlc: &RlcChip, + witness: Self::FirstPhasePayload, + ) { + let chip = RlpChip::new(range, Some(rlc)); + let (ctx_gate, ctx_rlc) = builder.rlc_ctx_pair(); + chip.decompose_rlp_field_phase1((ctx_gate, ctx_rlc), witness); + } +} + +fn rlp_string_circuit( + stage: CircuitBuilderStage, + encoded: Vec, + max_len: usize, + prank_idx: Option, + prank_component: Option, +) -> RlcCircuit> { + let input = + RlpStringTest { encoded, max_len, prank_idx, prank_component, _marker: PhantomData }; + let mut builder = RlcCircuitBuilder::from_stage(stage, 6).use_k(DEGREE as usize); + builder.base.set_lookup_bits(8); + let circuit = RlcExecutor::new(builder, input); + // auto-configure circuit if not in prover mode for convenience + if !stage.witness_gen_only() { + circuit.0.calculate_params(Some(9)); + } + circuit +} + +#[test_case(Vec::from_hex("a012341234123412341234123412341234123412341234123412341234123412340000").unwrap(), 34; "default")] +#[test_case(vec![127], 34; "short")] +#[test_case(vec![0], 32; "literal")] +#[test_case(Vec::from_hex("a09bdb004d9b1e7f3e5f86fbdc9856f21f9dcb07a44c42f5de8eec178514d279df0000000000000000000000000000000000000000000000000000000000").unwrap(), 60; "long")] +#[test_case(Vec::from_hex("b83adb004d9b1e7f3e5f86fbdc9856f21f9dcb07a44c42f5de8eec178514d279df0000000000000000000000000000000000000000000000000000000000").unwrap(), 60; "long long")] +pub fn test_mock_rlp_string(mut input_bytes: Vec, max_len: usize) { + let k = DEGREE; + input_bytes.resize(max_rlp_encoding_len(max_len), 0u8); + let circuit = + rlp_string_circuit::(CircuitBuilderStage::Mock, input_bytes, max_len, None, None); + MockProver::run(k, &circuit, vec![]).unwrap().assert_satisfied(); +} + +#[test_case(Vec::from_hex("a012341234123412341234123412341234123412341234123412341234123412340000").unwrap(), 34; "default")] +#[test_case(vec![127], 34; "short")] +#[test_case(vec![0], 32; "literal")] +#[test_case(Vec::from_hex("a09bdb004d9b1e7f3e5f86fbdc9856f21f9dcb07a44c42f5de8eec178514d279df0000000000000000000000000000000000000000000000000000000000").unwrap(), 60; "long")] +#[test_case(Vec::from_hex("b83adb004d9b1e7f3e5f86fbdc9856f21f9dcb07a44c42f5de8eec178514d279df0000000000000000000000000000000000000000000000000000000000").unwrap(), 60; "long long")] +pub fn prank_test_mock_rlp_string(mut input_bytes: Vec, max_len: usize) { + let k = DEGREE; + input_bytes.resize(max_rlp_encoding_len(max_len), 0u8); + let prank_lens = [input_bytes.len(), 3, input_bytes.len(), 1]; + for (j, prank_len) in prank_lens.into_iter().enumerate() { + for i in 0..prank_len { + let circuit = rlp_string_circuit::( + CircuitBuilderStage::Mock, + input_bytes.clone(), + max_len, + Some(i), + Some(j), + ); + let prover = MockProver::run(k, &circuit, vec![]).unwrap(); + assert!(prover.verify().is_err(), "Unconstrained at {i}, should not have verified",); + } + } +} diff --git a/axiom-eth/src/rlp/types.rs b/axiom-eth/src/rlp/types.rs new file mode 100644 index 00000000..de2756fc --- /dev/null +++ b/axiom-eth/src/rlp/types.rs @@ -0,0 +1,90 @@ +use halo2_base::{utils::ScalarField, AssignedValue}; + +use crate::rlc::types::RlcTrace; + +#[derive(Clone, Debug)] +pub struct RlpFieldPrefixParsed { + pub is_not_literal: AssignedValue, + pub is_big: AssignedValue, + + pub next_len: AssignedValue, + pub len_len: AssignedValue, +} + +#[derive(Clone, Debug)] +pub struct RlpArrayPrefixParsed { + // is_empty: AssignedValue, + pub is_big: AssignedValue, + + pub next_len: AssignedValue, + pub len_len: AssignedValue, +} + +#[derive(Clone, Debug)] +pub struct RlpPrefixParsed { + pub is_not_literal: AssignedValue, + pub is_big: AssignedValue, + + pub next_len: AssignedValue, + pub len_len: AssignedValue, +} + +/// All witnesses involved in the RLP decoding of (prefix + length + payload) +/// where payload can be either a byte string or a list. +#[derive(Clone, Debug)] +pub struct RlpFieldWitness { + // The RLP encoding is decomposed into: prefix, length, payload + pub prefix: AssignedValue, // value of the prefix + pub prefix_len: AssignedValue, + pub len_len: AssignedValue, + pub len_cells: Vec>, + + /// The byte length of the payload bytes (decoded byte string) + pub field_len: AssignedValue, + /// The payload (decoded byte string) + pub field_cells: Vec>, + pub max_field_len: usize, + + /// This is the original raw RLP encoded byte string, padded with zeros to a known fixed maximum length + pub encoded_item: Vec>, + /// This is variable length of `encoded_item` in bytes + pub encoded_item_len: AssignedValue, +} + +#[derive(Clone, Debug)] +/// The outputed values after RLP decoding of (prefix + length + payload) in SecondPhase. +/// This contains RLC of substrings from the decomposition. Payload can be either a byte string or a list. +pub struct RlpFieldTrace { + pub prefix: AssignedValue, // value of the prefix + pub prefix_len: AssignedValue, + pub len_trace: RlcTrace, + pub field_trace: RlcTrace, + // to save memory maybe we don't need this + /// This is the rlc of the full rlp (prefix + len + payload) + pub rlp_trace: RlcTrace, +} + +#[derive(Clone, Debug)] +pub struct RlpArrayWitness { + pub field_witness: Vec>, + + pub len_len: AssignedValue, + pub len_cells: Vec>, + + /// Length of the full RLP encoding (bytes) of the array + pub rlp_len: AssignedValue, + /// The original raw RLP encoded array, padded with zeros to a known fixed maximum length + pub rlp_array: Vec>, + + /// The length of the array/list this is an encoding of. Only stored if + /// this is the encoding of a variable length array. + pub list_len: Option>, +} + +#[derive(Clone, Debug)] +pub struct RlpArrayTrace { + pub len_trace: RlcTrace, + pub field_trace: Vec>, + // to save memory we don't need this + // pub array_trace: RlcTrace, +} diff --git a/axiom-eth/src/solidity/mod.rs b/axiom-eth/src/solidity/mod.rs new file mode 100644 index 00000000..6ff630c4 --- /dev/null +++ b/axiom-eth/src/solidity/mod.rs @@ -0,0 +1,295 @@ +use crate::Field; +use crate::{ + keccak::KeccakChip, + mpt::{MPTChip, MPTProof}, + rlc::{ + chip::RlcChip, + circuit::builder::RlcContextPair, + concat_array::{concat_var_fixed_array_phase0, concat_var_fixed_array_phase1}, + types::{AssignedVarLenVec, ConcatVarFixedArrayWitness}, + }, + rlp::RlpChip, + solidity::types::VarMappingTrace, + storage::{EthStorageChip, EthStorageTrace, EthStorageWitness}, +}; +use halo2_base::{ + gates::{GateChip, GateInstructions, RangeChip, RangeInstructions}, + safe_types::{SafeBytes32, SafeTypeChip, VarLenBytesVec}, + AssignedValue, Context, + QuantumCell::Constant, +}; +use itertools::Itertools; + +use self::types::{ + MappingTrace, MappingWitness, NestedMappingWitness, SolidityType, VarMappingWitness, +}; + +#[cfg(all(test, feature = "providers"))] +pub mod tests; +pub mod types; + +/// Trait which implements functions to prove statements about Solidity operations over Ethereum storage. +/// +/// Supports constraining: +/// * Mappings for keys of fixed length (Value) solidity types: +/// * `keccak256(left_pad_32(key) . mapping_slot)` +/// * Mappings for keys of variable length (NonValue) solidity types: +/// * `keccak256(key . mapping_slot)` +/// * Nested Mappings +/// * Spec for double nested mapping: +/// * Fixed Length: +/// * `keccak256(left_pad_32(key_2) . keccak256(left_pad_32(key_1) . mapping_slot))` +/// * Variable Length: +/// * `keccak256(key_2 . keccak256(key_1 . mapping_slot))` +/// * Proving the inclusion of a value at the storage slot of the constrained mapping or nested mapping. +pub struct SolidityChip<'chip, F: Field> { + pub mpt: &'chip MPTChip<'chip, F>, + pub max_nesting: usize, + pub max_key_byte_len: usize, // currently not used +} + +impl<'chip, F: Field> SolidityChip<'chip, F> { + pub fn new(mpt: &'chip MPTChip<'chip, F>, max_nesting: usize, max_key_byte_len: usize) -> Self { + Self { mpt, max_nesting, max_key_byte_len } + } + + pub fn gate(&self) -> &GateChip { + self.mpt.gate() + } + + pub fn range(&self) -> &RangeChip { + self.mpt.range() + } + + pub fn rlc(&self) -> &RlcChip { + self.mpt.rlc() + } + + pub fn rlp(&self) -> RlpChip { + self.mpt.rlp() + } + + pub fn keccak(&self) -> &KeccakChip { + self.mpt.keccak() + } + + /// Computes the storage slot of a solidity mapping at a fixed length (Value type) key by: + /// ```ignore + /// keccak256(left_pad_32(key) . mapping_slot) + /// ``` + /// + /// Inputs: + /// - `mapping_slot`: [SafeBytes32] representing the slot of the Solidity mapping itself. + /// - `key`: [SafeBytes32] representing the key of the Solidity mapping. + /// + /// Returns: + /// * The storage slot of the mapping key in question + pub fn slot_for_mapping_value_key( + &self, + ctx: &mut Context, + mapping_slot: &SafeBytes32, + key: &SafeBytes32, + ) -> SafeBytes32 { + let key_slot_concat = [key.value(), mapping_slot.value()].concat(); + debug_assert_eq!(key_slot_concat.len(), 64); + + let hash_query = self.keccak().keccak_fixed_len(ctx, key_slot_concat); + hash_query.output_bytes + } + + /// Performs witness generation within phase0 to compute the storage slot of a Solidity mapping at a variable length (non-value type) key. + /// The storage slot corresponding to `key` is computed according to https://docs.soliditylang.org/en/v0.8.19/internals/layout_in_storage.html#mappings-and-dynamic-arrays + /// ```ignore + /// keccak256(key . mapping_slot) + /// ``` + /// This is a variable length concatenation, so we need to use RLC in two phases. + /// + /// Inputs: + /// - `mapping_slot`: [SafeBytes32] representing the evm storage slot of the mapping. + /// - `key`: [VarLenBytesVec] representing the key of the mapping. + /// + /// Returns: + /// - [VarMappingWitness] representing the witness of the computation of the Solidity mapping. + pub fn slot_for_mapping_nonvalue_key_phase0( + &self, + ctx: &mut Context, + mapping_slot: SafeBytes32, + key: VarLenBytesVec, + ) -> VarMappingWitness { + let key_values = key.bytes().iter().map(|b| *b.as_ref()).collect(); + let prefix = AssignedVarLenVec { values: key_values, len: *key.len() }; + let suffix = mapping_slot.value().to_vec(); + let concat_witness = concat_var_fixed_array_phase0(ctx, self.gate(), prefix, suffix); + let concat_values = concat_witness.concat.values; + let concat_len = concat_witness.concat.len; + + let hash_query = self.keccak().keccak_var_len(ctx, concat_values, concat_len, 32); + VarMappingWitness { mapping_slot, key, hash_query } + } + + /// Performs rlc concatenation within phase1 to constrain the computation of a Solidity `mapping(key => value)`. + /// + /// * `ctx`: Circuit [Context] to assign witnesses to. + /// * `witness`: [VarMappingWitness] representing the witness of the computation of the Solidity mapping. + /// + /// Returns: + /// * [VarMappingTrace] trace of the rlc computation to constrain the computation of the Solidity mapping. + pub fn slot_for_mapping_nonvalue_key_phase1( + &self, + (ctx_gate, ctx_rlc): RlcContextPair, + witness: VarMappingWitness, + ) -> VarMappingTrace { + let VarMappingWitness { mapping_slot, key, hash_query } = witness; + // constrain the concatenation + let key_values = key.bytes().iter().map(|b| *b.as_ref()).collect(); + let prefix = AssignedVarLenVec { values: key_values, len: *key.len() }; + let suffix = mapping_slot.value().to_vec(); + let concat = + AssignedVarLenVec { values: hash_query.input_assigned.clone(), len: hash_query.length }; + let witness = ConcatVarFixedArrayWitness { prefix, suffix, concat }; + + concat_var_fixed_array_phase1((ctx_gate, ctx_rlc), self.gate(), self.rlc(), witness) + } + + /// Performs witness generation within phase0 to compute the storage slot of a Solidity mapping at a key of either value or non-value type. + /// The storage slot corresponding to `key` is computed according to https://docs.soliditylang.org/en/v0.8.19/internals/layout_in_storage.html#mappings-and-dynamic-arrays + /// + /// # Assumptions + /// - The type of the key is known at compile time + pub fn slot_for_mapping_key_phase0( + &self, + ctx: &mut Context, + mapping_slot: SafeBytes32, + key: SolidityType, + ) -> MappingWitness { + match key { + SolidityType::Value(key) => { + MappingWitness::Value(self.slot_for_mapping_value_key(ctx, &mapping_slot, &key)) + } + SolidityType::NonValue(key) => MappingWitness::NonValue( + self.slot_for_mapping_nonvalue_key_phase0(ctx, mapping_slot, key), + ), + } + } + + /// Performs rlc concatenation within phase1 to constrain the computation of a Solidity `mapping(key => value)`. + /// Does nothing if mapping key was of Value type. + /// + /// * `ctx`: Circuit [Context] to assign witnesses to. + /// * `witness`: [MappingWitness] representing the witness of the computation of the Solidity mapping. + /// + /// Returns: + /// * [MappingTrace] trace of the rlc computation to constrain the computation of the Solidity mapping. + pub fn slot_for_mapping_key_phase1( + &self, + (ctx_gate, ctx_rlc): RlcContextPair, + witness: MappingWitness, + ) -> MappingTrace { + match witness { + MappingWitness::Value(_) => None, + MappingWitness::NonValue(witness) => { + Some(self.slot_for_mapping_nonvalue_key_phase1((ctx_gate, ctx_rlc), witness)) + } + } + } + + /// Performs witness generation within phase0 to constrain the computation of a nested Solidity mapping. + /// Supports variable number of nestings, up to `MAX_NESTING`. + /// + /// Inputs: + /// - `keccak`: [KeccakChip] to constrain the computation of the Solidity mapping. + /// - `mappings`: (Vec<[SolidityType]>, [SafeType]) representing the successive keys of the nested mapping in order paired with the mapping slot. + /// - `nestings`: Specifies the amount of nesting (as this function supports variable nesting). + /// + /// Returns: + /// - [NestedMappingWitness] representing the witnesses to constrain the computation of the nested mapping, the desired mapping_slot, and the number of nestings. + /// - Will return `bytes32(0)` if `nestings` is 0. + /// + /// # Assumptions + /// - The type (Value vs Non-Value) of each key is known at compile time. + /// - `keys` is padded to `MAX_NESTING` length + pub fn slot_for_nested_mapping_phase0( + &self, + ctx: &mut Context, + mapping_slot: SafeBytes32, + keys: [SolidityType; MAX_NESTING], + nestings: AssignedValue, + ) -> NestedMappingWitness { + self.range().check_less_than_safe(ctx, nestings, MAX_NESTING as u64 + 1); + let mut witness = Vec::with_capacity(MAX_NESTING); + // slots[i] is the storage slot corresponding to keys[i] + let mut slots = Vec::with_capacity(MAX_NESTING); + for key in keys { + let m_slot = slots.last().unwrap_or(&mapping_slot).clone(); + let w = self.slot_for_mapping_key_phase0(ctx, m_slot.clone(), key); + slots.push(w.slot()); + witness.push(w); + } + let slot = if MAX_NESTING == 1 { + // if only 1 nesting, we ignore `nestings` and just return the slot assuming `nestings` is 1 + slots.pop().unwrap() + } else { + let nestings_minus_one = self.gate().sub(ctx, nestings, Constant(F::ONE)); + let indicator = self.gate().idx_to_indicator(ctx, nestings_minus_one, MAX_NESTING); + let slot = self.gate().select_array_by_indicator(ctx, &slots, &indicator); + SafeTypeChip::unsafe_to_safe_type(slot) + }; + NestedMappingWitness { witness, slot, nestings } + } + + /// Performs rlc concatenation within phase1 to constrain the computation of a Solidity nested mapping. + /// + /// * `ctx`: Circuit [Context] to assign witnesses to. + /// * `witnesses`: Vec<[MappingWitness]> representing the witnesses of the computation of the Solidity mapping that will be constrained. + /// + /// Returns: + /// * `Vec<[MappingTrace]>` containing the traces of the rlc computation to constrain the computation of the Solidity mapping. + pub fn slot_for_nested_mapping_phase1( + &self, + (ctx_gate, ctx_rlc): RlcContextPair, + witness: NestedMappingWitness, + ) -> Vec> { + witness + .witness + .into_iter() + .map(|w| self.slot_for_mapping_key_phase1((ctx_gate, ctx_rlc), w)) + .collect_vec() + } + + /// Combines `EthStorageChip::parse_storage_proof_phase0` with `slot_for_nested_mapping_phase0` to compute the storage slot of a nested Solidity mapping and a storage proof of its inclusion within the storage trie. + /// + /// Inputs: + /// - `proof` should be the MPT for the storage slot of the nested mapping, which must match the output of `slot_for_nested_mapping_phase0`. + pub fn verify_mapping_storage_phase0( + &self, + ctx: &mut Context, + mapping_slot: SafeBytes32, + keys: [SolidityType; MAX_NESTING], + nestings: AssignedValue, + proof: MPTProof, + ) -> (NestedMappingWitness, EthStorageWitness) { + let mapping_witness = + self.slot_for_nested_mapping_phase0(ctx, mapping_slot, keys, nestings); + let storage_witness = { + let storage_chip = EthStorageChip::new(self.mpt, None); + storage_chip.parse_storage_proof_phase0(ctx, mapping_witness.slot.clone(), proof) + }; + (mapping_witness, storage_witness) + } + + /// Combines `EthStorageChip::parse_storage_proof_phase1` with `slot_for_nested_mapping_phase1` to compute the slot of a nested Solidity mapping and a storage proof of its inclusion within the storage trie. + pub fn verify_mapping_storage_phase1( + &self, + (ctx_gate, ctx_rlc): RlcContextPair, + mapping_witness: NestedMappingWitness, + storage_witness: EthStorageWitness, + ) -> (Vec>, EthStorageTrace) { + let mapping_trace = + self.slot_for_nested_mapping_phase1((ctx_gate, ctx_rlc), mapping_witness); + let storage_trace = { + let storage_chip = EthStorageChip::new(self.mpt, None); + storage_chip.parse_storage_proof_phase1((ctx_gate, ctx_rlc), storage_witness) + }; + (mapping_trace, storage_trace) + } +} diff --git a/axiom-eth/src/solidity/tests/mapping.rs b/axiom-eth/src/solidity/tests/mapping.rs new file mode 100644 index 00000000..bec5ae85 --- /dev/null +++ b/axiom-eth/src/solidity/tests/mapping.rs @@ -0,0 +1,138 @@ +use test_case::test_case; +use test_log::test; + +use crate::{ + rlc::{circuit::RlcCircuitParams, tests::get_rlc_params}, + solidity::types::MappingWitness, +}; + +use super::*; + +//======== Mock Prover Tests ========= +#[test_case(ANVIL_BALANCE_OF_PATH; "anvil_balance_of")] +#[test_case(WETH_BALANCE_OF_ADDRESS_PATH; "weth_balance_of_address")] +#[test_case(WETH_BALANCE_OF_BYTES32_PATH; "weth_balance_of_bytes32")] +pub fn test_mock_mapping_pos_from_json(json_path: &str) { + let data = mapping_test_input(Path::new(json_path)); + let (circuit, instance) = mapping_circuit(CircuitBuilderStage::Mock, data, None, None); + let k = circuit.params().k() as u32; + MockProver::run(k, &circuit, vec![instance]).unwrap().assert_satisfied() +} + +#[test_case((20, None); "address")] +#[test_case((1, None); "uint8")] +#[test_case((2, None); "uint16")] +#[test_case((4, None); "uint32")] +#[test_case((8, None); "uint64")] +#[test_case((16, None); "uint128")] +#[test_case((32, None); "uint256")] +#[test_case((0, Some(1)); "dynamic_var_len_0")] +#[test_case((1, Some(1)); "dynamic_var_len_1")] +pub fn test_mock_mapping_pos_random(data: MappingTestData) { + let data = rand_mapping_data(data); + let (circuit, instance) = mapping_circuit(CircuitBuilderStage::Mock, data, None, None); + let k = circuit.params().k() as u32; + MockProver::run(k, &circuit, vec![instance]).unwrap().assert_satisfied() +} + +// Should panic from assert +#[test] +#[should_panic] +pub fn neg_mock_dynamic_var_len_equal_max_len() { + let data = rand_mapping_data((2, Some(1))); + let (circuit, instance) = mapping_circuit(CircuitBuilderStage::Mock, data, None, None); + let k = circuit.params().k() as u32; + assert!(MockProver::run(k, &circuit, vec![instance]).unwrap().verify().is_ok()); +} + +//======== Prover Tests ========= +impl EthCircuitInstructions for MappingTest { + type FirstPhasePayload = (MappingWitness, usize); + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + ) -> Self::FirstPhasePayload { + let max_key_byte_len = self.max_var_len.unwrap_or(32); + let chip = SolidityChip::new(mpt, MAX_NESTING, max_key_byte_len); + let safe = SafeTypeChip::new(mpt.range()); + let ctx = builder.base.main(FIRST_PHASE); + let input = self.clone().assign(ctx, &safe); + let witness = chip.slot_for_mapping_key_phase0(ctx, input.mapping_slot, input.key); + + let assigned_instances = witness.slot().as_ref().to_vec(); + builder.base.assigned_instances[0] = assigned_instances; + (witness, max_key_byte_len) + } + fn virtual_assign_phase1( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + (witness, max_key_byte_len): Self::FirstPhasePayload, + ) { + let chip = SolidityChip::new(mpt, MAX_NESTING, max_key_byte_len); + let (ctx_gate, ctx_rlc) = builder.rlc_ctx_pair(); + chip.slot_for_mapping_key_phase1((ctx_gate, ctx_rlc), witness); + } +} + +pub fn mapping_circuit( + stage: CircuitBuilderStage, + test_data: MappingTest, + params: Option, + break_points: Option, +) -> (EthCircuitImpl>, Vec) { + let instance_wo_commit = + test_data.ground_truth_slot.iter().map(|x| Fr::from(*x as u64)).collect_vec(); + let mut params = if let Some(params) = params { + params + } else { + get_rlc_params("configs/tests/storage_mapping.json") + }; + params.base.num_instance_columns = 1; + let mut circuit = create_circuit(stage, params, test_data); + circuit.mock_fulfill_keccak_promises(None); + if !stage.witness_gen_only() { + circuit.calculate_params(); + } + if let Some(bp) = break_points { + circuit.set_break_points(bp); + } + let instances = circuit.instances(); + let mut instance = instances[0].clone(); + instance.pop().unwrap(); + assert_eq!(instance_wo_commit, instance); + + (circuit, instances[0].clone()) +} + +#[test_case((1, None), (1, None), true; "pos_prover_uint8")] +#[test_case((32, None), (32, None), true; "pos_prover_bytes32")] +#[test_case((1, Some(32)), (1, Some(32)), true; "pos_prover_dynamic_keys_same_var_len_and_max_len")] +#[test_case((1, Some(32)), (3, Some(32)), true; "pos_prover_dynamic_keys_diff_var_len_same_max_len")] +#[test_case((1, None), (2, None), false; "neg_prover_uint8_uint16")] +#[test_case((1, Some(32)), (1, Some(33)), false; "neg_dynamic_keys_diff_max_len")] +pub fn mapping_prover_satisfied( + keygen_data: MappingTestData, + proof_data: MappingTestData, + expected: bool, +) { + let (circuit, _) = + mapping_circuit(CircuitBuilderStage::Keygen, rand_mapping_data(keygen_data), None, None); + let bench_params = circuit.params().rlc; + let k = bench_params.base.k as u32; + let params = gen_srs(k); + let vk = keygen_vk(¶ms, &circuit).unwrap(); + let pk = keygen_pk(¶ms, vk, &circuit).unwrap(); + + let break_points = circuit.break_points(); + + let (circuit, instance) = mapping_circuit( + CircuitBuilderStage::Prover, + rand_mapping_data(proof_data), + Some(bench_params), + Some(break_points), + ); + let proof = gen_proof_with_instances(¶ms, &pk, circuit, &[&instance]); + check_proof_with_instances(¶ms, pk.get_vk(), &proof, &[&instance], expected); +} diff --git a/axiom-eth/src/solidity/tests/mapping_storage.rs b/axiom-eth/src/solidity/tests/mapping_storage.rs new file mode 100644 index 00000000..49632a68 --- /dev/null +++ b/axiom-eth/src/solidity/tests/mapping_storage.rs @@ -0,0 +1,129 @@ +use std::str::FromStr; + +use test_case::test_case; + +use crate::rlc::{circuit::RlcCircuitParams, tests::get_rlc_params}; + +use super::*; +const MAX_MAPPING_KEY_BYTE_LEN: usize = 32; + +//======== Sourced Data Tests ========= +#[test_case(vec![CRYPTOPUNKS_BALANCE_OF_PATH], CRYPTOPUNKS_MAINNET_ADDR; "cryptopunks_balance_of")] +#[test_case(vec![UNISOCKS_ERC20_BALANCE_OF_PATH], UNISOCKS_ERC20_MAINNET_ADDR; "unisocks_erc20_balance_of")] +#[test_case(vec![UNISOCKS_ERC721_BALANCE_OF_PATH], UNISOCKS_ERC721_MAINNET_ADDR; "unisocks_erc721_balance_of")] +#[test_case(vec![WETH_BALANCE_OF_ADDRESS_PATH], WETH_MAINNET_ADDR; "weth_balance_of")] +#[test_case(vec![WETH_ALLOWANCE_ADDR_ADDR_PATH, WETH_ALLOWANCE_ADDR_UINT_PATH], WETH_MAINNET_ADDR; "weth_allowance_double_mapping")] +#[test_case(vec![UNI_V3_ADDR_ADDR_PATH, UNI_V3_ADDR_UINT_PATH, UNI_V3_UINT_ADDR_PATH], UNI_V3_FACTORY_MAINNET_ADDR; "uni_v3_triple_mapping")] +pub fn test_mock_mapping_storage_pos(paths: Vec<&str>, addr: &str) { + let mapping_data = + paths.into_iter().map(|path| mapping_test_input(Path::new(path))).collect_vec(); + let slot = vec![H256::from_slice(&mapping_data.last().unwrap().ground_truth_slot)]; + let storage_pf = get_block_storage_input( + &setup_provider(Chain::Mainnet), + TEST_BLOCK_NUM, + H160::from_str(addr).unwrap(), + slot, + 8, + 8, + ) + .storage; + let circuit = + mapping_storage_circuit(CircuitBuilderStage::Mock, mapping_data, storage_pf, None, None); + let k = circuit.params().k() as u32; + let instances = circuit.instances(); + MockProver::run(k, &circuit, instances).unwrap().assert_satisfied(); +} + +#[test_case(vec![WETH_BALANCE_OF_ADDRESS_PATH], WETH_MAINNET_ADDR, H256::zero())] +#[test_case(vec![WETH_ALLOWANCE_ADDR_ADDR_PATH, WETH_ALLOWANCE_ADDR_UINT_PATH], WETH_MAINNET_ADDR, H256::zero())] +#[test_case(vec![UNI_V3_ADDR_ADDR_PATH, UNI_V3_ADDR_UINT_PATH, UNI_V3_UINT_ADDR_PATH], UNI_V3_FACTORY_MAINNET_ADDR, H256::zero())] +pub fn test_mock_mapping_storage_neg(paths: Vec<&str>, addr: &str, invalid_slot: H256) { + let mapping_data = + paths.into_iter().map(|path| mapping_test_input(Path::new(path))).collect_vec(); + let invalid_storage_pf = get_block_storage_input( + &setup_provider(Chain::Mainnet), + TEST_BLOCK_NUM, + H160::from_str(addr).unwrap(), + vec![invalid_slot], + 8, + 8, + ) + .storage; + let circuit = mapping_storage_circuit( + CircuitBuilderStage::Mock, + mapping_data, + invalid_storage_pf, + None, + None, + ); + let k = circuit.params().k() as u32; + let instances = circuit.instances(); + assert!(MockProver::run(k, &circuit, instances).unwrap().verify().is_err()); +} + +#[derive(Clone)] +struct MappingStorageTest { + test_data: Vec>, + proof: EthStorageInput, +} + +impl EthCircuitInstructions for MappingStorageTest { + type FirstPhasePayload = (NestedMappingWitness, EthStorageWitness); + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + ) -> Self::FirstPhasePayload { + let chip = SolidityChip::new(mpt, MAX_NESTING, MAX_MAPPING_KEY_BYTE_LEN); + let safe = SafeTypeChip::new(mpt.range()); + let ctx = builder.base.main(FIRST_PHASE); + let inputs = self.test_data.iter().map(|data| data.assign(ctx, &safe)).collect_vec(); + let mapping_slot = inputs[0].mapping_slot.clone(); + let proof = self.proof.storage_pfs[0].2.clone().assign(ctx); + let mut keys = inputs.iter().map(|input| input.key.clone()).collect_vec(); + let nestings = ctx.load_witness(F::from(inputs.len() as u64)); + keys.resize(MAX_NESTING, keys[0].clone()); + chip.verify_mapping_storage_phase0::<{ MAX_NESTING }>( + ctx, + mapping_slot, + keys.try_into().unwrap(), + nestings, + proof, + ) + } + fn virtual_assign_phase1( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + (nested_witness, storage_witness): Self::FirstPhasePayload, + ) { + let chip = SolidityChip::new(mpt, MAX_NESTING, MAX_MAPPING_KEY_BYTE_LEN); + let (ctx_gate, ctx_rlc) = builder.rlc_ctx_pair(); + chip.verify_mapping_storage_phase1((ctx_gate, ctx_rlc), nested_witness, storage_witness); + } +} + +fn mapping_storage_circuit( + stage: CircuitBuilderStage, + test_data: Vec>, + proof: EthStorageInput, + params: Option, + break_points: Option, +) -> EthCircuitImpl> { + let params = if let Some(params) = params { + params + } else { + get_rlc_params("configs/tests/storage_mapping.json") + }; + let input = MappingStorageTest { test_data, proof }; + + let mut circuit = create_circuit(stage, params, input); + circuit.mock_fulfill_keccak_promises(None); + circuit.calculate_params(); + if let Some(bp) = break_points { + circuit.set_break_points(bp); + } + circuit +} + +// Skipping prover tests because it is combination of mapping slot computation and storage proof diff --git a/axiom-eth/src/solidity/tests/mod.rs b/axiom-eth/src/solidity/tests/mod.rs new file mode 100644 index 00000000..e88b4568 --- /dev/null +++ b/axiom-eth/src/solidity/tests/mod.rs @@ -0,0 +1,36 @@ +use ethers_core::types::{H160, H256}; + +use ethers_core::types::Chain; +use halo2_base::{ + gates::circuit::CircuitBuilderStage, + halo2_proofs::dev::MockProver, + halo2_proofs::{ + halo2curves::bn256::Fr, + plonk::{keygen_pk, keygen_vk, Circuit}, + }, + safe_types::SafeTypeChip, + utils::{ + fs::gen_srs, + testing::{check_proof_with_instances, gen_proof_with_instances}, + }, +}; +use itertools::Itertools; +use std::{panic::catch_unwind, path::Path, vec}; + +use crate::{ + mpt::MPTChip, + providers::{setup_provider, storage::get_block_storage_input}, + rlc::{circuit::builder::RlcCircuitBuilder, virtual_region::RlcThreadBreakPoints, FIRST_PHASE}, + solidity::{tests::utils::*, types::NestedMappingWitness, SolidityChip}, + storage::{circuit::EthStorageInput, EthStorageWitness}, + utils::eth_circuit::{create_circuit, EthCircuitImpl, EthCircuitInstructions}, + Field, +}; + +pub mod mapping; +pub mod mapping_storage; +pub mod nested_mappings; +pub mod prop_pos; +pub mod utils; + +const MAX_NESTING: usize = 3; diff --git a/axiom-eth/src/solidity/tests/nested_mappings.rs b/axiom-eth/src/solidity/tests/nested_mappings.rs new file mode 100644 index 00000000..ab436262 --- /dev/null +++ b/axiom-eth/src/solidity/tests/nested_mappings.rs @@ -0,0 +1,172 @@ +use test_case::test_case; + +use crate::rlc::{circuit::RlcCircuitParams, tests::get_rlc_params}; + +use super::*; + +//======== Sourced Data Tests ========= +#[test] +pub fn test_mock_weth_allowance() { + let data = vec![ + mapping_test_input(Path::new(WETH_ALLOWANCE_ADDR_ADDR_PATH)), + mapping_test_input(Path::new(WETH_ALLOWANCE_ADDR_UINT_PATH)), + ]; + let (circuit, instance) = nested_mapping_circuit(CircuitBuilderStage::Mock, data, None, None); + let k = circuit.params().k() as u32; + MockProver::run(k, &circuit, vec![instance]).unwrap().assert_satisfied() +} + +#[test] +pub fn test_mock_uni_v3_factory_get_pool() { + let data = vec![ + mapping_test_input(Path::new(UNI_V3_ADDR_ADDR_PATH)), + mapping_test_input(Path::new(UNI_V3_ADDR_UINT_PATH)), + mapping_test_input(Path::new(UNI_V3_UINT_ADDR_PATH)), + ]; + let (circuit, instance) = nested_mapping_circuit(CircuitBuilderStage::Mock, data, None, None); + let k = circuit.params().k() as u32; + MockProver::run(k, &circuit, vec![instance]).unwrap().assert_satisfied() +} + +//======== Mock Prover Tests ========= +#[test_case(vec![(32, None), (20, None)]; "double_mapping_test_bytes32_addr")] +#[test_case(vec![(1, None), (20, None), (32, None)]; "triple_mapping_test_uint8_addr_bytes32")] +#[test_case(vec![(1, Some(2)), (1, None), (20, None)]; "triple_mapping_test_dynamic_uint8_addr")] +#[test_case(vec![(1, None), (1, Some(32)), (20, None)]; "triple_mapping_test_uint8_dynamic_addr")] +#[test_case(vec![(1, None), (20, None), (1, Some(32))]; "triple_mapping_test_uint8_addr_dynamic")] +pub fn test_mock_nested_mapping_pos(data: Vec) { + let data = rand_nested_mapping_data(data); + let (circuit, instance) = nested_mapping_circuit(CircuitBuilderStage::Mock, data, None, None); + let k = circuit.params().k() as u32; + MockProver::run(k, &circuit, vec![instance]).unwrap().assert_satisfied() +} + +//======== Prover Tests ========= +#[derive(Clone)] +struct NestedMappingTest(Vec>); + +impl EthCircuitInstructions for NestedMappingTest { + type FirstPhasePayload = (NestedMappingWitness, usize); + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + ) -> Self::FirstPhasePayload { + let max_key_byte_len = self.0[0].max_var_len.unwrap_or(32); + let chip = SolidityChip::new(mpt, MAX_NESTING, max_key_byte_len); + let safe = SafeTypeChip::new(mpt.range()); + let ctx = builder.base.main(FIRST_PHASE); + let inputs = self.0.iter().map(|data| data.assign(ctx, &safe)).collect_vec(); + let mapping_slot = inputs[0].mapping_slot.clone(); + let mut keys = inputs.iter().map(|input| input.key.clone()).collect_vec(); + let nestings = ctx.load_witness(F::from(keys.len() as u64)); + keys.resize(MAX_NESTING, keys[0].clone()); + let witness = chip.slot_for_nested_mapping_phase0::<{ MAX_NESTING }>( + ctx, + mapping_slot, + keys.try_into().unwrap(), + nestings, + ); + builder.base.assigned_instances[0] = witness.slot.as_ref().to_vec(); + (witness, max_key_byte_len) + } + fn virtual_assign_phase1( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + (witness, max_key_byte_len): Self::FirstPhasePayload, + ) { + let chip = SolidityChip::new(mpt, MAX_NESTING, max_key_byte_len); + chip.slot_for_nested_mapping_phase1(builder.rlc_ctx_pair(), witness); + } +} + +// re-use MappingTest struct for keys, but only use first mapping slot +fn nested_mapping_circuit( + stage: CircuitBuilderStage, + test_data: Vec>, + params: Option, + break_points: Option, +) -> (EthCircuitImpl>, Vec) { + let mut params = if let Some(params) = params { + params + } else { + get_rlc_params("configs/tests/storage_mapping.json") + }; + let instance_wo_commit = test_data + .last() + .unwrap() + .ground_truth_slot + .iter() + .map(|x| Fr::from(*x as u64)) + .collect_vec(); + params.base.num_instance_columns = 1; + let mut circuit = create_circuit(stage, params, NestedMappingTest(test_data)); + circuit.mock_fulfill_keccak_promises(None); + if !stage.witness_gen_only() { + circuit.calculate_params(); + } + if let Some(bp) = break_points { + circuit.set_break_points(bp); + } + let instances = circuit.instances(); + let mut instance = instances[0].clone(); + instance.pop().unwrap(); + assert_eq!(instance_wo_commit, instance); + + (circuit, instances[0].clone()) +} + +#[test_case(vec![(20, None), (1, None), (32, None)], vec![(20, None), (1, None), (32, None)], true; +"pos_prover_triple_mapping_addr_uint8_bytes32")] +#[test_case(vec![(1, Some(2)), (20, None), (32, None)], vec![(1, Some(2)), (20, None), (32, None)], true; +"pos_prover_triple_mapping_dynamic_addr_bytes32")] +#[test_case(vec![(20, None), (1, Some(2)), (32, None)], vec![(20, None), (1, Some(2)), (32, None)], true; +"pos_prover_triple_mapping_addr_dynamic_bytes32")] +#[test_case(vec![(20, None), (32, None), (1, Some(2))], vec![(20, None), (32, None), (1, Some(2))], true; +"pos_prover_triple_mapping_addr_bytes32_dynamic")] +#[test_case(vec![(20, None), (1, None), (32, None)], vec![(20, None), (32, None), (1, None)], false; +"neg_prover_triple_mapping_addr_uint8_bytes32_to_address_bytes32_uint8")] +// #[test_case(vec![(20, None), (1, None), (32, None)], vec![(20, Some(21)), (1, None), (32, None)], false; +// "neg_prover_triple_mapping_addr_uint8_bytes32_to_dynamic_uint8_bytes32")] // catch_unwind not catching this, should panic +// #[test_case(vec![(20, None), (1, None), (32, None)], vec![(20, None), (1, Some(2)), (32, None)], false; +// "neg_prover_triple_mapping_addr_uint8_bytes32_to_addr_dynamic_uint8")] // catch_unwind not catching, should panic +// #[test_case(vec![(20, None), (1, None), (32, None)], vec![(20, None), (1, None), (32, Some(33))], false; +// "neg_prover_triple_mapping_addr_uint8_bytes32_to_addr_uint8_dynamic")] // catch_unwind not catching this, should panic +pub fn nested_mapping_prover_satisfied( + keygen_input: Vec, + proof_input: Vec, + expected: bool, +) { + let (circuit, _) = nested_mapping_circuit( + CircuitBuilderStage::Keygen, + rand_nested_mapping_data(keygen_input), + None, + None, + ); + let bench_params = circuit.params().rlc; + let k = bench_params.base.k as u32; + + let params = gen_srs(k); + let vk = keygen_vk(¶ms, &circuit).unwrap(); + let pk = keygen_pk(¶ms, vk, &circuit).unwrap(); + + let break_points = circuit.break_points(); + + let pf_instance = catch_unwind(|| { + let (circuit, instance) = nested_mapping_circuit( + CircuitBuilderStage::Prover, + rand_nested_mapping_data(proof_input), + Some(bench_params), + Some(break_points), + ); + let proof = gen_proof_with_instances(¶ms, &pk, circuit, &[&instance]); + (proof, instance) + }); + if let Ok((proof, instance)) = pf_instance { + check_proof_with_instances(¶ms, pk.get_vk(), &proof, &[&instance], expected); + } else { + // On some bad inputs we have assert fails during witness generation + assert!(!expected, "Runtime error in proof generation"); + } +} diff --git a/axiom-eth/src/solidity/tests/prop_pos.rs b/axiom-eth/src/solidity/tests/prop_pos.rs new file mode 100644 index 00000000..b46d1f98 --- /dev/null +++ b/axiom-eth/src/solidity/tests/prop_pos.rs @@ -0,0 +1,122 @@ +use crate::{ + solidity::tests::{mapping::*, utils::MappingTest}, + utils::eth_circuit::EthCircuitImpl, +}; +use ethers_core::{types::H256, utils::keccak256}; +use halo2_base::{ + gates::circuit::CircuitBuilderStage, + halo2_proofs::{ + dev::{MockProver, VerifyFailure}, + halo2curves::bn256::Fr, + plonk::Circuit, + }, +}; +use proptest::{collection::vec, prelude::*, sample::select}; +use std::marker::PhantomData; + +prop_compose! { + pub fn rand_hex_string(max: usize)(val in vec(any::(), max)) -> Vec { val } +} + +prop_compose! { + // We assume key is of integer type so we left pad with 0s. Note bytes20 would need to be right padded with 0s + pub fn rand_mapping_test(max: usize) + (key in rand_hex_string(max), mapping_slot in rand_hex_string(32)) -> MappingTest { + let mapping_slot = [vec![0; 32 - mapping_slot.len()], mapping_slot.clone()].concat(); + let mslot = H256::from_slice(&mapping_slot.clone()); + assert!(key.len() <= 32); + let padded_key = [vec![0; 32 - key.len()], key.clone()].concat(); + let ground_truth_concat_key = [padded_key.as_slice(), mslot.as_bytes()].concat(); + debug_assert_eq!(ground_truth_concat_key.len(), padded_key.len() + 32); + let ground_truth_slot = keccak256(ground_truth_concat_key.clone()).to_vec(); + + MappingTest { + key, + var_len: None, + max_var_len: None, + mapping_slot, + ground_truth_concat_key, + ground_truth_slot, + _marker: PhantomData + } + } +} + +prop_compose! { + pub fn rand_var_len_mapping_test(max_len: usize) + (var_len in 1..=max_len) + (mut key in rand_hex_string(var_len), mapping_slot in rand_hex_string(32), var_len in Just(var_len), max_len in Just(max_len)) -> MappingTest { + assert_eq!(key.len(), var_len); + let slot = [vec![0; 32 - mapping_slot.len()], mapping_slot.clone()].concat(); + let m_slot = H256::from_slice(&slot); + let mut ground_truth_concat_key = [key.as_slice(), m_slot.as_bytes()].as_slice().concat(); + let ground_truth_slot = keccak256(ground_truth_concat_key.clone()).to_vec(); + ground_truth_concat_key.resize(max_len + 32, 0); + key.resize(max_len, 0); + MappingTest { + key, + var_len: Some(var_len), + max_var_len: Some(max_len), + mapping_slot, + ground_truth_concat_key, + ground_truth_slot, + _marker: PhantomData + } + } +} + +prop_compose! { + pub fn rand_byte_sized_mapping_test() + (size in select(vec![1,2,4,8,16,32])) + (key in rand_hex_string(size), mapping_slot in rand_hex_string(32)) -> MappingTest { + let slot = [vec![0; 32 - mapping_slot.len()], mapping_slot.clone()].concat(); + let mslot = H256::from_slice(&slot.clone()); + let padded_key = [vec![0; 32 - key.len()], key.clone()].concat(); + let ground_truth_concat_key = [padded_key.as_slice(), mslot.as_bytes()].concat(); + debug_assert!(ground_truth_concat_key.len() == 64); + let ground_truth_slot = keccak256(ground_truth_concat_key.clone()).to_vec(); + MappingTest { + key, + var_len: None, + max_var_len: None, + mapping_slot, + ground_truth_concat_key, + ground_truth_slot, + _marker: PhantomData + } + } +} + +pub fn prop_mock_prover_satisfied( + (circuit, instance): (EthCircuitImpl>, Vec), +) -> Result<(), Vec> { + let k = circuit.params().k() as u32; + MockProver::run(k, &circuit, vec![instance]).unwrap().verify() +} + +proptest! { + + #[test] + #[ignore] + fn prop_test_pos_rand_bytes32_key(input in rand_mapping_test(32)) { + prop_mock_prover_satisfied(mapping_circuit(CircuitBuilderStage::Mock, input,None, None)).unwrap(); + } + + #[test] + #[ignore] + fn prop_test_pos_rand_address_key(input in rand_mapping_test(20)) { + prop_mock_prover_satisfied(mapping_circuit(CircuitBuilderStage::Mock, input,None, None)).unwrap(); + } + + #[test] + #[ignore] + fn prop_test_pos_rand_uint_key(input in rand_byte_sized_mapping_test()) { + prop_mock_prover_satisfied(mapping_circuit(CircuitBuilderStage::Mock, input, None, None)).unwrap(); + } + + #[test] + #[ignore] + fn prop_test_pos_rand_dynamic_array_key(input in rand_var_len_mapping_test(36)) { + prop_mock_prover_satisfied(mapping_circuit(CircuitBuilderStage::Mock, input, None, None)).unwrap(); + } +} diff --git a/axiom-eth/src/solidity/tests/utils.rs b/axiom-eth/src/solidity/tests/utils.rs new file mode 100644 index 00000000..2895680b --- /dev/null +++ b/axiom-eth/src/solidity/tests/utils.rs @@ -0,0 +1,237 @@ +use crate::Field; +use ethers_core::{types::H256, utils::keccak256}; +use halo2_base::{ + halo2_proofs::halo2curves::bn256::Fr, + safe_types::{SafeBytes32, SafeTypeChip}, + AssignedValue, Context, +}; +use hex::FromHex; +use itertools::Itertools; +use rand::RngCore; +use serde::{Deserialize, Serialize}; +use std::{iter, marker::PhantomData, path::Path}; + +use crate::{ + solidity::types::SolidityType, utils::eth_circuit::EthCircuitParams, + utils::unsafe_bytes_to_assigned, +}; + +pub const TEST_BLOCK_NUM: u32 = 17595887; + +//Mainnet Contract Address +pub const WETH_MAINNET_ADDR: &str = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; +pub const CRYPTOPUNKS_MAINNET_ADDR: &str = "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb"; +pub const UNI_V3_FACTORY_MAINNET_ADDR: &str = "0x1f98431c8ad98523631ae4a59f267346ea31f984"; +pub const UNISOCKS_ERC20_MAINNET_ADDR: &str = "0x23b608675a2b2fb1890d3abbd85c5775c51691d5"; +pub const UNISOCKS_ERC721_MAINNET_ADDR: &str = "0x65770b5283117639760bea3f867b69b3697a91dd"; + +//Json Paths +pub const UNI_V3_ADDR_ADDR_PATH: &str = "data/mappings/uni_v3_factory_get_pool_addr_addr.json"; +pub const UNI_V3_ADDR_UINT_PATH: &str = "data/mappings/uni_v3_factory_get_pool_addr_uint.json"; +pub const UNI_V3_UINT_ADDR_PATH: &str = "data/mappings/uni_v3_factory_get_pool_uint_addr.json"; +pub const UNI_V3_FAKE_PATH: &str = "data/mappings/uni_v3_factory_fake.json"; + +pub const UNISOCKS_ERC20_BALANCE_OF_PATH: &str = + "data/mappings/unisocks_erc20_balance_of_addr_uint.json"; +pub const UNISOCKS_ERC721_BALANCE_OF_PATH: &str = + "data/mappings/unisocks_erc721_balance_of_addr_uint.json"; +pub const CRYPTOPUNKS_BALANCE_OF_PATH: &str = "data/mappings/cryptopunks_balance_of_addr_uint.json"; + +pub const WETH_BALANCE_OF_ADDRESS_PATH: &str = "data/mappings/weth_balance_of_addr_uint.json"; +pub const WETH_BALANCE_OF_BYTES32_PATH: &str = "data/mappings/weth_balance_of_bytes32_uint.json"; + +pub const WETH_ALLOWANCE_ADDR_ADDR_PATH: &str = "data/mappings/weth_allowance_addr_addr.json"; +pub const WETH_ALLOWANCE_ADDR_UINT_PATH: &str = "data/mappings/weth_allowance_addr_uint.json"; + +pub const ANVIL_BALANCE_OF_PATH: &str = "data/mappings/anvil_dynamic_uint.json"; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct MappingTest { + pub key: Vec, + pub var_len: Option, + pub max_var_len: Option, + pub mapping_slot: Vec, + pub ground_truth_concat_key: Vec, + pub ground_truth_slot: Vec, + pub _marker: PhantomData, +} + +#[derive(Debug, Clone)] +pub struct AssignedMappingTest { + pub key: SolidityType, + pub var_len: Option, + pub max_var_len: Option, + pub mapping_slot: SafeBytes32, +} + +impl MappingTest { + pub fn assign(&self, ctx: &mut Context, safe: &SafeTypeChip) -> AssignedMappingTest { + let key_bytes = unsafe_bytes_to_assigned(ctx, &self.key); + // Determine value or nonvalue based on Option of var_len + let key = get_test_key(&self.var_len, &self.max_var_len, safe, ctx, key_bytes); + + let mapping_slot_bytes = unsafe_bytes_to_assigned(ctx, &self.mapping_slot); + let mapping_slot = safe.raw_bytes_to::<1, 256>(ctx, mapping_slot_bytes); + + AssignedMappingTest { + key, + var_len: self.var_len, + max_var_len: self.max_var_len, + mapping_slot, + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct BenchParams(pub EthCircuitParams, pub usize); // (params, num_slots) +/// (byte len, optional max byte len if non-value type) +pub type MappingTestData = (usize, Option); + +/// Inputs a mapping test from a json file +pub fn mapping_test_input(path: impl AsRef) -> MappingTest { + let mapping_test_str = std::fs::read_to_string(path).unwrap(); + let mapping_test: serde_json::Value = serde_json::from_str(mapping_test_str.as_str()).unwrap(); + + let key_bytes_str: String = serde_json::from_value(mapping_test["key"].clone()).unwrap(); + let key = Vec::from_hex(key_bytes_str).unwrap(); + + let var_len_str: String = serde_json::from_value(mapping_test["var_len"].clone()).unwrap(); + let var_len = match var_len_str.is_empty() { + true => None, + false => Some(var_len_str.parse::().unwrap()), + }; + + let max_var_len_str: String = + serde_json::from_value(mapping_test["max_var_len"].clone()).unwrap(); + let max_var_len = match max_var_len_str.is_empty() { + true => None, + false => Some(max_var_len_str.parse::().unwrap()), + }; + + let mapping_slot_str: String = + serde_json::from_value(mapping_test["mapping_slot"].clone()).unwrap(); + let mapping_slot = Vec::from_hex(mapping_slot_str).unwrap(); + + let ground_truth_concat_key_str: String = + serde_json::from_value(mapping_test["ground_truth_concat_key"].clone()).unwrap(); + let ground_truth_concat_key = Vec::from_hex(ground_truth_concat_key_str).unwrap(); + + let ground_truth_slot_str: String = + serde_json::from_value(mapping_test["ground_truth_slot"].clone()).unwrap(); + let ground_truth_slot = Vec::from_hex(ground_truth_slot_str).unwrap(); + + MappingTest { + key, + var_len, + max_var_len, + mapping_slot, + ground_truth_concat_key, + ground_truth_slot, + _marker: PhantomData, + } +} + +pub fn get_test_key( + var_len: &Option, + max_var_len: &Option, + safe: &SafeTypeChip, + ctx: &mut Context, + test_key_bytes: Vec>, +) -> SolidityType { + match var_len { + Some(var_len) => SolidityType::NonValue({ + let max_var_len = max_var_len.unwrap_or(test_key_bytes.len()); + let var_len = ctx.load_witness(F::from(*var_len as u64)); + safe.raw_to_var_len_bytes_vec(ctx, test_key_bytes, var_len, max_var_len) + }), + None => { + let test_key_bytes = iter::repeat(ctx.load_zero()) + .take(32 - test_key_bytes.len()) + .chain(test_key_bytes) + .collect_vec(); + SolidityType::Value(safe.raw_bytes_to::<1, 256>(ctx, test_key_bytes)) + } + } +} + +pub fn rand_hex_array(max: usize) -> Vec { + let mut rng = rand::thread_rng(); + let mut bytes = vec![0; max]; + rng.fill_bytes(&mut bytes); + bytes +} + +pub fn rand_mapping_data((len, max_len): (usize, Option)) -> MappingTest { + let key = rand_hex_array(len); + let mapping_slot = rand_hex_array(32); + let m_slot = H256::from_slice(&mapping_slot.clone()); + + let (key, ground_truth_slot, ground_truth_concat_key, var_len, max_var_len) = match max_len { + Some(max_len) => { + let mut ground_truth_concat_key = [key.as_slice(), m_slot.as_bytes()].concat(); + let ground_truth_slot = keccak256(&ground_truth_concat_key[..len + 32]).to_vec(); + ground_truth_concat_key.resize(max_len + 32, 0); + let key = + key.iter().chain(iter::repeat(&0)).take(max_len).cloned().collect::>(); + (key, ground_truth_slot, ground_truth_concat_key, Some(len), Some(max_len)) + } + None => { + let padded_key = [vec![0; 32 - key.len()], key.clone()].concat(); + let ground_truth_concat_key = [padded_key.as_slice(), m_slot.as_bytes()].concat(); + debug_assert!(ground_truth_concat_key.len() == 64); + let ground_truth_slot = keccak256(ground_truth_concat_key.clone()).to_vec(); + (key, ground_truth_slot, ground_truth_concat_key, None, None) + } + }; + + MappingTest { + key, + var_len, + max_var_len, + mapping_slot, + ground_truth_concat_key, + ground_truth_slot, + _marker: PhantomData, + } +} + +pub fn rand_nested_mapping_data(tests: Vec<(usize, Option)>) -> Vec> { + let mut slot = rand_hex_array(32); + + tests.into_iter().fold(Vec::new(), |mut acc, (len, max_len)| { + let key = rand_hex_array(len); + let mapping_slot = slot.to_vec(); + let m_slot = H256::from_slice(&mapping_slot.clone()); + + let (key, ground_truth_slot, ground_truth_concat_key, var_len, max_var_len) = match max_len + { + Some(max_len) => { + let ground_truth_concat_key = + [key.as_slice(), m_slot.as_bytes(), vec![0; max_len - len].as_slice()].concat(); + let key = + key.iter().chain(iter::repeat(&0)).take(max_len).cloned().collect::>(); + let ground_truth_slot = keccak256(&ground_truth_concat_key[..len + 32]).to_vec(); + (key, ground_truth_slot, ground_truth_concat_key, Some(len), Some(max_len)) + } + None => { + let padded_key = [vec![0; 32 - key.len()], key.clone()].concat(); + let ground_truth_concat_key = [padded_key.as_slice(), m_slot.as_bytes()].concat(); + debug_assert!(ground_truth_concat_key.len() == 64); + let ground_truth_slot = keccak256(ground_truth_concat_key.clone()).to_vec(); + (key, ground_truth_slot, ground_truth_concat_key, None, None) + } + }; + + acc.push(MappingTest { + key, + var_len, + max_var_len, + mapping_slot, + ground_truth_concat_key, + ground_truth_slot: ground_truth_slot.clone(), + _marker: PhantomData, + }); + slot = ground_truth_slot; + acc + }) +} diff --git a/axiom-eth/src/solidity/types.rs b/axiom-eth/src/solidity/types.rs new file mode 100644 index 00000000..30c33442 --- /dev/null +++ b/axiom-eth/src/solidity/types.rs @@ -0,0 +1,65 @@ +//TODO: Replace arrays and gate with &impl +use crate::Field; +use crate::{keccak::types::KeccakVarLenQuery, rlc::types::ConcatVarFixedArrayTrace}; +use halo2_base::{ + safe_types::{SafeBytes32, VarLenBytesVec}, + AssignedValue, +}; + +/// Witness for the computation of a nested mapping +#[derive(Debug, Clone)] +pub struct NestedMappingWitness { + pub witness: Vec>, + pub slot: SafeBytes32, + pub nestings: AssignedValue, +} + +/// Witness for the computation of a mapping with a variable length (Non-Value) key. +#[derive(Debug, Clone)] +pub struct VarMappingWitness { + pub mapping_slot: SafeBytes32, + pub key: VarLenBytesVec, + /// The output of this hash is the storage slot for the mapping key + pub hash_query: KeccakVarLenQuery, +} + +pub type VarMappingTrace = ConcatVarFixedArrayTrace; + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone)] +pub enum MappingWitness { + /// The storage slot corresponding to mapping key of value type + Value(SafeBytes32), + /// Witness for mapping key of non-value type + NonValue(VarMappingWitness), +} + +impl MappingWitness { + pub fn slot(&self) -> SafeBytes32 { + match self { + MappingWitness::Value(slot) => slot.clone(), + MappingWitness::NonValue(witness) => witness.hash_query.output_bytes.clone(), + } + } +} + +/// Return after phase1 of mapping computation. None if the mapping key is of Value type. +pub type MappingTrace = Option>; + +/// Enum whose variants which represents different primitive types of Solidity types that can be represented in circuit. +/// Each variant wraps a `Vec>` representing the bytes of primitive type. +/// +/// Fixed Length Types (Value): +/// -------------------------- +/// * `UInt256`: 32 bytes +/// Fixed length primitive types are represented by a fixed length `Vec>` converted to a `SafeBytes32`. +/// SafeTypes range check that each AssignedValue of the vector is within byte range 0-255 and the vector has length 32. +/// +/// Variable Length Types (NonValue): +/// --------------------------------- +/// * `NonValue`: Variable length byte array +#[derive(Debug, Clone)] +pub enum SolidityType { + Value(SafeBytes32), + NonValue(VarLenBytesVec), +} diff --git a/axiom-eth/src/storage/circuit.rs b/axiom-eth/src/storage/circuit.rs new file mode 100644 index 00000000..c40f9bea --- /dev/null +++ b/axiom-eth/src/storage/circuit.rs @@ -0,0 +1,160 @@ +use ethers_core::types::{Address, Block, Chain, H256, U256}; +#[cfg(feature = "providers")] +use ethers_providers::{JsonRpcClient, Provider}; +use halo2_base::{gates::GateInstructions, Context}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::marker::PhantomData; + +use crate::{ + block_header::get_block_header_rlp_max_lens, + mpt::{MPTChip, MPTInput}, + rlc::{circuit::builder::RlcCircuitBuilder, FIRST_PHASE}, + utils::{ + assign_vec, encode_addr_to_field, encode_h256_to_hilo, eth_circuit::EthCircuitInstructions, + }, + Field, +}; + +use super::{ + EIP1186ResponseDigest, EthBlockAccountStorageWitness, EthBlockStorageInputAssigned, + EthStorageChip, EthStorageInputAssigned, +}; + +#[derive(Clone, Debug, Hash, Serialize, Deserialize)] +pub struct EthStorageInput { + pub addr: Address, + pub acct_pf: MPTInput, + pub acct_state: Vec>, + /// A vector of (slot, value, proof) tuples + pub storage_pfs: Vec<(H256, U256, MPTInput)>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct EthBlockStorageInput { + pub block: Block, + pub block_number: u32, + pub block_hash: H256, // provided for convenience, actual block_hash is computed from block_header + pub block_header: Vec, + pub storage: EthStorageInput, +} + +impl EthStorageInput { + pub fn assign(self, ctx: &mut Context) -> EthStorageInputAssigned { + let address = encode_addr_to_field(&self.addr); + let address = ctx.load_witness(address); + let acct_pf = self.acct_pf.assign(ctx); + let storage_pfs = self + .storage_pfs + .into_iter() + .map(|(slot, _, pf)| { + let slot = encode_h256_to_hilo(&slot).hi_lo(); + let slot = slot.map(|slot| ctx.load_witness(slot)); + let pf = pf.assign(ctx); + (slot, pf) + }) + .collect(); + EthStorageInputAssigned { address, acct_pf, storage_pfs } + } +} + +impl EthBlockStorageInput { + pub fn assign( + self, + ctx: &mut Context, + network: Chain, + ) -> EthBlockStorageInputAssigned { + // let block_hash = encode_h256_to_field(&self.block_hash); + // let block_hash = block_hash.map(|block_hash| ctx.load_witness(block_hash)); + let storage = self.storage.assign(ctx); + let max_len = get_block_header_rlp_max_lens(network).0; + let block_header = assign_vec(ctx, self.block_header, max_len); + EthBlockStorageInputAssigned { block_header, storage } + } +} + +#[derive(Clone, Debug)] +pub struct EthBlockStorageCircuit { + pub inputs: EthBlockStorageInput, // public and private inputs + pub network: Chain, + _marker: PhantomData, +} + +impl EthBlockStorageCircuit { + pub fn new(inputs: EthBlockStorageInput, network: Chain) -> Self { + Self { inputs, network, _marker: PhantomData } + } + + #[cfg(feature = "providers")] + pub fn from_provider( + provider: &Provider

, + block_number: u32, + address: Address, + slots: Vec, + acct_pf_max_depth: usize, + storage_pf_max_depth: usize, + network: Chain, + ) -> Self { + use crate::providers::storage::get_block_storage_input; + + let inputs = get_block_storage_input( + provider, + block_number, + address, + slots, + acct_pf_max_depth, + storage_pf_max_depth, + ); + Self::new(inputs, network) + } +} + +impl EthCircuitInstructions for EthBlockStorageCircuit { + type FirstPhasePayload = (EthBlockAccountStorageWitness, Chain); + + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + ) -> Self::FirstPhasePayload { + let chip = EthStorageChip::new(mpt, Some(self.network)); + // ================= FIRST PHASE ================ + let ctx = builder.base.main(FIRST_PHASE); + let input = self.inputs.clone().assign(ctx, self.network); + let (witness, digest) = chip.parse_eip1186_proofs_from_block_phase0(builder, input); + let EIP1186ResponseDigest { + block_hash, + block_number, + address, + slots_values, + address_is_empty, + slot_is_empty, + } = digest; + let assigned_instances = block_hash + .into_iter() + .chain([block_number, address]) + .chain(slots_values.into_iter().flat_map(|(slot, value)| slot.into_iter().chain(value))) + .collect_vec(); + assert_eq!(builder.base.assigned_instances.len(), 1); + builder.base.assigned_instances[0] = assigned_instances; + // For now this circuit is going to constrain that all slots are occupied. We can also create a circuit that exposes the bitmap of slot_is_empty + { + let ctx = builder.base.main(FIRST_PHASE); + mpt.gate().assert_is_const(ctx, &address_is_empty, &F::ZERO); + for slot_is_empty in slot_is_empty { + mpt.gate().assert_is_const(ctx, &slot_is_empty, &F::ZERO); + } + } + (witness, self.network) + } + + fn virtual_assign_phase1( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + (witness, network): Self::FirstPhasePayload, + ) { + let chip = EthStorageChip::new(mpt, Some(network)); + let _trace = chip.parse_eip1186_proofs_from_block_phase1(builder, witness); + } +} diff --git a/axiom-eth/src/storage/mod.rs b/axiom-eth/src/storage/mod.rs index 8df75610..60649783 100644 --- a/axiom-eth/src/storage/mod.rs +++ b/axiom-eth/src/storage/mod.rs @@ -1,37 +1,31 @@ +use ethers_core::types::Chain; +use getset::Getters; +use halo2_base::{ + gates::{flex_gate::threads::parallelize_core, GateChip, RangeChip}, + safe_types::{SafeAddress, SafeBytes32, SafeTypeChip}, + AssignedValue, Context, +}; +use itertools::Itertools; + use crate::{ - batch_query::response::{ByteArray, FixedByteArray}, - block_header::{ - EthBlockHeaderChip, EthBlockHeaderTrace, EthBlockHeaderTraceWitness, - GOERLI_BLOCK_HEADER_RLP_MAX_BYTES, MAINNET_BLOCK_HEADER_RLP_MAX_BYTES, - }, - keccak::{ - parallelize_keccak_phase0, ContainsParallelizableKeccakQueries, FixedLenRLCs, FnSynthesize, - KeccakChip, VarLenRLCs, + block_header::{EthBlockHeaderChip, EthBlockHeaderTrace, EthBlockHeaderWitness}, + keccak::KeccakChip, + mpt::{MPTChip, MPTProof, MPTProofWitness}, + rlc::{ + chip::RlcChip, + circuit::builder::{RlcCircuitBuilder, RlcContextPair}, + types::RlcTrace, + FIRST_PHASE, }, - mpt::{AssignedBytes, MPTFixedKeyInput, MPTFixedKeyProof, MPTFixedKeyProofWitness}, rlp::{ - builder::{parallelize_phase1, RlcThreadBreakPoints, RlcThreadBuilder}, - rlc::{RlcContextPair, RlcTrace, FIRST_PHASE}, - RlpArrayTraceWitness, RlpChip, RlpFieldTraceWitness, RlpFieldWitness, + types::{RlpArrayWitness, RlpFieldWitness}, + RlpChip, }, - util::{ - bytes_be_to_u128, bytes_be_to_uint, bytes_be_var_to_fixed, encode_addr_to_field, - encode_h256_to_field, encode_u256_to_field, uint_to_bytes_be, AssignedH256, - }, - EthChip, EthCircuitBuilder, EthPreCircuit, Field, Network, ETH_LOOKUP_BITS, + utils::{bytes_be_to_u128, uint_to_bytes_be, AssignedH256}, + Field, }; -use ethers_core::types::{Address, Block, H256, U256}; -#[cfg(feature = "providers")] -use ethers_providers::{JsonRpcClient, Provider}; -use halo2_base::{ - gates::{builder::GateThreadBuilder, GateInstructions, RangeChip, RangeInstructions}, - halo2_proofs::halo2curves::bn256::Fr, - AssignedValue, Context, -}; -use itertools::Itertools; -use serde::{Deserialize, Serialize}; -use std::cell::RefCell; +pub mod circuit; #[cfg(all(test, feature = "providers"))] mod tests; @@ -45,25 +39,17 @@ mod tests; account nonce is uint64 by https://eips.ethereum.org/EIPS/eip-2681 */ -pub(crate) const NUM_ACCOUNT_STATE_FIELDS: usize = 4; -pub(crate) const ACCOUNT_STATE_FIELDS_MAX_BYTES: [usize; NUM_ACCOUNT_STATE_FIELDS] = - [8, 12, 32, 32]; -pub(crate) const ACCOUNT_STATE_FIELD_IS_VAR_LEN: [bool; NUM_ACCOUNT_STATE_FIELDS] = +pub const NUM_ACCOUNT_STATE_FIELDS: usize = 4; +pub const ACCOUNT_STATE_FIELDS_MAX_BYTES: [usize; NUM_ACCOUNT_STATE_FIELDS] = [8, 12, 32, 32]; +#[allow(dead_code)] +pub const ACCOUNT_STATE_FIELD_IS_VAR_LEN: [bool; NUM_ACCOUNT_STATE_FIELDS] = [true, true, false, false]; pub(crate) const ACCOUNT_PROOF_VALUE_MAX_BYTE_LEN: usize = 90; pub(crate) const STORAGE_PROOF_VALUE_MAX_BYTE_LEN: usize = 33; -/* -let max_branch_bytes = MAX_BRANCH_LENS.1; -let (_, max_ext_bytes) = max_ext_lens(32); -// max_len dominated by max branch rlp length = 533 -let max_len = max(max_ext_bytes, max_branch_bytes, 2 * 32, {ACCOUNT,STORAGE}_PROOF_VALUE_MAX_BYTE_LEN); -let cache_bits = bit_length(max_len) -*/ -const CACHE_BITS: usize = 10; - -pub const ACCOUNT_PROOF_MAX_DEPTH: usize = 10; -pub const STORAGE_PROOF_MAX_DEPTH: usize = 9; +#[allow(dead_code)] +pub(crate) const STORAGE_PROOF_KEY_MAX_BYTE_LEN: usize = 32; +/// Stores Account rlcs to be used in later functions. Is returned by `parse_account_proof_phase1`. #[derive(Clone, Debug)] pub struct EthAccountTrace { pub nonce_trace: RlcTrace, @@ -72,14 +58,17 @@ pub struct EthAccountTrace { pub code_hash_trace: RlcTrace, } -#[derive(Clone, Debug)] -pub struct EthAccountTraceWitness { - pub address: AssignedBytes, - pub(crate) array_witness: RlpArrayTraceWitness, - pub(crate) mpt_witness: MPTFixedKeyProofWitness, +/// Stores Account information to be used in later functions. Is returned by `parse_account_proof_phase0`. +#[derive(Clone, Debug, Getters)] +pub struct EthAccountWitness { + pub address: SafeAddress, + #[getset(get = "pub")] + pub(crate) array_witness: RlpArrayWitness, + #[getset(get = "pub")] + pub(crate) mpt_witness: MPTProofWitness, } -impl EthAccountTraceWitness { +impl EthAccountWitness { pub fn get_nonce(&self) -> &RlpFieldWitness { &self.array_witness.field_witness[0] } @@ -94,18 +83,24 @@ impl EthAccountTraceWitness { } } +/// Stores the rlc of a value in a storage slot. Is returned by `parse_storage_proof_phase1`. #[derive(Clone, Debug)] pub struct EthStorageTrace { pub value_trace: RlcTrace, } -#[derive(Clone, Debug)] -pub struct EthStorageTraceWitness { - pub slot: AssignedBytes, - pub(crate) value_witness: RlpFieldTraceWitness, - pub(crate) mpt_witness: MPTFixedKeyProofWitness, +/// Stores storage slot information as well as a proof of inclusion to be verified in parse_storage_phase1. Is returned +/// by `parse_storage_phase0`. +#[derive(Clone, Debug, Getters)] +pub struct EthStorageWitness { + pub slot: SafeBytes32, + #[getset(get = "pub")] + pub(crate) value_witness: RlpFieldWitness, + #[getset(get = "pub")] + pub(crate) mpt_witness: MPTProofWitness, } +/// Stores the rlcs for an account in a block, and the rlcs of slots in the account. Is returned by `parse_eip1186_proofs_from_block_phase1`. #[derive(Clone, Debug)] pub struct EthBlockAccountStorageTrace { pub block_trace: EthBlockHeaderTrace, @@ -113,197 +108,75 @@ pub struct EthBlockAccountStorageTrace { pub storage_trace: Vec>, } +/// Stores a block, an account, and multiple storage witnesses. Is returned by `parse_eip1186_proofs_from_block_phase0`. #[derive(Clone, Debug)] -pub struct EthBlockAccountStorageTraceWitness { - pub block_witness: EthBlockHeaderTraceWitness, - pub acct_witness: EthAccountTraceWitness, - pub storage_witness: Vec>, +pub struct EthBlockAccountStorageWitness { + pub block_witness: EthBlockHeaderWitness, + pub acct_witness: EthAccountWitness, + pub storage_witness: Vec>, } +/// Returns public instances from `parse_eip1186_proofs_from_block_phase0`. #[derive(Clone, Debug)] pub struct EIP1186ResponseDigest { pub block_hash: AssignedH256, pub block_number: AssignedValue, pub address: AssignedValue, // the value U256 is interpreted as H256 (padded with 0s on left) + /// Pairs of (slot, value) where value is a left padded with 0s to fixed width 32 bytes pub slots_values: Vec<(AssignedH256, AssignedH256)>, pub address_is_empty: AssignedValue, pub slot_is_empty: Vec>, } -impl ContainsParallelizableKeccakQueries for EthStorageTraceWitness { - fn shift_query_indices(&mut self, fixed_shift: usize, var_shift: usize) { - self.mpt_witness.shift_query_indices(fixed_shift, var_shift); - } +/// Chip to prove correctness of account and storage proofs +pub struct EthStorageChip<'chip, F: Field> { + pub mpt: &'chip MPTChip<'chip, F>, + /// The network to use for block header decoding. Must be provided if using functions that prove into block header (as opposed to state / storage root) + pub network: Option, } -impl ContainsParallelizableKeccakQueries for EthAccountTraceWitness { - fn shift_query_indices(&mut self, fixed_shift: usize, var_shift: usize) { - self.mpt_witness.shift_query_indices(fixed_shift, var_shift); +impl<'chip, F: Field> EthStorageChip<'chip, F> { + pub fn new(mpt: &'chip MPTChip<'chip, F>, network: Option) -> Self { + Self { mpt, network } } -} -pub trait EthStorageChip { - /// Does inclusion/exclusion proof of `key = keccak(addr)` into an alleged MPT state trie. - /// RLP decodes the ethereumAccount. - /// - /// There is one global state trie, and it is updated every time a client processes a block. In it, a path is always: keccak256(ethereumAddress) and a value is always: rlp(ethereumAccount). More specifically an ethereum account is a 4 item array of [nonce,balance,storageRoot,codeHash]. - fn parse_account_proof_phase0( - &self, - ctx: &mut Context, - keccak: &mut KeccakChip, - addr: AssignedBytes, - proof: MPTFixedKeyProof, - ) -> EthAccountTraceWitness; - - /// SecondPhase of account proof parsing. - fn parse_account_proof_phase1( - &self, - ctx: RlcContextPair, - witness: EthAccountTraceWitness, - ) -> EthAccountTrace; - - /// Does multiple calls to [`parse_account_proof_phase0`] in parallel. - fn parse_account_proofs_phase0( - &self, - thread_pool: &mut GateThreadBuilder, - keccak: &mut KeccakChip, - addr_proofs: Vec<(AssignedBytes, MPTFixedKeyProof)>, - ) -> Vec> - where - Self: Sync, - { - parallelize_keccak_phase0(thread_pool, keccak, addr_proofs, |ctx, keccak, (addr, proof)| { - self.parse_account_proof_phase0(ctx, keccak, addr, proof) - }) + pub fn gate(&self) -> &GateChip { + self.mpt.gate() } - /// SecondPhase of account proofs parsing. - fn parse_account_proofs_phase1( - &self, - thread_pool: &mut RlcThreadBuilder, - witness: Vec>, - ) -> Vec>; - - /// Does inclusion/exclusion proof of `keccak(slot)` into an alleged MPT storage trie. - /// - /// storageRoot: A 256-bit hash of the root node of a Merkle Patricia tree that encodes the storage contents of the account (a mapping between 256-bit integer values), encoded into the trie as a mapping from the Keccak 256-bit hash of the 256-bit integer keys to the RLP-encoded 256-bit integer values. The hash is formally denoted σ[a]_s. - fn parse_storage_proof_phase0( - &self, - ctx: &mut Context, - keccak: &mut KeccakChip, - slot: AssignedBytes, - proof: MPTFixedKeyProof, - ) -> EthStorageTraceWitness; - - /// SecondPhase of storage proof parsing. - fn parse_storage_proof_phase1( - &self, - ctx: RlcContextPair, - witness: EthStorageTraceWitness, - ) -> EthStorageTrace; - - /// Does multiple calls to [`parse_storage_proof_phase0`] in parallel. - fn parse_storage_proofs_phase0( - &self, - thread_pool: &mut GateThreadBuilder, - keccak: &mut KeccakChip, - slot_proofs: Vec<(AssignedBytes, MPTFixedKeyProof)>, - ) -> Vec> - where - Self: Sync, - { - parallelize_keccak_phase0(thread_pool, keccak, slot_proofs, |ctx, keccak, (slot, proof)| { - self.parse_storage_proof_phase0(ctx, keccak, slot, proof) - }) + pub fn range(&self) -> &RangeChip { + self.mpt.range() } - /// SecondPhase of account proofs parsing. - fn parse_storage_proofs_phase1( - &self, - thread_pool: &mut RlcThreadBuilder, - witness: Vec>, - ) -> Vec>; - - /// Does inclusion/exclusion proof of `key = keccak(addr)` into an alleged MPT state trie. - /// RLP decodes the ethereumAccount, which in particular gives the storageRoot. - /// Does (multiple) inclusion/exclusion proof of `keccak(slot)` into the MPT storage trie with root storageRoot. - fn parse_eip1186_proofs_phase0( - &self, - thread_pool: &mut GateThreadBuilder, - keccak: &mut KeccakChip, - addr: AssignedBytes, - acct_pf: MPTFixedKeyProof, - storage_pfs: Vec<(AssignedBytes, MPTFixedKeyProof)>, // (slot_bytes, storage_proof) - ) -> (EthAccountTraceWitness, Vec>) - where - Self: Sync, - { - // TODO: spawn separate thread for account proof; just need to get storage_root first somehow - let ctx = thread_pool.main(FIRST_PHASE); - let acct_trace = self.parse_account_proof_phase0(ctx, keccak, addr, acct_pf); - // ctx dropped - let storage_root = &acct_trace.get_storage_root().field_cells; - let storage_trace = parallelize_keccak_phase0( - thread_pool, - keccak, - storage_pfs, - |ctx, keccak, (slot, storage_pf)| { - let witness = self.parse_storage_proof_phase0(ctx, keccak, slot, storage_pf); - // check MPT root is storage_root - for (pf_byte, byte) in - witness.mpt_witness.root_hash_bytes.iter().zip_eq(storage_root.iter()) - { - ctx.constrain_equal(pf_byte, byte); - } - witness - }, - ); - (acct_trace, storage_trace) + pub fn rlc(&self) -> &RlcChip { + self.mpt.rlc() } - fn parse_eip1186_proofs_phase1( - &self, - thread_pool: &mut RlcThreadBuilder, - witness: (EthAccountTraceWitness, Vec>), - ) -> (EthAccountTrace, Vec>); - - // slot and block_hash are big-endian 16-byte - // inputs have H256 represented in (hi,lo) format as two u128s - // block number and slot values can be derived from the final trace output - fn parse_eip1186_proofs_from_block_phase0( - &self, - thread_pool: &mut GateThreadBuilder, - keccak: &mut KeccakChip, - input: EthBlockStorageInputAssigned, - network: Network, - ) -> (EthBlockAccountStorageTraceWitness, EIP1186ResponseDigest) - where - Self: EthBlockHeaderChip; - - fn parse_eip1186_proofs_from_block_phase1( - &self, - thread_pool: &mut RlcThreadBuilder, - witness: EthBlockAccountStorageTraceWitness, - ) -> EthBlockAccountStorageTrace - where - Self: EthBlockHeaderChip; -} + pub fn rlp(&self) -> RlpChip { + self.mpt.rlp() + } -impl<'chip, F: Field> EthStorageChip for EthChip<'chip, F> { - fn parse_account_proof_phase0( + pub fn keccak(&self) -> &KeccakChip { + self.mpt.keccak() + } + /// Does inclusion/exclusion proof of `key = keccak(addr)` into an alleged MPT state trie. Alleged means the proof is with respect to an alleged stateRoot. + /// RLP decodes the ethereumAccount. + /// + /// There is one global state trie, and it is updated every time a client processes a block. In it, a path is always: keccak256(ethereumAddress) and a value is always: rlp(ethereumAccount). More specifically an ethereum account is a 4 item array of [nonce,balance,storageRoot,codeHash]. + /// + /// Does input validation of `proof` (e.g., checking witnesses are bytes). + pub fn parse_account_proof_phase0( &self, ctx: &mut Context, - keccak: &mut KeccakChip, - address: AssignedBytes, - proof: MPTFixedKeyProof, - ) -> EthAccountTraceWitness { + address: SafeAddress, + proof: MPTProof, + ) -> EthAccountWitness { assert_eq!(32, proof.key_bytes.len()); // check key is keccak(addr) - assert_eq!(address.len(), 20); - let hash_query_idx = keccak.keccak_fixed_len(ctx, self.gate(), address.clone(), None); - let hash_addr = &keccak.fixed_len_queries[hash_query_idx].output_assigned; + let hash_query = self.keccak().keccak_fixed_len(ctx, address.as_ref().to_vec()); + let hash_addr = hash_query.output_bytes.as_ref(); for (byte, key) in hash_addr.iter().zip_eq(proof.key_bytes.iter()) { ctx.constrain_equal(byte, key); @@ -318,19 +191,20 @@ impl<'chip, F: Field> EthStorageChip for EthChip<'chip, F> { ); // Check MPT inclusion for: // keccak(addr) => RLP([nonce, balance, storage_root, code_hash]) - let mpt_witness = self.parse_mpt_inclusion_fixed_key_phase0(ctx, keccak, proof); + let mpt_witness = self.mpt.parse_mpt_inclusion_phase0(ctx, proof); - EthAccountTraceWitness { address, array_witness, mpt_witness } + EthAccountWitness { address, array_witness, mpt_witness } } - fn parse_account_proof_phase1( + /// SecondPhase of account proof parsing. + pub fn parse_account_proof_phase1( &self, (ctx_gate, ctx_rlc): RlcContextPair, - witness: EthAccountTraceWitness, + witness: EthAccountWitness, ) -> EthAccountTrace { // Comments below just to log what load_rlc_cache calls are done in the internal functions: // load_rlc_cache bit_length(2*mpt_witness.key_byte_len) - self.parse_mpt_inclusion_fixed_key_phase1((ctx_gate, ctx_rlc), witness.mpt_witness); + self.mpt.parse_mpt_inclusion_phase1((ctx_gate, ctx_rlc), witness.mpt_witness); // load rlc_cache bit_length(array_witness.rlp_array.len()) let array_trace: [_; 4] = self .rlp() @@ -343,32 +217,45 @@ impl<'chip, F: Field> EthStorageChip for EthChip<'chip, F> { EthAccountTrace { nonce_trace, balance_trace, storage_root_trace, code_hash_trace } } - fn parse_account_proofs_phase1( + /// Does multiple calls to [`parse_account_proof_phase0`] in parallel. + pub fn parse_account_proofs_phase0( &self, - thread_pool: &mut RlcThreadBuilder, - acct_witness: Vec>, - ) -> Vec> { - // pre-load rlc cache so later parallelization is deterministic - let (ctx_gate, ctx_rlc) = thread_pool.rlc_ctx_pair(); - self.rlc().load_rlc_cache((ctx_gate, ctx_rlc), self.gate(), CACHE_BITS); + builder: &mut RlcCircuitBuilder, + addr_proofs: Vec<(SafeAddress, MPTProof)>, + ) -> Vec> { + parallelize_core(builder.base.pool(0), addr_proofs, |ctx, (addr, proof)| { + self.parse_account_proof_phase0(ctx, addr, proof) + }) + } - parallelize_phase1(thread_pool, acct_witness, |(ctx_gate, ctx_rlc), witness| { + /// SecondPhase of account proofs parsing. + pub fn parse_account_proofs_phase1( + &self, + builder: &mut RlcCircuitBuilder, + acct_witness: Vec>, + ) -> Vec> { + // rlc cache is loaded globally when `builder` was constructed; no longer done here to avoid concurrency issues + builder.parallelize_phase1(acct_witness, |(ctx_gate, ctx_rlc), witness| { self.parse_account_proof_phase1((ctx_gate, ctx_rlc), witness) }) } - fn parse_storage_proof_phase0( + /// Does inclusion/exclusion proof of `keccak(slot)` into an alleged MPT storage trie. Alleged means the proof is with respect to an alleged storageRoot. + /// + /// storageRoot: A 256-bit hash of the root node of a Merkle Patricia tree that encodes the storage contents of the account (a mapping between 256-bit integer values), encoded into the trie as a mapping from the Keccak 256-bit hash of the 256-bit integer keys to the RLP-encoded 256-bit integer values. The hash is formally denoted σ[a]_s. + /// + /// Will do input validation on `proof` (e.g., checking witnesses are bytes). + pub fn parse_storage_proof_phase0( &self, ctx: &mut Context, - keccak: &mut KeccakChip, - slot: AssignedBytes, - proof: MPTFixedKeyProof, - ) -> EthStorageTraceWitness { + slot: SafeBytes32, + proof: MPTProof, + ) -> EthStorageWitness { assert_eq!(32, proof.key_bytes.len()); // check key is keccak(slot) - let hash_query_idx = keccak.keccak_fixed_len(ctx, self.gate(), slot.clone(), None); - let hash_bytes = &keccak.fixed_len_queries[hash_query_idx].output_assigned; + let hash_query = self.keccak().keccak_fixed_len(ctx, slot.as_ref().to_vec()); + let hash_bytes = hash_query.output_bytes.as_ref(); for (hash, key) in hash_bytes.iter().zip_eq(proof.key_bytes.iter()) { ctx.constrain_equal(hash, key); @@ -378,18 +265,19 @@ impl<'chip, F: Field> EthStorageChip for EthChip<'chip, F> { let value_witness = self.rlp().decompose_rlp_field_phase0(ctx, proof.value_bytes.clone(), 32); // check MPT inclusion - let mpt_witness = self.parse_mpt_inclusion_fixed_key_phase0(ctx, keccak, proof); - EthStorageTraceWitness { slot, value_witness, mpt_witness } + let mpt_witness = self.mpt.parse_mpt_inclusion_phase0(ctx, proof); + EthStorageWitness { slot, value_witness, mpt_witness } } - fn parse_storage_proof_phase1( + /// SecondPhase of storage proof parsing. + pub fn parse_storage_proof_phase1( &self, (ctx_gate, ctx_rlc): RlcContextPair, - witness: EthStorageTraceWitness, + witness: EthStorageWitness, ) -> EthStorageTrace { // Comments below just to log what load_rlc_cache calls are done in the internal functions: // load_rlc_cache bit_length(2*mpt_witness.key_byte_len) - self.parse_mpt_inclusion_fixed_key_phase1((ctx_gate, ctx_rlc), witness.mpt_witness); + self.mpt.parse_mpt_inclusion_phase1((ctx_gate, ctx_rlc), witness.mpt_witness); // load rlc_cache bit_length(value_witness.rlp_field.len()) let value_trace = self.rlp().decompose_rlp_field_phase1((ctx_gate, ctx_rlc), witness.value_witness); @@ -398,63 +286,97 @@ impl<'chip, F: Field> EthStorageChip for EthChip<'chip, F> { EthStorageTrace { value_trace } } - fn parse_storage_proofs_phase1( + /// Does multiple calls to [`parse_storage_proof_phase0`] in parallel. + pub fn parse_storage_proofs_phase0( + &self, + builder: &mut RlcCircuitBuilder, + slot_proofs: Vec<(SafeBytes32, MPTProof)>, + ) -> Vec> { + parallelize_core(builder.base.pool(0), slot_proofs, |ctx, (slot, proof)| { + self.parse_storage_proof_phase0(ctx, slot, proof) + }) + } + + /// SecondPhase of account proofs parsing. + pub fn parse_storage_proofs_phase1( &self, - thread_pool: &mut RlcThreadBuilder, - storage_witness: Vec>, + builder: &mut RlcCircuitBuilder, + storage_witness: Vec>, ) -> Vec> { - let (ctx_gate, ctx_rlc) = thread_pool.rlc_ctx_pair(); - // pre-load rlc cache so later parallelization is deterministic - self.rlc().load_rlc_cache((ctx_gate, ctx_rlc), self.gate(), CACHE_BITS); - parallelize_phase1(thread_pool, storage_witness, |(ctx_gate, ctx_rlc), witness| { + // rlc cache is loaded globally when `builder` was constructed; no longer done here to avoid concurrency issues + builder.parallelize_phase1(storage_witness, |(ctx_gate, ctx_rlc), witness| { self.parse_storage_proof_phase1((ctx_gate, ctx_rlc), witness) }) } - fn parse_eip1186_proofs_phase1( + /// Does inclusion/exclusion proof of `key = keccak(addr)` into an alleged MPT state trie. Alleged means the proof is with respect to an alleged stateRoot. + /// + /// RLP decodes the ethereumAccount, which in particular gives the storageRoot. + /// + /// Does (multiple) inclusion/exclusion proof of `keccak(slot)` into the MPT storage trie with root storageRoot. + pub fn parse_eip1186_proofs_phase0( + &self, + builder: &mut RlcCircuitBuilder, + addr: SafeAddress, + acct_pf: MPTProof, + storage_pfs: Vec<(SafeBytes32, MPTProof)>, // (slot_bytes, storage_proof) + ) -> (EthAccountWitness, Vec>) { + // TODO: spawn separate thread for account proof; just need to get storage_root first somehow + let ctx = builder.base.main(FIRST_PHASE); + let acct_trace = self.parse_account_proof_phase0(ctx, addr, acct_pf); + // ctx dropped + let storage_root = &acct_trace.get_storage_root().field_cells; + let storage_trace = + parallelize_core(builder.base.pool(0), storage_pfs, |ctx, (slot, storage_pf)| { + let witness = self.parse_storage_proof_phase0(ctx, slot, storage_pf); + // check MPT root is storage_root + for (pf_byte, byte) in + witness.mpt_witness.root_hash_bytes.iter().zip_eq(storage_root.iter()) + { + ctx.constrain_equal(pf_byte, byte); + } + witness + }); + (acct_trace, storage_trace) + } + + /// SecondPhase of `parse_eip1186_proofs_phase0` + pub fn parse_eip1186_proofs_phase1( &self, - thread_pool: &mut RlcThreadBuilder, - (acct_witness, storage_witness): ( - EthAccountTraceWitness, - Vec>, - ), + builder: &mut RlcCircuitBuilder, + (acct_witness, storage_witness): (EthAccountWitness, Vec>), ) -> (EthAccountTrace, Vec>) { - let (ctx_gate, ctx_rlc) = thread_pool.rlc_ctx_pair(); + let (ctx_gate, ctx_rlc) = builder.rlc_ctx_pair(); let acct_trace = self.parse_account_proof_phase1((ctx_gate, ctx_rlc), acct_witness); - let storage_trace = self.parse_storage_proofs_phase1(thread_pool, storage_witness); + let storage_trace = self.parse_storage_proofs_phase1(builder, storage_witness); (acct_trace, storage_trace) } - fn parse_eip1186_proofs_from_block_phase0( + /// Proves (multiple) storage proofs into storageRoot, prove account proof of storageRoot into stateRoot, and proves stateRoot is an RLP decoded block header. + /// Computes the block hash by hashing the RLP encoded block header. + /// In other words, proves block, account, storage with respect to an alleged block hash. + // inputs have H256 represented in (hi,lo) format as two u128s + // block number and slot values can be derived from the final trace output + pub fn parse_eip1186_proofs_from_block_phase0( &self, - thread_pool: &mut GateThreadBuilder, - keccak: &mut KeccakChip, + builder: &mut RlcCircuitBuilder, input: EthBlockStorageInputAssigned, - network: Network, - ) -> (EthBlockAccountStorageTraceWitness, EIP1186ResponseDigest) - where - Self: EthBlockHeaderChip, - { - let ctx = thread_pool.main(FIRST_PHASE); + ) -> (EthBlockAccountStorageWitness, EIP1186ResponseDigest) { + let ctx = builder.base.main(FIRST_PHASE); let address = input.storage.address; - let mut block_header = input.block_header; - let max_len = match network { - Network::Goerli => GOERLI_BLOCK_HEADER_RLP_MAX_BYTES, - Network::Mainnet => MAINNET_BLOCK_HEADER_RLP_MAX_BYTES, + let block_header = input.block_header; + let block_witness = { + let block_header_chip = + EthBlockHeaderChip::new_from_network(self.rlp(), self.network.unwrap()); + block_header_chip.decompose_block_header_phase0(ctx, self.keccak(), &block_header) }; - block_header.resize(max_len, 0); - let block_witness = self.decompose_block_header_phase0(ctx, keccak, &block_header, network); let state_root = &block_witness.get_state_root().field_cells; - let block_hash_hi_lo = bytes_be_to_u128(ctx, self.gate(), &block_witness.block_hash); + let block_hash_hi_lo = block_witness.get_block_hash_hi_lo(); // compute block number from big-endian bytes - let block_num_bytes = &block_witness.get_number().field_cells; - let block_num_len = block_witness.get_number().field_len; - let block_number = - bytes_be_var_to_fixed(ctx, self.gate(), block_num_bytes, block_num_len, 4); - let block_number = bytes_be_to_uint(ctx, self.gate(), &block_number, 4); + let block_number = block_witness.get_number_value(ctx, self.gate()); // verify account + storage proof let addr_bytes = uint_to_bytes_be(ctx, self.range(), &address, 20); @@ -465,19 +387,18 @@ impl<'chip, F: Field> EthStorageChip for EthChip<'chip, F> { .map(|(slot, storage_pf)| { let slot_bytes = slot.iter().map(|u128| uint_to_bytes_be(ctx, self.range(), u128, 16)).concat(); - (slot, (slot_bytes, storage_pf)) + (slot, (slot_bytes.try_into().unwrap(), storage_pf)) }) .unzip(); // drop ctx let (acct_witness, storage_witness) = self.parse_eip1186_proofs_phase0( - thread_pool, - keccak, - addr_bytes, + builder, + addr_bytes.try_into().unwrap(), input.storage.acct_pf, storage_pfs, ); - let ctx = thread_pool.main(FIRST_PHASE); + let ctx = builder.base.main(FIRST_PHASE); // check MPT root of acct_witness is state root for (pf_byte, byte) in acct_witness.mpt_witness.root_hash_bytes.iter().zip_eq(state_root.iter()) @@ -490,15 +411,18 @@ impl<'chip, F: Field> EthStorageChip for EthChip<'chip, F> { .zip(storage_witness.iter()) .map(|(slot, witness)| { // get value as U256 from RLP decoding, convert to H256, then to hi-lo - let value_bytes: ByteArray = (&witness.value_witness.witness).into(); - let value_bytes = value_bytes.to_fixed(ctx, self.gate()); + let value_bytes = witness.value_witness.field_cells.clone(); + let value_bytes_len = witness.value_witness.field_len; + let var_bytes = + SafeTypeChip::unsafe_to_var_len_bytes_vec(value_bytes, value_bytes_len, 32); + let value_bytes = var_bytes.left_pad_to_fixed(ctx, self.gate()); let value: [_; 2] = - bytes_be_to_u128(ctx, self.gate(), &value_bytes.0).try_into().unwrap(); + bytes_be_to_u128(ctx, self.gate(), value_bytes.bytes()).try_into().unwrap(); (slot, value) }) .collect_vec(); let digest = EIP1186ResponseDigest { - block_hash: block_hash_hi_lo.try_into().unwrap(), + block_hash: block_hash_hi_lo, block_number, address, slots_values, @@ -508,229 +432,39 @@ impl<'chip, F: Field> EthStorageChip for EthChip<'chip, F> { .map(|witness| witness.mpt_witness.slot_is_empty) .collect_vec(), }; - ( - EthBlockAccountStorageTraceWitness { block_witness, acct_witness, storage_witness }, - digest, - ) + (EthBlockAccountStorageWitness { block_witness, acct_witness, storage_witness }, digest) } - fn parse_eip1186_proofs_from_block_phase1( + pub fn parse_eip1186_proofs_from_block_phase1( &self, - thread_pool: &mut RlcThreadBuilder, - witness: EthBlockAccountStorageTraceWitness, - ) -> EthBlockAccountStorageTrace - where - Self: EthBlockHeaderChip, - { - let block_trace = - self.decompose_block_header_phase1(thread_pool.rlc_ctx_pair(), witness.block_witness); - let (acct_trace, storage_trace) = self.parse_eip1186_proofs_phase1( - thread_pool, - (witness.acct_witness, witness.storage_witness), - ); + builder: &mut RlcCircuitBuilder, + witness: EthBlockAccountStorageWitness, + ) -> EthBlockAccountStorageTrace { + let block_trace = { + let block_header_chip = + EthBlockHeaderChip::new_from_network(self.rlp(), self.network.unwrap()); + block_header_chip + .decompose_block_header_phase1(builder.rlc_ctx_pair(), witness.block_witness) + }; + let (acct_trace, storage_trace) = self + .parse_eip1186_proofs_phase1(builder, (witness.acct_witness, witness.storage_witness)); EthBlockAccountStorageTrace { block_trace, acct_trace, storage_trace } } } -#[derive(Clone, Debug, Hash, Serialize, Deserialize)] -pub struct EthStorageInput { - pub addr: Address, - pub acct_pf: MPTFixedKeyInput, - pub acct_state: Vec>, - /// A vector of (slot, value, proof) tuples - pub storage_pfs: Vec<(H256, U256, MPTFixedKeyInput)>, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct EthBlockStorageInput { - pub block: Block, - pub block_number: u32, - pub block_hash: H256, // provided for convenience, actual block_hash is computed from block_header - pub block_header: Vec, - pub storage: EthStorageInput, -} - -impl EthStorageInput { - pub fn assign(self, ctx: &mut Context) -> EthStorageInputAssigned { - let address = encode_addr_to_field(&self.addr); - let address = ctx.load_witness(address); - let acct_pf = self.acct_pf.assign(ctx); - let storage_pfs = self - .storage_pfs - .into_iter() - .map(|(slot, _, pf)| { - let slot = encode_h256_to_field(&slot); - let slot = slot.map(|slot| ctx.load_witness(slot)); - let pf = pf.assign(ctx); - (slot, pf) - }) - .collect(); - EthStorageInputAssigned { address, acct_pf, storage_pfs } - } - - pub fn assign_account( - self, - ctx: &mut Context, - range: &impl RangeInstructions, - ) -> AccountQuery { - let address = FixedByteArray::new(ctx, range, self.addr.as_bytes()); - let acct_pf = self.acct_pf.assign(ctx); - AccountQuery { address, acct_pf } - } - - pub fn assign_storage( - self, - ctx: &mut Context, - range: &impl RangeInstructions, - ) -> StorageQuery { - assert_eq!(self.storage_pfs.len(), 1); - let (slot, _, storage_pf) = self.storage_pfs.into_iter().next().unwrap(); - let slot = FixedByteArray::new(ctx, range, slot.as_bytes()); - let storage_pf = storage_pf.assign(ctx); - StorageQuery { slot, storage_pf } - } -} - -impl EthBlockStorageInput { - pub fn assign(self, ctx: &mut Context) -> EthBlockStorageInputAssigned { - // let block_hash = encode_h256_to_field(&self.block_hash); - // let block_hash = block_hash.map(|block_hash| ctx.load_witness(block_hash)); - let storage = self.storage.assign(ctx); - EthBlockStorageInputAssigned { block_header: self.block_header, storage } - } -} - -#[derive(Clone, Debug)] -pub struct AccountQuery { - pub address: FixedByteArray, // 20 bytes - pub acct_pf: MPTFixedKeyProof, -} - -#[derive(Clone, Debug)] -pub struct StorageQuery { - pub slot: FixedByteArray, // 20 bytes - pub storage_pf: MPTFixedKeyProof, -} - +/// Account and storage proof inputs in compressed form #[derive(Clone, Debug)] pub struct EthStorageInputAssigned { pub address: AssignedValue, // U160 - pub acct_pf: MPTFixedKeyProof, - pub storage_pfs: Vec<(AssignedH256, MPTFixedKeyProof)>, // (slot, proof) where slot is H256 as (u128, u128) + pub acct_pf: MPTProof, + pub storage_pfs: Vec<(AssignedH256, MPTProof)>, // (slot, proof) where slot is H256 as (u128, u128) } #[derive(Clone, Debug)] pub struct EthBlockStorageInputAssigned { // block_hash: AssignedH256, // H256 as (u128, u128) - pub block_header: Vec, + /// The RLP encoded block header for the block that alleged contains the stateRoot from `storage`. + pub block_header: Vec>, + /// Account proof and (multiple) storage proofs, with respect to an alleged stateRoot. pub storage: EthStorageInputAssigned, } - -#[derive(Clone, Debug)] -pub struct EthBlockStorageCircuit { - pub inputs: EthBlockStorageInput, // public and private inputs - pub network: Network, -} - -impl EthBlockStorageCircuit { - #[cfg(feature = "providers")] - pub fn from_provider( - provider: &Provider

, - block_number: u32, - address: Address, - slots: Vec, - acct_pf_max_depth: usize, - storage_pf_max_depth: usize, - network: Network, - ) -> Self { - use crate::providers::get_block_storage_input; - - let inputs = get_block_storage_input( - provider, - block_number, - address, - slots, - acct_pf_max_depth, - storage_pf_max_depth, - ); - Self { inputs, network } - } - - // MAYBE UNUSED - // blockHash, blockNumber, address, (slot, value)s - // with H256 encoded as hi-lo (u128, u128) - pub fn instance(&self) -> Vec { - let EthBlockStorageInput { block_number, block_hash, storage, .. } = &self.inputs; - let EthStorageInput { addr, storage_pfs, .. } = storage; - let mut instance = Vec::with_capacity(4 + 4 * storage_pfs.len()); - instance.extend(encode_h256_to_field::(block_hash)); - instance.push(F::from(*block_number as u64)); - instance.push(encode_addr_to_field(addr)); - for (slot, value, _) in storage_pfs { - instance.extend(encode_h256_to_field::(slot)); - instance.extend(encode_u256_to_field::(value)); - } - instance - } -} - -impl EthPreCircuit for EthBlockStorageCircuit { - fn create( - self, - mut builder: RlcThreadBuilder, - break_points: Option, - ) -> EthCircuitBuilder> { - let range = RangeChip::default(ETH_LOOKUP_BITS); - let chip = EthChip::new(RlpChip::new(&range, None), None); - let mut keccak = KeccakChip::default(); - // ================= FIRST PHASE ================ - let ctx = builder.gate_builder.main(FIRST_PHASE); - let input = self.inputs.assign(ctx); - let (witness, digest) = chip.parse_eip1186_proofs_from_block_phase0( - &mut builder.gate_builder, - &mut keccak, - input, - self.network, - ); - let EIP1186ResponseDigest { - block_hash, - block_number, - address, - slots_values, - address_is_empty, - slot_is_empty, - } = digest; - let assigned_instances = block_hash - .into_iter() - .chain([block_number, address]) - .chain( - slots_values - .into_iter() - .flat_map(|(slot, value)| slot.into_iter().chain(value.into_iter())), - ) - .collect_vec(); - // For now this circuit is going to constrain that all slots are occupied. We can also create a circuit that exposes the bitmap of slot_is_empty - { - let ctx = builder.gate_builder.main(FIRST_PHASE); - range.gate.assert_is_const(ctx, &address_is_empty, &Fr::zero()); - for slot_is_empty in slot_is_empty { - range.gate.assert_is_const(ctx, &slot_is_empty, &Fr::zero()); - } - } - - EthCircuitBuilder::new( - assigned_instances, - builder, - RefCell::new(keccak), - range, - break_points, - move |builder: &mut RlcThreadBuilder, - rlp: RlpChip, - keccak_rlcs: (FixedLenRLCs, VarLenRLCs)| { - // ======== SECOND PHASE =========== - let chip = EthChip::new(rlp, Some(keccak_rlcs)); - let _trace = chip.parse_eip1186_proofs_from_block_phase1(builder, witness); - }, - ) - } -} diff --git a/axiom-eth/src/storage/tests.rs b/axiom-eth/src/storage/tests.rs index 4cd6265e..bb856179 100644 --- a/axiom-eth/src/storage/tests.rs +++ b/axiom-eth/src/storage/tests.rs @@ -1,282 +1,190 @@ -use super::*; -use crate::util::EthConfigParams; +use super::{circuit::EthBlockStorageCircuit, *}; use crate::{ - halo2_proofs::{ - dev::MockProver, - halo2curves::bn256::{Bn256, G1Affine}, - plonk::*, - poly::commitment::ParamsProver, - poly::kzg::{ - commitment::KZGCommitmentScheme, - multiopen::{ProverSHPLONK, VerifierSHPLONK}, - strategy::SingleStrategy, - }, - transcript::{ - Blake2bRead, Blake2bWrite, Challenge255, TranscriptReadBuffer, TranscriptWriterBuffer, - }, - }, - providers::{GOERLI_PROVIDER_URL, MAINNET_PROVIDER_URL}, + providers::setup_provider, + rlc::{circuit::RlcCircuitParams, tests::get_rlc_params}, + utils::eth_circuit::{create_circuit, EthCircuitParams}, }; use ark_std::{end_timer, start_timer}; -use ethers_core::utils::keccak256; -use halo2_base::utils::fs::gen_srs; -use rand_core::OsRng; -use serde::{Deserialize, Serialize}; -use std::env::var; -use std::{ - env::set_var, - fs::File, - io::{BufReader, Write}, +use ethers_core::{ + types::{Address, H256}, + utils::keccak256, +}; +use halo2_base::{ + gates::circuit::BaseCircuitParams, + utils::{ + fs::gen_srs, + testing::{check_proof_with_instances, gen_proof_with_instances}, + }, +}; +use halo2_base::{ + gates::circuit::CircuitBuilderStage, + halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr, plonk::*}, }; +use serde::{Deserialize, Serialize}; +use std::{fs::File, io::Write}; use test_log::test; -fn get_test_circuit(network: Network, num_slots: usize) -> EthBlockStorageCircuit { - assert!(num_slots <= 10); - let infura_id = var("INFURA_ID").expect("INFURA_ID environmental variable not set"); - let provider_url = match network { - Network::Mainnet => format!("{MAINNET_PROVIDER_URL}{infura_id}"), - Network::Goerli => format!("{GOERLI_PROVIDER_URL}{infura_id}"), - }; - let provider = - Provider::new_client(&provider_url, 10, 500).expect("could not instantiate HTTP Provider"); +fn get_test_circuit(network: Chain, num_slots: usize) -> EthBlockStorageCircuit { + let provider = setup_provider(network); let addr; let block_number; match network { - Network::Mainnet => { + Chain::Mainnet => { // cryptopunks addr = "0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB".parse::

().unwrap(); block_number = 16356350; //block_number = 0xf929e6; } - Network::Goerli => { + Chain::Goerli => { addr = "0xf2d1f94310823fe26cfa9c9b6fd152834b8e7849".parse::
().unwrap(); block_number = 0x713d54; } + _ => { + todo!() + } } // For only occupied slots: - let slot_nums = vec![0u64, 1u64, 2u64, 3u64, 6u64, 8u64]; - let mut slots = (0..4) - .map(|x| { - let mut bytes = [0u8; 64]; - bytes[31] = x; - bytes[63] = 10; - H256::from_slice(&keccak256(bytes)) - }) - .collect::>(); + let slot_nums = [0u64, 1u64, 2u64, 3u64, 6u64, 8u64]; + let mut slots = vec![]; slots.extend(slot_nums.iter().map(|x| H256::from_low_u64_be(*x))); + slots.extend((0..num_slots.saturating_sub(slot_nums.len())).map(|x| { + let mut bytes = [0u8; 64]; + bytes[31] = x as u8; + bytes[63] = 10; + H256::from_slice(&keccak256(bytes)) + })); slots.truncate(num_slots); - // let slots: Vec<_> = (0..num_slots).map(|x| H256::from_low_u64_be(x as u64)).collect(); - slots.truncate(num_slots); - EthBlockStorageCircuit::from_provider(&provider, block_number, addr, slots, 8, 8, network) + EthBlockStorageCircuit::from_provider(&provider, block_number, addr, slots, 13, 13, network) } #[test] -pub fn test_mock_single_eip1186() -> Result<(), Box> { - let params = EthConfigParams::from_path("configs/tests/storage.json"); - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(¶ms).unwrap()); - let k = params.degree; +pub fn test_mock_single_eip1186() { + let params = get_rlc_params("configs/tests/storage.json"); + let k = params.base.k as u32; + + let input = get_test_circuit(Chain::Mainnet, 1); + let mut circuit = create_circuit(CircuitBuilderStage::Mock, params, input); + circuit.mock_fulfill_keccak_promises(None); + circuit.calculate_params(); + let instances = circuit.instances(); + MockProver::run(k, &circuit, instances).unwrap().assert_satisfied(); +} - let input = get_test_circuit(Network::Mainnet, 1); - let circuit = input.create_circuit(RlcThreadBuilder::mock(), None); - MockProver::run(k, &circuit, vec![circuit.instance()]).unwrap().assert_satisfied(); - Ok(()) +fn get_test_circuit_detailed_tether( + network: Chain, + slot_nums: Vec, +) -> EthBlockStorageCircuit { + assert!(slot_nums.len() <= 10); + let provider = setup_provider(network); + let addr; + let block_number; + match network { + Chain::Mainnet => { + addr = "0xdAC17F958D2ee523a2206206994597C13D831ec7".parse::
().unwrap(); + block_number = 16799999; + } + Chain::Goerli => { + addr = "0xdAC17F958D2ee523a2206206994597C13D831ec7".parse::
().unwrap(); + block_number = 16799999; + } + _ => todo!(), + } + // For only occupied slots: + let slots = slot_nums; + EthBlockStorageCircuit::from_provider(&provider, block_number, addr, slots, 8, 9, network) +} + +#[test] +pub fn test_mock_small_val() { + let params = get_rlc_params("configs/tests/storage.json"); + let k = params.base.k as u32; + let lower: u128 = 0xdfad11d8b97bedfbd5b2574864aec982; + let upper: u128 = 0x015130eac76c1a0c44f4cd1dcd859cd8; + let mut bytes: [u8; 32] = [0; 32]; + bytes[..16].copy_from_slice(&upper.to_be_bytes()); + bytes[16..].copy_from_slice(&lower.to_be_bytes()); + let slot = H256(bytes); + let input = get_test_circuit_detailed_tether(Chain::Mainnet, vec![slot]); + let mut circuit = create_circuit(CircuitBuilderStage::Mock, params, input); + circuit.mock_fulfill_keccak_promises(None); + circuit.calculate_params(); + let instances = circuit.instances(); + MockProver::run(k, &circuit, instances).unwrap().assert_satisfied(); } #[derive(Serialize, Deserialize)] -struct BenchParams(EthConfigParams, usize); // (params, num_slots) +struct BenchParams(RlcCircuitParams, usize); // (params, num_slots) #[test] +#[ignore = "bench"] pub fn bench_eip1186() -> Result<(), Box> { - let bench_params_file = File::open("configs/bench/storage.json").unwrap(); + let bench_params_file = File::create("configs/bench/storage.json").unwrap(); std::fs::create_dir_all("data/bench")?; let mut fs_results = File::create("data/bench/storage.csv").unwrap(); - writeln!(fs_results, "degree,total_advice,num_rlc_columns,num_advice,num_lookup,num_fixed,proof_time,verify_time")?; - - let bench_params_reader = BufReader::new(bench_params_file); - let bench_params: Vec = serde_json::from_reader(bench_params_reader).unwrap(); - for bench_params in bench_params { - println!( - "---------------------- degree = {} ------------------------------", - bench_params.0.degree - ); - - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(&bench_params.0).unwrap()); - let input = get_test_circuit(Network::Mainnet, bench_params.1); - let instance = input.instance(); - let circuit = input.clone().create_circuit(RlcThreadBuilder::keygen(), None); - - let params = gen_srs(bench_params.0.degree); + writeln!(fs_results, "degree,num_slots,total_advice,num_rlc_columns,num_advice,num_lookup,num_fixed,proof_time,verify_time")?; + + let mut all_bench_params = vec![]; + let bench_k_num = vec![(15, 1), (18, 10), (20, 32)]; + for (k, num_slots) in bench_k_num { + println!("---------------------- degree = {k} ------------------------------",); + let input = get_test_circuit(Chain::Mainnet, num_slots); + let mut dummy_params = EthCircuitParams::default().rlc; + dummy_params.base.k = k; + let mut circuit = create_circuit(CircuitBuilderStage::Keygen, dummy_params, input.clone()); + circuit.mock_fulfill_keccak_promises(None); + circuit.calculate_params(); + + let params = gen_srs(k as u32); let vk = keygen_vk(¶ms, &circuit)?; let pk = keygen_pk(¶ms, vk, &circuit)?; - let break_points = circuit.circuit.break_points.take(); + let bench_params = circuit.params().rlc; + let break_points = circuit.break_points(); // create a proof let proof_time = start_timer!(|| "create proof SHPLONK"); let phase0_time = start_timer!(|| "phase 0 synthesize"); - let circuit = input.create_circuit(RlcThreadBuilder::prover(), Some(break_points)); + let circuit = create_circuit(CircuitBuilderStage::Prover, bench_params.clone(), input) + .use_break_points(break_points); + circuit.mock_fulfill_keccak_promises(None); + let instances = circuit.instances(); + let instances = instances.iter().map(|x| &x[..]).collect_vec(); end_timer!(phase0_time); - let mut transcript = Blake2bWrite::<_, _, Challenge255<_>>::init(vec![]); - create_proof::< - KZGCommitmentScheme, - ProverSHPLONK<'_, Bn256>, - Challenge255, - _, - Blake2bWrite, G1Affine, Challenge255>, - _, - >(¶ms, &pk, &[circuit], &[&[&instance]], OsRng, &mut transcript)?; - let proof = transcript.finalize(); + let proof = gen_proof_with_instances(¶ms, &pk, circuit, &instances); end_timer!(proof_time); let verify_time = start_timer!(|| "Verify time"); - let verifier_params = params.verifier_params(); - let strategy = SingleStrategy::new(¶ms); - let mut transcript = Blake2bRead::<_, _, Challenge255<_>>::init(&proof[..]); - verify_proof::< - KZGCommitmentScheme, - VerifierSHPLONK<'_, Bn256>, - Challenge255, - Blake2bRead<&[u8], G1Affine, Challenge255>, - SingleStrategy<'_, Bn256>, - >(verifier_params, pk.get_vk(), strategy, &[&[&instance]], &mut transcript) - .unwrap(); + check_proof_with_instances(¶ms, pk.get_vk(), &proof, &instances, true); end_timer!(verify_time); - let keccak_advice = std::env::var("KECCAK_ADVICE_COLUMNS") - .unwrap_or_else(|_| "0".to_string()) - .parse::() - .unwrap(); - let bench_params: EthConfigParams = - serde_json::from_str(var("ETH_CONFIG_PARAMS").unwrap().as_str()).unwrap(); + let RlcCircuitParams { + base: + BaseCircuitParams { + k, + num_advice_per_phase, + num_fixed, + num_lookup_advice_per_phase, + .. + }, + num_rlc_columns, + } = bench_params.clone(); writeln!( fs_results, - "{},{},{},{:?},{:?},{},{:.2}s,{:?}", - bench_params.degree, - bench_params.num_rlc_columns - + bench_params.num_range_advice.iter().sum::() - + bench_params.num_lookup_advice.iter().sum::() - + keccak_advice, - bench_params.num_rlc_columns, - bench_params.num_range_advice, - bench_params.num_lookup_advice, - bench_params.num_fixed, + "{},{},{},{},{:?},{:?},{},{:.2}s,{:?}", + k, + num_slots, + num_rlc_columns + + num_advice_per_phase.iter().sum::() + + num_lookup_advice_per_phase.iter().sum::(), + num_rlc_columns, + num_advice_per_phase, + num_lookup_advice_per_phase, + num_fixed, proof_time.time.elapsed().as_secs_f64(), verify_time.time.elapsed() ) .unwrap(); + all_bench_params.push(BenchParams(bench_params, num_slots)); } - Ok(()) -} - -#[test] -#[cfg(feature = "evm")] -pub fn bench_evm_eip1186() -> Result<(), Box> { - use crate::util::circuit::custom_gen_evm_verifier_shplonk; - use halo2_base::gates::builder::CircuitBuilderStage; - use snark_verifier_sdk::{ - evm::{evm_verify, gen_evm_proof_shplonk, write_calldata}, - gen_pk, - halo2::{ - aggregation::{AggregationCircuit, AggregationConfigParams}, - gen_snark_shplonk, - }, - CircuitExt, SHPLONK, - }; - use std::{fs, path::Path}; - let bench_params_file = File::open("configs/bench/storage.json").unwrap(); - let evm_params_file = File::open("configs/bench/storage_evm.json").unwrap(); - std::fs::create_dir_all("data/bench")?; - let mut fs_results = File::create("data/bench/storage.csv").unwrap(); - writeln!(fs_results, "degree,total_advice,num_rlc_columns,num_advice,num_lookup,num_fixed,storage_proof_time,evm_proof_time")?; - - let bench_params_reader = BufReader::new(bench_params_file); - let bench_params: Vec = serde_json::from_reader(bench_params_reader).unwrap(); - let evm_params_reader = BufReader::new(evm_params_file); - let evm_params: Vec = - serde_json::from_reader(evm_params_reader).unwrap(); - for (bench_params, evm_params) in bench_params.iter().zip(evm_params.iter()) { - println!( - "---------------------- degree = {} ------------------------------", - bench_params.0.degree - ); - - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(&bench_params.0).unwrap()); - - let (storage_snark, storage_proof_time) = { - let k = bench_params.0.degree; - let input = get_test_circuit(Network::Mainnet, bench_params.1); - let circuit = input.clone().create_circuit(RlcThreadBuilder::keygen(), None); - let params = gen_srs(k); - let pk = gen_pk(¶ms, &circuit, None); - let break_points = circuit.circuit.break_points.take(); - let storage_proof_time = start_timer!(|| "Storage Proof SHPLONK"); - let circuit = input.create_circuit(RlcThreadBuilder::prover(), Some(break_points)); - let snark = gen_snark_shplonk(¶ms, &pk, circuit, None::<&str>); - end_timer!(storage_proof_time); - (snark, storage_proof_time) - }; - - let k = evm_params.degree; - let params = gen_srs(k); - set_var("LOOKUP_BITS", evm_params.lookup_bits.to_string()); - let evm_circuit = AggregationCircuit::public::( - CircuitBuilderStage::Keygen, - None, - evm_params.lookup_bits, - ¶ms, - vec![storage_snark.clone()], - false, - ); - evm_circuit.config(k, Some(10)); - let pk = gen_pk(¶ms, &evm_circuit, None); - let break_points = evm_circuit.break_points(); - - let instances = evm_circuit.instances(); - let evm_proof_time = start_timer!(|| "EVM Proof SHPLONK"); - let pf_circuit = AggregationCircuit::public::( - CircuitBuilderStage::Prover, - Some(break_points), - evm_params.lookup_bits, - ¶ms, - vec![storage_snark], - false, - ); - let proof = gen_evm_proof_shplonk(¶ms, &pk, pf_circuit, instances.clone()); - end_timer!(evm_proof_time); - fs::create_dir_all("data/storage").unwrap(); - write_calldata(&instances, &proof, Path::new("data/storage/test.calldata")).unwrap(); - - let deployment_code = custom_gen_evm_verifier_shplonk( - ¶ms, - pk.get_vk(), - &evm_circuit, - Some(Path::new("data/storage/test.yul")), - ); - - // this verifies proof in EVM and outputs gas cost (if successful) - evm_verify(deployment_code, instances, proof); - - let keccak_advice = std::env::var("KECCAK_ADVICE_COLUMNS") - .unwrap_or_else(|_| "0".to_string()) - .parse::() - .unwrap(); - let bench_params: EthConfigParams = - serde_json::from_str(var("ETH_CONFIG_PARAMS").unwrap().as_str()).unwrap(); - writeln!( - fs_results, - "{},{},{},{:?},{:?},{},{:.2}s,{:?}", - bench_params.degree, - bench_params.num_rlc_columns - + bench_params.num_range_advice.iter().sum::() - + bench_params.num_lookup_advice.iter().sum::() - + keccak_advice, - bench_params.num_rlc_columns, - bench_params.num_range_advice, - bench_params.num_lookup_advice, - bench_params.num_fixed, - storage_proof_time.time.elapsed().as_secs_f64(), - evm_proof_time.time.elapsed() - ) - .unwrap(); - } + serde_json::to_writer_pretty(bench_params_file, &all_bench_params).unwrap(); Ok(()) } diff --git a/axiom-eth/src/transaction/mod.rs b/axiom-eth/src/transaction/mod.rs new file mode 100644 index 00000000..b36703ac --- /dev/null +++ b/axiom-eth/src/transaction/mod.rs @@ -0,0 +1,519 @@ +use std::cmp::max; + +use ethers_core::{ + types::{Block, Chain, Transaction, H256}, + utils::hex::FromHex, +}; +use halo2_base::{ + gates::{ + flex_gate::threads::parallelize_core, GateChip, GateInstructions, RangeChip, + RangeInstructions, + }, + AssignedValue, Context, + QuantumCell::{Constant, Existing}, +}; +use itertools::Itertools; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; + +use crate::{ + block_header::{ + get_block_header_rlp_max_lens, EthBlockHeaderChip, EthBlockHeaderTrace, + EthBlockHeaderWitness, + }, + keccak::KeccakChip, + mpt::{MPTChip, MPTInput, MPTProof, MPTProofWitness}, + rlc::{ + chip::RlcChip, + circuit::builder::{RlcCircuitBuilder, RlcContextPair}, + FIRST_PHASE, + }, + rlp::{evaluate_byte_array, RlpChip}, + utils::circuit_utils::constrain_no_leading_zeros, + Field, +}; + +// pub mod helpers; +#[cfg(all(test, feature = "providers"))] +mod tests; +mod types; + +pub use types::*; + +lazy_static! { + static ref KECCAK_RLP_EMPTY_STRING: Vec = + Vec::from_hex("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(); +} + +// type 0 tx has 9 fields +// type 1 tx has 11 fields +// type 2 tx has 12 fields +pub const TRANSACTION_MAX_FIELDS: usize = 12; +pub(crate) const TRANSACTION_TYPE_0_FIELDS_MAX_BYTES: [usize; TRANSACTION_MAX_FIELDS] = + [32, 32, 32, 20, 32, 0, 8, 32, 32, 1, 1, 1]; +pub(crate) const TRANSACTION_TYPE_1_FIELDS_MAX_BYTES: [usize; TRANSACTION_MAX_FIELDS] = + [8, 32, 32, 32, 20, 32, 0, 0, 1, 32, 32, 1]; +pub(crate) const TRANSACTION_TYPE_2_FIELDS_MAX_BYTES: [usize; TRANSACTION_MAX_FIELDS] = + [8, 8, 8, 8, 32, 20, 32, 0, 0, 1, 32, 32]; +pub(crate) const TRANSACTION_IDX_MAX_LEN: usize = 2; +pub const TX_IDX_MAX_BYTES: usize = TRANSACTION_IDX_MAX_LEN; + +/// Calculate max rlp length of a transaction given some parameters +pub fn calc_max_val_len( + max_data_byte_len: usize, + max_access_list_len: usize, + enable_types: [bool; 3], +) -> usize { + let mut t0_len = 2; + // TODO: do not hardcode + let prefix_tot_max = 1 + 4 + 10 + 4 * 2; + let mut field_len_sum = max_data_byte_len; + for field_len in TRANSACTION_TYPE_0_FIELDS_MAX_BYTES { + field_len_sum += field_len; + } + if enable_types[0] { + t0_len = max(t0_len, prefix_tot_max + field_len_sum); + } + field_len_sum = max_data_byte_len + max_access_list_len; + for field_len in TRANSACTION_TYPE_1_FIELDS_MAX_BYTES { + field_len_sum += field_len; + } + if enable_types[1] { + t0_len = max(t0_len, prefix_tot_max + field_len_sum); + } + field_len_sum = max_data_byte_len + max_access_list_len; + for field_len in TRANSACTION_TYPE_2_FIELDS_MAX_BYTES { + field_len_sum += field_len; + } + if enable_types[2] { + t0_len = max(t0_len, prefix_tot_max + field_len_sum); + } + // Transaction and variable len fields have a prefix at most 3, others have prefix at most 1. + // Add 1 for the transaction type byte + t0_len +} + +/// Calculate the max capacity needed to contain fields at all positions across all transaction types. +fn calc_max_field_len( + max_data_byte_len: usize, + max_access_list_len: usize, + enable_types: [bool; 3], +) -> Vec { + let mut base = vec![0; TRANSACTION_MAX_FIELDS]; + base[0] = 1; + for i in 0..TRANSACTION_MAX_FIELDS { + if enable_types[0] { + if i == 5 { + base[i] = max(base[i], max_data_byte_len); + } else { + base[i] = max(base[i], TRANSACTION_TYPE_0_FIELDS_MAX_BYTES[i]); + } + } + if enable_types[1] { + if i == 6 { + base[i] = max(base[i], max_data_byte_len); + } else if i == 7 { + base[i] = max(base[i], max_access_list_len); + } else { + base[i] = max(base[i], TRANSACTION_TYPE_1_FIELDS_MAX_BYTES[i]); + } + } + if enable_types[2] { + if i == 7 { + base[i] = max(base[i], max_data_byte_len); + } else if i == 8 { + base[i] = max(base[i], max_access_list_len); + } else { + base[i] = max(base[i], TRANSACTION_TYPE_2_FIELDS_MAX_BYTES[i]); + } + } + } + base +} + +/// Configuration parameters to construct [EthTransactionChip] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Hash, Default)] +pub struct EthTransactionChipParams { + /// Sets the `max_field_length` for possible positions for the `data` field. + pub max_data_byte_len: usize, + /// Sets the `max_field_length` for possible positions for the `accessList` field. + pub max_access_list_len: usize, + /// Specifies which transaction types [0x0, 0x1, 0x2] this chip supports + pub enable_types: [bool; 3], + /// Must provide network to use functions involving block header + pub network: Option, +} + +/// Chip that supports functions that prove transactions and transaction fields +#[derive(Clone, Debug)] +pub struct EthTransactionChip<'chip, F: Field> { + pub mpt: &'chip MPTChip<'chip, F>, + pub params: EthTransactionChipParams, +} + +impl<'chip, F: Field> EthTransactionChip<'chip, F> { + pub fn new(mpt: &'chip MPTChip<'chip, F>, params: EthTransactionChipParams) -> Self { + Self { mpt, params } + } + + pub fn gate(&self) -> &GateChip { + self.mpt.gate() + } + + pub fn range(&self) -> &RangeChip { + self.mpt.range() + } + + pub fn rlc(&self) -> &RlcChip { + self.mpt.rlc() + } + + pub fn rlp(&self) -> RlpChip { + self.mpt.rlp() + } + + pub fn keccak(&self) -> &KeccakChip { + self.mpt.keccak() + } + + pub fn mpt(&self) -> &'chip MPTChip<'chip, F> { + self.mpt + } + + pub fn network(&self) -> Option { + self.params.network + } + + pub fn block_header_chip(&self) -> EthBlockHeaderChip { + EthBlockHeaderChip::new_from_network( + self.rlp(), + self.network().expect("Must provide network to access block header chip"), + ) + } + + /// FirstPhase of proving the inclusion **or** exclusion of a transaction index within a transaction root. + /// In the case of + /// - inclusion: then parses the transaction + /// - exclusion: `input.proof.slot_is_empty` is true, and we return `tx_type = -1, value = rlp(0x00)`. + pub fn parse_transaction_proof_phase0( + &self, + ctx: &mut Context, + input: EthTransactionInputAssigned, + ) -> EthTransactionWitness { + let EthTransactionChipParams { + max_data_byte_len, max_access_list_len, enable_types, .. + } = self.params; + let EthTransactionInputAssigned { transaction_index, proof } = input; + // Load value early to avoid borrow errors + let slot_is_empty = proof.slot_is_empty; + + let all_disabled = !(enable_types[0] || enable_types[1] || enable_types[2]); + // check key is rlp(idx): + // given rlp(idx), parse idx as var len bytes + let idx_witness = self.rlp().decompose_rlp_field_phase0( + ctx, + proof.key_bytes.clone(), + TRANSACTION_IDX_MAX_LEN, + ); + // evaluate idx to number + let tx_idx = + evaluate_byte_array(ctx, self.gate(), &idx_witness.field_cells, idx_witness.field_len); + // check idx equals provided transaction_index from input + ctx.constrain_equal(&tx_idx, &transaction_index); + constrain_no_leading_zeros( + ctx, + self.gate(), + &idx_witness.field_cells, + idx_witness.field_len, + ); + + // check MPT inclusion + let mpt_witness = self.mpt.parse_mpt_inclusion_phase0(ctx, proof); + // parse transaction + // when we disable all types, we use that as a flag to parse dummy values which are two bytes long + let max_field_lens = + calc_max_field_len(max_data_byte_len, max_access_list_len, enable_types); + if all_disabled { + let one = ctx.load_constant(F::ONE); + ctx.constrain_equal(&slot_is_empty, &one); + } + // type > 0 are stored as {0x01, 0x02} . encode(tx) + let type_is_not_zero = + self.range().is_less_than(ctx, mpt_witness.value_bytes[0], Constant(F::from(128)), 8); + // if the first byte is greater than 0xf7, the type is zero. Otherwise, the type is the first byte. + let mut tx_type = self.gate().mul(ctx, type_is_not_zero, mpt_witness.value_bytes[0]); + let max_val_len = if all_disabled { + 2_usize + } else { + calc_max_val_len(max_data_byte_len, max_access_list_len, enable_types) + }; + debug_assert!(max_val_len > 1); + let mut new_value_witness = Vec::with_capacity(max_val_len); + let slot_is_full = self.gate().not(ctx, slot_is_empty); + tx_type = self.gate().mul(ctx, tx_type, slot_is_full); + // tx_type = -1 if and only if the slot is empty, serves as a flag + tx_type = self.gate().sub(ctx, tx_type, slot_is_empty); + // parse the zeroes string if the slot is empty so that we don't run into errors + for i in 0..max_val_len { + let mut val_byte = self.gate().select( + ctx, + mpt_witness + .value_bytes + .get(i + 1) + .map(|a| Existing(*a)) + .unwrap_or(Constant(F::ZERO)), + mpt_witness.value_bytes[i], + type_is_not_zero, + ); + val_byte = if i == 0 { + // 0xc100 = rlp(0x00) + self.gate().select(ctx, val_byte, Constant(F::from(0xc1)), slot_is_full) + } else { + self.gate().mul(ctx, val_byte, slot_is_full) + }; + new_value_witness.push(val_byte); + } + let value_witness = + self.rlp().decompose_rlp_array_phase0(ctx, new_value_witness, &max_field_lens, true); + EthTransactionWitness { + transaction_type: tx_type, + idx: tx_idx, + idx_witness, + value_witness, + mpt_witness, + } + } + + /// SecondPhase of proving inclusion **or** exclusion of a transaction index in transaction root, and then parses + /// the transaction. See [`parse_transaction_proof_phase0`] for more details. + pub fn parse_transaction_proof_phase1( + &self, + (ctx_gate, ctx_rlc): RlcContextPair, + witness: EthTransactionWitness, + ) -> EthTransactionTrace { + // Comments below just to log what load_rlc_cache calls are done in the internal functions: + self.rlp().decompose_rlp_field_phase1((ctx_gate, ctx_rlc), witness.idx_witness); + // load_rlc_cache bit_length(2*mpt_witness.key_byte_len) + self.mpt.parse_mpt_inclusion_phase1((ctx_gate, ctx_rlc), witness.mpt_witness); + // load rlc_cache bit_length(value_witness.rlp_field.len()) + let value_trace = + self.rlp().decompose_rlp_array_phase1((ctx_gate, ctx_rlc), witness.value_witness, true); + let value_trace = value_trace.field_trace; + EthTransactionTrace { transaction_type: witness.transaction_type, value_trace } + } + + /// Parallelizes `parse_transaction_proof_phase0`. + pub fn parse_transaction_proofs_phase0( + &self, + builder: &mut RlcCircuitBuilder, + input: Vec>, + ) -> Vec> { + parallelize_core(builder.base.pool(0), input, |ctx, input| { + self.parse_transaction_proof_phase0(ctx, input) + }) + } + + /// Parallelizes `parse_transaction_proof_phase1` + pub fn parse_transaction_proofs_phase1( + &self, + builder: &mut RlcCircuitBuilder, + transaction_witness: Vec>, + ) -> Vec> { + // rlc cache should be loaded globally, no longer done here + builder.parallelize_phase1(transaction_witness, |(ctx_gate, ctx_rlc), witness| { + self.parse_transaction_proof_phase1((ctx_gate, ctx_rlc), witness) + }) + } + + /// FirstPhase of proving the inclusion **or** exclusion of transactions into the block header of a given block. + /// Also parses the transaction into fields. + /// + /// If `input.len_proof` is Some, then we prove the total number of transactions in this block. + /// + /// This performs [`EthBlockHeaderChip::decompose_block_header_phase0`] (single-threaded) and then multi-threaded `parse_transaction_proof_phase0`. + pub fn parse_transaction_proofs_from_block_phase0( + &self, + builder: &mut RlcCircuitBuilder, + input: EthBlockTransactionsInputAssigned, + ) -> EthBlockTransactionsWitness { + let block_witness = { + let ctx = builder.base.main(FIRST_PHASE); + let block_header = input.block_header; + self.block_header_chip().decompose_block_header_phase0( + ctx, + self.keccak(), + &block_header, + ) + }; + let transactions_root = &block_witness.get_transactions_root().field_cells; + + // verify transaction proofs + let transaction_witness = { + parallelize_core(builder.base.pool(FIRST_PHASE), input.tx_inputs, |ctx, input| { + let witness = self.parse_transaction_proof_phase0(ctx, input); + // check MPT root is transactions_root + for (pf_byte, byte) in + witness.mpt_witness.root_hash_bytes.iter().zip_eq(transactions_root.iter()) + { + ctx.constrain_equal(pf_byte, byte); + } + witness + }) + }; + // ctx dropped + let (len, len_witness) = if let Some(len_proof) = input.len_proof { + // we calculate and prove the total number of transactions in this block + let ctx = builder.base.main(FIRST_PHASE); + let one = ctx.load_constant(F::ONE); + let is_empty = { + let mut is_empty = one; + let mut empty_hash = Vec::with_capacity(32); + for i in 0..32 { + empty_hash.push(ctx.load_constant(F::from(KECCAK_RLP_EMPTY_STRING[i] as u64))); + } + for (pf_byte, byte) in empty_hash.iter().zip(transactions_root.iter()) { + let byte_match = self.gate().is_equal(ctx, *pf_byte, *byte); + is_empty = self.gate().and(ctx, is_empty, byte_match); + } + is_empty + }; + let inclusion_idx = len_proof[0].transaction_index; + let noninclusion_idx = len_proof[1].transaction_index; + let diff = self.gate().sub(ctx, noninclusion_idx, inclusion_idx); + // If non_empty, the difference should be equal to 1 + let correct_diff = self.gate().is_equal(ctx, diff, one); + // If empty, the second index should be 0 + let correct_empty = self.gate().is_zero(ctx, noninclusion_idx); + let correct = self.gate().or(ctx, correct_diff, correct_empty); + ctx.constrain_equal(&correct, &one); + // Constrains that the first is an inclusion proof and that the latter is a noninclusion proof + // If empty, the first can be a noninclusion proof + let slot_is_full = self.gate().not(ctx, len_proof[0].proof.slot_is_empty); + // If transaction trie is empty, then `noninclusion_idx` must equal `0`. + // Otherwise the proof for `inclusion_idx` must be an inclusion proof. + let inclusion_constraint = + self.gate().select(ctx, correct_empty, slot_is_full, is_empty); + ctx.constrain_equal(&inclusion_constraint, &one); + ctx.constrain_equal(&len_proof[1].proof.slot_is_empty, &one); + // Checks that the proofs are correct + ( + Some(noninclusion_idx), + Some(len_proof.map(|tx_input| { + let witness = self.parse_transaction_proof_phase0(ctx, tx_input); + // check MPT root is transactions_root + for (pf_byte, byte) in + witness.mpt_witness.root_hash_bytes.iter().zip(transactions_root.iter()) + { + ctx.constrain_equal(pf_byte, byte); + } + witness + })), + ) + } else { + (None, None) + }; + + EthBlockTransactionsWitness { block_witness, transaction_witness, len, len_witness } + } + + /// SecondPhase of proving the inclusion of transactions into the transaction root of a given block. + /// Also parses the transaction into fields. + pub fn parse_transaction_proofs_from_block_phase1( + &self, + builder: &mut RlcCircuitBuilder, + witness: EthBlockTransactionsWitness, + ) -> EthBlockTransactionsTrace { + let block_trace = self + .block_header_chip() + .decompose_block_header_phase1(builder.rlc_ctx_pair(), witness.block_witness); + let transaction_trace = + self.parse_transaction_proofs_phase1(builder, witness.transaction_witness); + let len_trace = witness + .len_witness + .map(|len_witness| self.parse_transaction_proofs_phase1(builder, len_witness.to_vec())); + EthBlockTransactionsTrace { block_trace, transaction_trace, len: witness.len, len_trace } + } + + /// Combination of `decompose_block_header_phase0` and `parse_transaction_proof_phase0`. + /// Constrains that the `transaction_root` is contained in the `block_header`. + /// + /// The difference between this function and `parse_transaction_proofs_from_block_phase0` is that this function + /// is entirely single-threaded, so you can then further parallelize the entire function. + /// + /// This _will_ range check `block_header` to be bytes. + pub fn parse_transaction_proof_from_block_phase0( + &self, + ctx: &mut Context, + block_header: &[AssignedValue], + tx_input: EthTransactionInputAssigned, + ) -> (EthBlockHeaderWitness, EthTransactionWitness) { + let block_witness = self.block_header_chip().decompose_block_header_phase0( + ctx, + self.keccak(), + block_header, + ); + let transactions_root = &block_witness.get_transactions_root().field_cells; + // check MPT root of transaction_witness is block_witness.transaction_root + let transaction_witness = { + let witness = self.parse_transaction_proof_phase0(ctx, tx_input); + // check MPT root is transactions_root + for (pf_byte, byte) in + witness.mpt_witness.root_hash_bytes.iter().zip_eq(transactions_root.iter()) + { + ctx.constrain_equal(pf_byte, byte); + } + witness + }; + (block_witness, transaction_witness) + } + + /// Combination of `decompose_block_header_phase1` and `parse_transaction_proof_phase1`. Single-threaded. + pub fn parse_transaction_proof_from_block_phase1( + &self, + (ctx_gate, ctx_rlc): RlcContextPair, + block_witness: EthBlockHeaderWitness, + tx_witness: EthTransactionWitness, + ) -> (EthBlockHeaderTrace, EthTransactionTrace) { + let block_trace = self + .block_header_chip() + .decompose_block_header_phase1((ctx_gate, ctx_rlc), block_witness); + let transaction_trace = + self.parse_transaction_proof_phase1((ctx_gate, ctx_rlc), tx_witness); + (block_trace, transaction_trace) + } + + /// Extracts the field at `field_idx` from the given rlp decomposition of a transaction. + /// + /// Constrains that `witness` must be an inclusion proof of the transaction into transaction root + /// (a priori it could be exclusion proof). + pub fn extract_field( + &self, + ctx: &mut Context, + witness: EthTransactionWitness, + field_idx: AssignedValue, + ) -> EthTransactionFieldWitness { + let field_witness = &witness.value_witness.field_witness; + let slot_is_empty = witness.mpt_witness.slot_is_empty; + let ans_len = field_witness.iter().map(|w| w.field_cells.len()).max().unwrap(); + let indicator = self.gate().idx_to_indicator(ctx, field_idx, TRANSACTION_MAX_FIELDS); + assert_eq!(field_witness.len(), TRANSACTION_MAX_FIELDS); + let zero = ctx.load_zero(); + ctx.constrain_equal(&slot_is_empty, &zero); + let mut field_bytes = Vec::with_capacity(ans_len); + for i in 0..ans_len { + let entries = field_witness.iter().map(|w| *w.field_cells.get(i).unwrap_or(&zero)); + let field_byte = self.gate().select_by_indicator(ctx, entries, indicator.clone()); + field_bytes.push(field_byte); + } + let lens = field_witness.iter().map(|w| w.field_len); + let len = self.gate().select_by_indicator(ctx, lens, indicator); + EthTransactionFieldWitness { + transaction_type: witness.transaction_type, + transaction_witness: witness, + field_idx, + field_bytes, + len, + max_len: ans_len, + } + } +} diff --git a/axiom-eth/src/transaction/tests/data/field/single_tx_pos_test_legacy.json b/axiom-eth/src/transaction/tests/data/field/single_tx_pos_test_legacy.json new file mode 100644 index 00000000..c03a1dac --- /dev/null +++ b/axiom-eth/src/transaction/tests/data/field/single_tx_pos_test_legacy.json @@ -0,0 +1,9 @@ +{ + "idxs": [ + [ + 257, + 4 + ] + ], + "block_number": 5000008 +} \ No newline at end of file diff --git a/axiom-eth/src/transaction/tests/data/field/single_tx_pos_test_new.json b/axiom-eth/src/transaction/tests/data/field/single_tx_pos_test_new.json new file mode 100644 index 00000000..998198e8 --- /dev/null +++ b/axiom-eth/src/transaction/tests/data/field/single_tx_pos_test_new.json @@ -0,0 +1,9 @@ +{ + "idxs": [ + [ + 208, + 11 + ] + ], + "block_number": 17578525 +} \ No newline at end of file diff --git a/axiom-eth/src/transaction/tests/data/multi_tx_pos_test_legacy.json b/axiom-eth/src/transaction/tests/data/multi_tx_pos_test_legacy.json new file mode 100644 index 00000000..a3cd191a --- /dev/null +++ b/axiom-eth/src/transaction/tests/data/multi_tx_pos_test_legacy.json @@ -0,0 +1,5 @@ +{ + "idxs": [0, 1, 130, 257], + "block_number": 5000050 +} + \ No newline at end of file diff --git a/axiom-eth/src/transaction/tests/data/multi_tx_pos_test_new.json b/axiom-eth/src/transaction/tests/data/multi_tx_pos_test_new.json new file mode 100644 index 00000000..be83a028 --- /dev/null +++ b/axiom-eth/src/transaction/tests/data/multi_tx_pos_test_new.json @@ -0,0 +1,5 @@ +{ + "idxs": [0, 1], + "block_number": 17578525 +} + \ No newline at end of file diff --git a/axiom-eth/src/transaction/tests/data/single_tx_pos_test_legacy.json b/axiom-eth/src/transaction/tests/data/single_tx_pos_test_legacy.json new file mode 100644 index 00000000..134d6092 --- /dev/null +++ b/axiom-eth/src/transaction/tests/data/single_tx_pos_test_legacy.json @@ -0,0 +1,5 @@ +{ + "idxs": [257], + "block_number": 5000008 +} + \ No newline at end of file diff --git a/axiom-eth/src/transaction/tests/data/single_tx_pos_test_new.json b/axiom-eth/src/transaction/tests/data/single_tx_pos_test_new.json new file mode 100644 index 00000000..aecc9df0 --- /dev/null +++ b/axiom-eth/src/transaction/tests/data/single_tx_pos_test_new.json @@ -0,0 +1,5 @@ +{ + "idxs": [2], + "block_number": 17578525 +} + \ No newline at end of file diff --git a/axiom-eth/src/transaction/tests/data/stress_test.json b/axiom-eth/src/transaction/tests/data/stress_test.json new file mode 100644 index 00000000..9a037142 --- /dev/null +++ b/axiom-eth/src/transaction/tests/data/stress_test.json @@ -0,0 +1 @@ +10 \ No newline at end of file diff --git a/axiom-eth/src/transaction/tests/data/zero_tx_pos_test_legacy.json b/axiom-eth/src/transaction/tests/data/zero_tx_pos_test_legacy.json new file mode 100644 index 00000000..11c50660 --- /dev/null +++ b/axiom-eth/src/transaction/tests/data/zero_tx_pos_test_legacy.json @@ -0,0 +1,5 @@ +{ + "idxs": [], + "block_number": 1000 +} + \ No newline at end of file diff --git a/axiom-eth/src/transaction/tests/data/zero_tx_pos_test_new.json b/axiom-eth/src/transaction/tests/data/zero_tx_pos_test_new.json new file mode 100644 index 00000000..11c50660 --- /dev/null +++ b/axiom-eth/src/transaction/tests/data/zero_tx_pos_test_new.json @@ -0,0 +1,5 @@ +{ + "idxs": [], + "block_number": 1000 +} + \ No newline at end of file diff --git a/axiom-eth/src/transaction/tests/field.rs b/axiom-eth/src/transaction/tests/field.rs new file mode 100644 index 00000000..49b10b63 --- /dev/null +++ b/axiom-eth/src/transaction/tests/field.rs @@ -0,0 +1,260 @@ +#![cfg(feature = "providers")] +use crate::utils::assign_vec; + +use super::*; +use ethers_core::types::Chain; +use halo2_base::halo2_proofs::dev::MockProver; +use serde::{Deserialize, Serialize}; +use std::fs::File; +use test_log::test; + +#[derive(Clone, Debug, Hash, Deserialize, Serialize)] +/// Contains block information, a single `EthTransactionInput` for a transaction we want to prove in the block, +/// and a `constrain_len` flag that decides whether the number of transactions in the block should be proved as well. +/// In most cases, constrain_len will be set to `false` and len_proof to `None` when this is used. +pub struct EthBlockTransactionFieldInput { + pub block_number: u32, + pub block_hash: H256, // provided for convenience, actual block_hash is computed from block_header + pub block_header: Vec, + + pub tx_input: EthTransactionFieldInput, + pub constrain_len: bool, + // Inclusion and noninclusion proof pair of neighboring indices + pub len_proof: Option, +} + +#[derive(Clone, Debug)] +/// Assigned version of `EthTransactionFieldInput` +pub struct EthTransactionFieldInputAssigned { + pub transaction: EthTransactionInputAssigned, + pub field_idx: AssignedValue, +} + +#[derive(Clone, Debug)] +/// Assigned version of `EthBlockTransactionFieldInput` +pub struct EthBlockTransactionFieldInputAssigned { + // block_hash: AssignedH256, // H256 as (u128, u128) + pub block_header: Vec>, + pub single_field: EthTransactionFieldInputAssigned, +} + +impl EthTransactionFieldInput { + pub fn assign(self, ctx: &mut Context) -> EthTransactionFieldInputAssigned { + let transaction = EthTransactionInputAssigned { + transaction_index: ctx.load_witness(F::from(self.transaction_index as u64)), + proof: self.proof.assign(ctx), + }; + let field_idx = ctx.load_witness(F::from(self.field_idx as u64)); + EthTransactionFieldInputAssigned { transaction, field_idx } + } +} + +impl EthBlockTransactionFieldInput { + pub fn assign( + self, + ctx: &mut Context, + network: Chain, + ) -> EthBlockTransactionFieldInputAssigned { + let max_len = get_block_header_rlp_max_lens(network).0; + let block_header = assign_vec(ctx, self.block_header, max_len); + EthBlockTransactionFieldInputAssigned { + block_header, + single_field: self.tx_input.assign(ctx), + } + } +} + +#[derive(Clone)] +pub struct EthBlockTransactionFieldCircuit { + pub inputs: EthBlockTransactionFieldInput, // public and private inputs + pub params: EthTransactionChipParams, + _marker: PhantomData, +} + +impl EthBlockTransactionFieldCircuit { + #[allow(clippy::too_many_arguments)] + #[cfg(feature = "providers")] + pub fn from_provider( + provider: &Provider

, + idxs: Vec<(usize, usize)>, // (tx_idx, field_idx) + block_number: u32, + transaction_pf_max_depth: usize, + network: Chain, + max_data_byte_len: usize, + max_access_list_len: usize, + enable_types: [bool; 3], + constrain_len: bool, + ) -> Self { + use crate::providers::transaction::get_block_transaction_input; + + let tx_idx = idxs.clone().into_iter().map(|p| p.0).collect_vec(); + let inputs = get_block_transaction_input( + provider, + tx_idx, + block_number, + transaction_pf_max_depth, + max_data_byte_len, + max_access_list_len, + enable_types, + constrain_len, + ); + let tx_inputs = inputs + .tx_proofs + .into_iter() + .zip(idxs) + .map(|(tx_proof, (_, field_idx))| EthTransactionFieldInput { + transaction_index: tx_proof.tx_index, + proof: tx_proof.proof, + field_idx, + }) + .collect_vec(); + let inputs = EthBlockTransactionFieldInput { + block_number: inputs.block_number, + block_hash: inputs.block_hash, + block_header: inputs.block_header, + tx_input: tx_inputs[0].clone(), + constrain_len: false, + len_proof: None, + }; + let params = EthTransactionChipParams { + max_data_byte_len, + max_access_list_len, + enable_types, + network: Some(network), + }; + Self { inputs, params, _marker: PhantomData } + } +} + +impl EthCircuitInstructions for EthBlockTransactionFieldCircuit { + type FirstPhasePayload = + (EthBlockHeaderWitness, EthTransactionWitness, EthTransactionChipParams); + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + ) -> Self::FirstPhasePayload { + let chip = EthTransactionChip::new(mpt, self.params); + let ctx = builder.base.main(FIRST_PHASE); + let input = self.inputs.clone().assign(ctx, chip.network().unwrap()); + let (block_witness, tx_witness) = chip.parse_transaction_proof_from_block_phase0( + ctx, + &input.block_header, + input.single_field.transaction, + ); + let _ = chip.extract_field(ctx, tx_witness.clone(), input.single_field.field_idx); + (block_witness, tx_witness, self.params) + } + fn virtual_assign_phase1( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + (block_witness, tx_witness, chip_params): Self::FirstPhasePayload, + ) { + let chip = EthTransactionChip::new(mpt, chip_params); + chip.parse_transaction_proof_from_block_phase1( + builder.rlc_ctx_pair(), + block_witness, + tx_witness, + ); + } +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize)] +pub struct TxFieldProviderInput { + pub idxs: Vec<(usize, usize)>, + pub block_number: usize, +} + +fn get_test_field_circuit( + network: Chain, + idxs: Vec<(usize, usize)>, + block_number: usize, + max_data_byte_len: usize, + max_access_list_len: usize, + enable_types: [bool; 3], +) -> EthBlockTransactionFieldCircuit { + assert!(idxs.len() == 1); + let provider = setup_provider(network); + + EthBlockTransactionFieldCircuit::from_provider( + &provider, + idxs, + block_number.try_into().unwrap(), + 6, + network, + max_data_byte_len, + max_access_list_len, + enable_types, + false, + ) +} + +pub fn test_field_valid_input_json( + path: String, + max_data_byte_len: usize, + max_access_list_len: usize, + enable_types: [bool; 3], +) { + let file_inputs: TxFieldProviderInput = + serde_json::from_reader(File::open(path).expect("path does not exist")).unwrap(); + let idxs = file_inputs.idxs; + let block_number = file_inputs.block_number; + test_field_valid_input_direct( + idxs, + block_number, + max_data_byte_len, + max_access_list_len, + enable_types, + ); +} + +pub fn test_field_valid_input_direct( + idxs: Vec<(usize, usize)>, + block_number: usize, + max_data_byte_len: usize, + max_access_list_len: usize, + enable_types: [bool; 3], +) { + let params = get_rlc_params("configs/tests/transaction.json"); + let k = params.base.k as u32; + + let input = get_test_field_circuit( + Chain::Mainnet, + idxs, + block_number, + max_data_byte_len, + max_access_list_len, + enable_types, + ); + let mut circuit = create_circuit(CircuitBuilderStage::Mock, params, input); + circuit.mock_fulfill_keccak_promises(None); + circuit.calculate_params(); + let instances = circuit.instances(); + MockProver::run(k, &circuit, instances).unwrap().assert_satisfied(); +} + +#[test] +pub fn test_mock_single_field_legacy() { + test_field_valid_input_direct(vec![(257, 0 /*nonce*/)], 5000008, 256, 0, [true, false, false]); +} + +#[test] +pub fn test_mock_single_field_legacy_json() { + test_field_valid_input_json( + "src/transaction/tests/data/field/single_tx_pos_test_legacy.json".to_string(), + 256, + 0, + [true, false, false], + ); +} + +#[test] +pub fn test_mock_single_field_new_json() { + test_field_valid_input_json( + "src/transaction/tests/data/field/single_tx_pos_test_new.json".to_string(), + 256, + 512, + [true, false, true], + ); +} diff --git a/axiom-eth/src/transaction/tests/mod.rs b/axiom-eth/src/transaction/tests/mod.rs new file mode 100644 index 00000000..20c41395 --- /dev/null +++ b/axiom-eth/src/transaction/tests/mod.rs @@ -0,0 +1,458 @@ +#![cfg(feature = "providers")] +use crate::providers::block::get_block_rlp_from_num; +use crate::providers::setup_provider; +use crate::rlc::circuit::RlcCircuitParams; +use crate::rlc::tests::get_rlc_params; +use crate::utils::eth_circuit::{create_circuit, EthCircuitInstructions, EthCircuitParams}; + +use super::*; +use ark_std::{end_timer, start_timer}; +use halo2_base::gates::circuit::{BaseCircuitParams, CircuitBuilderStage}; +use halo2_base::halo2_proofs::dev::MockProver; + +use ethers_providers::{JsonRpcClient, Provider}; +use halo2_base::halo2_proofs::halo2curves::bn256::Fr; +use halo2_base::halo2_proofs::plonk::{keygen_pk, keygen_vk, Circuit}; +use halo2_base::utils::fs::gen_srs; +use halo2_base::utils::testing::{check_proof_with_instances, gen_proof_with_instances}; +use serde::{Deserialize, Serialize}; +use std::marker::PhantomData; +use std::{fs::File, io::Write}; +use test_log::test; + +pub mod field; + +#[derive(Clone, Debug)] +pub struct EthBlockTransactionCircuit { + pub inputs: EthBlockTransactionsInput, // public and private inputs + pub params: EthTransactionChipParams, + _marker: PhantomData, +} + +impl EthBlockTransactionCircuit { + #[allow(clippy::too_many_arguments)] + #[cfg(feature = "providers")] + pub fn from_provider( + provider: &Provider

, + idxs: Vec, + block_number: u32, + transaction_pf_max_depth: usize, + network: Chain, + max_data_byte_len: usize, + max_access_list_len: usize, + enable_types: [bool; 3], + constrain_len: bool, + ) -> Self { + use crate::providers::transaction::get_block_transaction_input; + + let inputs = get_block_transaction_input( + provider, + idxs, + block_number, + transaction_pf_max_depth, + max_data_byte_len, + max_access_list_len, + enable_types, + constrain_len, + ); + let params = EthTransactionChipParams { + max_data_byte_len, + max_access_list_len, + enable_types, + network: Some(network), + }; + Self { inputs, params, _marker: PhantomData } + } +} + +impl EthCircuitInstructions for EthBlockTransactionCircuit { + type FirstPhasePayload = (EthBlockTransactionsWitness, EthTransactionChipParams); + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + ) -> Self::FirstPhasePayload { + let ctx = builder.base.main(FIRST_PHASE); + let chip = EthTransactionChip::new(mpt, self.params); + let input = self.inputs.clone().assign(ctx, chip.network().unwrap()); + let witness = chip.parse_transaction_proofs_from_block_phase0(builder, input); + (witness, self.params) + } + fn virtual_assign_phase1( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + (witness, chip_params): Self::FirstPhasePayload, + ) { + let chip = EthTransactionChip::new(mpt, chip_params); + chip.parse_transaction_proofs_from_block_phase1(builder, witness); + } +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize)] +pub struct TxProviderInput { + pub idxs: Vec, + pub block_number: usize, +} + +fn get_test_circuit( + network: Chain, + idxs: Vec, + block_number: usize, + max_data_byte_len: usize, + max_access_list_len: usize, + enable_types: [bool; 3], + constrain_len: bool, +) -> EthBlockTransactionCircuit { + let provider = setup_provider(network); + + EthBlockTransactionCircuit::from_provider( + &provider, + idxs, + block_number.try_into().unwrap(), + 6, + network, + max_data_byte_len, + max_access_list_len, + enable_types, + constrain_len, + ) +} + +pub fn test_valid_input_json( + path: String, + max_data_byte_len: usize, + max_access_list_len: usize, + enable_types: [bool; 3], + constrain_len: bool, +) { + let file_inputs: TxProviderInput = + serde_json::from_reader(File::open(path).expect("path does not exist")).unwrap(); + let idxs = file_inputs.idxs; + let block_number = file_inputs.block_number; + test_valid_input_direct( + idxs, + block_number, + max_data_byte_len, + max_access_list_len, + enable_types, + constrain_len, + ); +} + +pub fn test_valid_input_direct( + idxs: Vec, + block_number: usize, + max_data_byte_len: usize, + max_access_list_len: usize, + enable_types: [bool; 3], + constrain_len: bool, +) { + let params = get_rlc_params("configs/tests/transaction.json"); + let k = params.base.k as u32; + + let input = get_test_circuit( + Chain::Mainnet, + idxs, + block_number, + max_data_byte_len, + max_access_list_len, + enable_types, + constrain_len, + ); + let mut circuit = create_circuit(CircuitBuilderStage::Mock, params, input); + circuit.mock_fulfill_keccak_promises(None); + circuit.calculate_params(); + let instances = circuit.instances(); + MockProver::run(k, &circuit, instances).unwrap().assert_satisfied(); +} + +#[test] +pub fn test_mock_single_tx_legacy() { + test_valid_input_json( + "src/transaction/tests/data/single_tx_pos_test_legacy.json".to_string(), + 256, + 0, + [true, false, false], + false, + ); +} + +#[test] +pub fn test_mock_multi_tx_legacy() { + test_valid_input_json( + "src/transaction/tests/data/multi_tx_pos_test_legacy.json".to_string(), + 256, + 0, + [true, false, false], + false, + ); +} + +#[test] +pub fn test_mock_zero_tx_legacy() { + test_valid_input_json( + "src/transaction/tests/data/zero_tx_pos_test_legacy.json".to_string(), + 256, + 0, + [true, false, false], + false, + ); +} + +#[test] +pub fn test_mock_single_tx_new() { + test_valid_input_json( + "src/transaction/tests/data/single_tx_pos_test_new.json".to_string(), + 256, + 512, + [true, true, true], + false, + ); +} + +#[test] +pub fn test_mock_multi_tx_new() { + test_valid_input_json( + "src/transaction/tests/data/multi_tx_pos_test_new.json".to_string(), + 256, + 512, + [true, false, true], + false, + ); +} + +#[test] +pub fn stress_test() { + let tx_num: usize = serde_json::from_reader( + File::open("src/transaction/tests/data/stress_test.json").expect("path does not exist"), + ) + .unwrap(); + let mut idxs = Vec::new(); + for i in 0..tx_num { + idxs.push(i); + } + return test_valid_input_direct(idxs, 5000008, 256, 0, [true, false, false], false); +} + +#[test] +pub fn test_invalid_block_header() { + let params = get_rlc_params("configs/tests/transaction.json"); + let file_inputs: TxProviderInput = serde_json::from_reader( + File::open("src/transaction/tests/data/multi_tx_pos_test_legacy.json") + .expect("path does not exist"), + ) + .unwrap(); + let idxs = file_inputs.idxs; + let block_number = file_inputs.block_number; + let k = params.base.k as u32; + + let mut input = + get_test_circuit(Chain::Mainnet, idxs, block_number, 256, 0, [true, false, false], false); + let provider = setup_provider(Chain::Mainnet); + let new_block_header = get_block_rlp_from_num(&provider, 1000000); + input.inputs.block_header = new_block_header; + let mut circuit = create_circuit(CircuitBuilderStage::Mock, params, input); + circuit.mock_fulfill_keccak_promises(None); + circuit.calculate_params(); + let instances = circuit.instances(); + let prover = MockProver::run(k, &circuit, instances).unwrap(); + assert!(prover.verify().is_err()); +} + +/* // ignore for now because an assert fails +#[test] +pub fn test_valid_root_wrong_block_header() -> Result<(), Box> { + let params = EthConfigParams::from_path("configs/tests/transaction.json"); + let file_inputs: TxProviderInput = serde_json::from_reader( + File::open("src/transaction/tests/data/multi_tx_pos_test_legacy.json") + .expect("path does not exist"), + ) + .unwrap(); + let idxs = file_inputs.idxs; + let block_number = file_inputs.block_number; + let k = params.degree; + + let mut input = + get_test_circuit(Chain::Mainnet, idxs, block_number, 256, 0, [true, false, false], false); + let provider = setup_provider(Chain::Mainnet); + let blocks = get_blocks(&provider, vec![block_number as u64 + 1]).unwrap(); + let block = blocks[0].clone(); + match block { + None => Ok(()), + Some(mut _block) => { + _block.transactions_root = input.inputs.tx_proofs[0].proof.root_hash; + let new_block_header = get_block_rlp(&_block); // panics because block hash fails + input.inputs.block_header = new_block_header; + let circuit = input.create_circuit(RlcThreadBuilder::mock(), params, None); + MockProver::run(k, &circuit, vec![circuit.instance()]).unwrap().assert_satisfied(); + Ok(()) + } + } +} +*/ + +// Tests if the key = rlp(idx) is correctly constrained +#[test] +pub fn test_invalid_key() { + let params = get_rlc_params("configs/tests/transaction.json"); + let confused_pairs = [(1, 256), (256, 1), (1, 0), (0, 1), (0, 256), (256, 0)]; + let block_number = 5000050; + let k = params.base.k as u32; + for (idx, tx_index) in confused_pairs { + let idxs = vec![idx]; + let mut input = get_test_circuit( + Chain::Mainnet, + idxs, + block_number, + 256, + 0, + [true, false, false], + false, + ); + input.inputs.tx_proofs[0].tx_index = tx_index; + let mut circuit = create_circuit(CircuitBuilderStage::Mock, params.clone(), input); + circuit.mock_fulfill_keccak_promises(None); + circuit.calculate_params(); + let instances = circuit.instances(); + let prover = MockProver::run(k, &circuit, instances).unwrap(); + assert!(prover.verify().is_err(), "Should not have verified"); + } +} + +#[test] +pub fn test_mock_single_tx_len_legacy() { + test_valid_input_json( + "src/transaction/tests/data/single_tx_pos_test_legacy.json".to_string(), + 256, + 0, + [true, false, false], + true, + ); +} + +#[test] +pub fn test_mock_multi_tx_len_legacy() { + test_valid_input_json( + "src/transaction/tests/data/multi_tx_pos_test_legacy.json".to_string(), + 256, + 0, + [true, false, false], + true, + ); +} + +#[test] +pub fn test_mock_zero_len_new() { + test_valid_input_direct([].into(), 1000, 256, 512, [true, false, true], true); +} + +#[test] +pub fn test_mock_zero_len_legacy() { + test_valid_input_direct([].into(), 1000, 256, 512, [true, false, false], true); +} + +#[test] +pub fn test_mock_one_len_new() { + test_valid_input_direct([].into(), 3482144, 256, 512, [true, false, true], true); +} + +#[test] +pub fn test_mock_one_len_legacy() { + test_valid_input_direct([0].into(), 3482144, 256, 512, [true, false, false], true); +} + +#[test] +pub fn test_mock_nonzero_len_new() { + test_valid_input_direct([].into(), 5000008, 256, 512, [true, false, true], true); +} + +#[test] +pub fn test_mock_nonzero_len_legacy() { + test_valid_input_direct([].into(), 5000008, 256, 512, [true, false, false], true); +} + +#[derive(Serialize, Deserialize)] +struct BenchParams(RlcCircuitParams, usize); // (params, num_slots) + +#[test] +#[ignore = "bench"] +pub fn bench_tx() -> Result<(), Box> { + let bench_params_file = File::create("configs/bench/transaction.json").unwrap(); + std::fs::create_dir_all("data/transaction")?; + let mut fs_results = File::create("data/bench/transaction.csv").unwrap(); + writeln!(fs_results, "degree,num_slots,total_advice,num_rlc_columns,num_advice,num_lookup,num_fixed,proof_time,verify_time")?; + + let mut all_bench_params = vec![]; + let bench_k_num = vec![(15, 1), (17, 10), (18, 32)]; + for (k, num_slots) in bench_k_num { + println!("---------------------- degree = {k} ------------------------------",); + let input = get_test_circuit( + Chain::Mainnet, + (0..num_slots).collect(), + 5000008, + 256, + 0, + [true, false, true], + false, + ); + let mut dummy_params = EthCircuitParams::default().rlc; + dummy_params.base.k = k; + let mut circuit = create_circuit(CircuitBuilderStage::Keygen, dummy_params, input.clone()); + circuit.mock_fulfill_keccak_promises(None); + circuit.calculate_params(); + + let params = gen_srs(k as u32); + let vk = keygen_vk(¶ms, &circuit)?; + let pk = keygen_pk(¶ms, vk, &circuit)?; + let bench_params = circuit.params().rlc; + let break_points = circuit.break_points(); + + // create a proof + let proof_time = start_timer!(|| "create proof SHPLONK"); + let phase0_time = start_timer!(|| "phase 0 synthesize"); + let circuit = create_circuit(CircuitBuilderStage::Prover, bench_params.clone(), input) + .use_break_points(break_points); + circuit.mock_fulfill_keccak_promises(None); + let instances = circuit.instances(); + end_timer!(phase0_time); + assert_eq!(instances.len(), 1); + let proof = gen_proof_with_instances(¶ms, &pk, circuit, &[&instances[0]]); + end_timer!(proof_time); + + let verify_time = start_timer!(|| "Verify time"); + check_proof_with_instances(¶ms, pk.get_vk(), &proof, &[&instances[0]], true); + end_timer!(verify_time); + + let RlcCircuitParams { + base: + BaseCircuitParams { + k, + num_advice_per_phase, + num_fixed, + num_lookup_advice_per_phase, + .. + }, + num_rlc_columns, + } = bench_params.clone(); + writeln!( + fs_results, + "{},{},{},{},{:?},{:?},{},{:.2}s,{:?}", + k, + num_slots, + num_rlc_columns + + num_advice_per_phase.iter().sum::() + + num_lookup_advice_per_phase.iter().sum::(), + num_rlc_columns, + num_advice_per_phase, + num_lookup_advice_per_phase, + num_fixed, + proof_time.time.elapsed().as_secs_f64(), + verify_time.time.elapsed() + ) + .unwrap(); + all_bench_params.push(BenchParams(bench_params, num_slots)); + } + serde_json::to_writer_pretty(bench_params_file, &all_bench_params).unwrap(); + Ok(()) +} diff --git a/axiom-eth/src/transaction/types.rs b/axiom-eth/src/transaction/types.rs new file mode 100644 index 00000000..11f3a92c --- /dev/null +++ b/axiom-eth/src/transaction/types.rs @@ -0,0 +1,133 @@ +use getset::Getters; + +use crate::{ + rlp::types::{RlpArrayWitness, RlpFieldTrace, RlpFieldWitness}, + utils::assign_vec, +}; + +use super::*; + +/// Assigned version of [`EthTransactionInput`] +#[derive(Clone, Debug)] +pub struct EthTransactionInputAssigned { + /// idx is the transaction index, varying from 0 to around 500 + pub transaction_index: AssignedValue, + pub proof: MPTProof, +} + +#[derive(Clone, Debug, Getters)] +pub struct EthTransactionWitness { + pub transaction_type: AssignedValue, + pub idx: AssignedValue, + pub(crate) idx_witness: RlpFieldWitness, + #[getset(get = "pub")] + pub(crate) value_witness: RlpArrayWitness, + #[getset(get = "pub")] + pub(crate) mpt_witness: MPTProofWitness, +} + +#[derive(Clone, Debug)] +pub struct EthTransactionTrace { + pub transaction_type: AssignedValue, + pub value_trace: Vec>, +} + +/// Container for extracting a specific field from a single transaction +#[derive(Clone, Debug)] +pub struct EthTransactionFieldWitness { + pub transaction_witness: EthTransactionWitness, + pub transaction_type: AssignedValue, + pub field_idx: AssignedValue, + pub field_bytes: Vec>, + pub len: AssignedValue, + pub max_len: usize, +} + +/// Assigned version of [`EthBlockTransactionsInput`] +#[derive(Clone, Debug)] +pub struct EthBlockTransactionsInputAssigned { + // block_hash: AssignedH256, // H256 as (u128, u128) + pub block_header: Vec>, + pub tx_inputs: Vec>, + // Inclusion and noninclusion proof pair of neighboring indices + pub len_proof: Option<[EthTransactionInputAssigned; 2]>, +} + +#[derive(Clone, Debug)] +pub struct EthBlockTransactionsWitness { + pub block_witness: EthBlockHeaderWitness, + pub transaction_witness: Vec>, + pub len: Option>, + pub len_witness: Option<[EthTransactionWitness; 2]>, +} + +#[derive(Clone, Debug)] +pub struct EthBlockTransactionsTrace { + pub block_trace: EthBlockHeaderTrace, + pub transaction_trace: Vec>, + pub len: Option>, + pub len_trace: Option>>, +} + +// rust native types + +#[derive(Clone, Debug, Hash, Serialize, Deserialize)] +pub struct EthTransactionProof { + pub tx_index: usize, + pub proof: MPTInput, +} + +/// Used to prove total number of transactions in a block +#[derive(Clone, Debug, Hash, Serialize, Deserialize)] +pub struct EthTransactionLenProof { + pub inclusion: EthTransactionProof, + pub noninclusion: EthTransactionProof, +} + +#[derive(Clone, Debug, Hash, Serialize, Deserialize)] +/// Contains an index, a proof, the desired field_idx to be queried. +pub struct EthTransactionFieldInput { + pub transaction_index: usize, + pub proof: MPTInput, + pub field_idx: usize, +} + +/// Contains block information, multiple `EthTransactionInput` for transactions we want to prove in the block, +/// and a `constrain_len` flag that decides whether the number of transactions in the block should be proved as well. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct EthBlockTransactionsInput { + pub block: Block, + pub block_number: u32, + pub block_hash: H256, // provided for convenience, actual block_hash is computed from block_header + pub block_header: Vec, + pub tx_proofs: Vec, + /// Inclusion and noninclusion proof pair of neighboring indices + pub len_proof: Option, +} + +impl EthTransactionProof { + pub fn assign(self, ctx: &mut Context) -> EthTransactionInputAssigned { + // let block_hash = encode_h256_to_field(&self.block_hash); + // let block_hash = block_hash.map(|block_hash| ctx.load_witness(block_hash)); + let transaction_index = ctx.load_witness(F::from(self.tx_index as u64)); + let proof = self.proof.assign(ctx); + EthTransactionInputAssigned { transaction_index, proof } + } +} + +impl EthBlockTransactionsInput { + pub fn assign( + self, + ctx: &mut Context, + network: Chain, + ) -> EthBlockTransactionsInputAssigned { + // let block_hash = encode_h256_to_field(&self.block_hash); + // let block_hash = block_hash.map(|block_hash| ctx.load_witness(block_hash)); + let tx_inputs = self.tx_proofs.into_iter().map(|pf| pf.assign(ctx)).collect(); + let len_proof = + self.len_proof.map(|pf| [pf.inclusion.assign(ctx), pf.noninclusion.assign(ctx)]); + let max_len = get_block_header_rlp_max_lens(network).0; + let block_header = assign_vec(ctx, self.block_header, max_len); + EthBlockTransactionsInputAssigned { block_header, tx_inputs, len_proof } + } +} diff --git a/axiom-eth/src/util/circuit.rs b/axiom-eth/src/util/circuit.rs deleted file mode 100644 index 13f8666b..00000000 --- a/axiom-eth/src/util/circuit.rs +++ /dev/null @@ -1,332 +0,0 @@ -use super::{AggregationConfigPinning, EthConfigPinning, Halo2ConfigPinning}; -use crate::{ - keccak::FnSynthesize, - rlp::builder::{RlcThreadBreakPoints, RlcThreadBuilder}, - EthCircuitBuilder, EthPreCircuit, Field, -}; -use halo2_base::{ - gates::builder::{ - CircuitBuilderStage, MultiPhaseThreadBreakPoints, RangeWithInstanceCircuitBuilder, - }, - halo2_proofs::{ - halo2curves::bn256::{Bn256, Fr, G1Affine}, - plonk::{Circuit, ProvingKey, VerifyingKey}, - poly::{commitment::Params, kzg::commitment::ParamsKZG}, - }, - utils::ScalarField, -}; -#[cfg(feature = "evm")] -use snark_verifier_sdk::evm::{gen_evm_proof_shplonk, gen_evm_verifier_shplonk}; -use snark_verifier_sdk::{ - gen_pk, - halo2::{aggregation::AggregationCircuit, gen_snark_shplonk}, - read_pk, CircuitExt, Snark, LIMBS, SHPLONK, -}; -use std::{env::var, fs::File, path::Path}; - -pub trait PinnableCircuit: CircuitExt { - type Pinning: Halo2ConfigPinning; - - fn break_points(&self) -> ::BreakPoints; - - fn write_pinning(&self, path: impl AsRef) { - let break_points = self.break_points(); - let pinning: Self::Pinning = Halo2ConfigPinning::from_var(break_points); - serde_json::to_writer_pretty(File::create(path).unwrap(), &pinning).unwrap(); - } -} - -impl> PinnableCircuit for EthCircuitBuilder { - type Pinning = EthConfigPinning; - - fn break_points(&self) -> RlcThreadBreakPoints { - self.circuit.break_points.borrow().clone() - } -} - -impl PinnableCircuit for AggregationCircuit { - type Pinning = AggregationConfigPinning; - - fn break_points(&self) -> MultiPhaseThreadBreakPoints { - AggregationCircuit::break_points(self) - } -} - -impl PinnableCircuit for RangeWithInstanceCircuitBuilder { - type Pinning = AggregationConfigPinning; - - fn break_points(&self) -> MultiPhaseThreadBreakPoints { - RangeWithInstanceCircuitBuilder::break_points(self) - } -} - -/// Common functionality we want to get out of any kind of circuit. -/// In particular used for types that hold multiple `PreCircuit`s. -pub trait AnyCircuit: Sized { - fn read_or_create_pk( - self, - params: &ParamsKZG, - pk_path: impl AsRef, - pinning_path: impl AsRef, - read_only: bool, - ) -> ProvingKey; - - fn gen_snark_shplonk( - self, - params: &ParamsKZG, - pk: &ProvingKey, - pinning_path: impl AsRef, - path: Option>, - ) -> Snark; - - #[cfg(feature = "evm")] - fn gen_evm_verifier_shplonk( - self, - params: &ParamsKZG, - pk: &ProvingKey, - yul_path: impl AsRef, - ) -> Vec; - - #[cfg(feature = "evm")] - fn gen_calldata( - self, - params: &ParamsKZG, - pk: &ProvingKey, - pinning_path: impl AsRef, - path: impl AsRef, - deployment_code: Option>, - ) -> String; -} - -pub trait PreCircuit: Sized { - type Pinning: Halo2ConfigPinning; - - /// Creates a [`PinnableCircuit`], auto-configuring the circuit if not in production or prover mode. - /// - /// `params` should be the universal trusted setup for the present aggregation circuit. - /// We assume the trusted setup for the previous SNARKs is compatible with `params` in the sense that - /// the generator point and toxic waste `tau` are the same. - fn create_circuit( - self, - stage: CircuitBuilderStage, - pinning: Option, - params: &ParamsKZG, - ) -> impl PinnableCircuit; - - /// Reads the proving key for the pre-circuit. - /// If `read_only` is true, then it is assumed that the proving key exists and can be read from `path` (otherwise the program will panic). - fn read_pk(self, params: &ParamsKZG, path: impl AsRef) -> ProvingKey { - let circuit = self.create_circuit(CircuitBuilderStage::Keygen, None, params); - custom_read_pk(path, &circuit) - } - - /// Creates the proving key for the pre-circuit if file at `pk_path` is not found. - /// If a new proving key is created, the new pinning data is written to `pinning_path`. - fn create_pk( - self, - params: &ParamsKZG, - pk_path: impl AsRef, - pinning_path: impl AsRef, - ) -> ProvingKey { - let circuit = self.create_circuit(CircuitBuilderStage::Keygen, None, params); - let pk_exists = pk_path.as_ref().exists(); - let pk = gen_pk(params, &circuit, Some(pk_path.as_ref())); - if !pk_exists { - // should only write pinning data if we created a new pkey - circuit.write_pinning(pinning_path); - } - pk - } - - fn get_degree(pinning_path: impl AsRef) -> u32 { - let pinning = Self::Pinning::from_path(pinning_path); - pinning.degree() - } -} - -impl PreCircuit for C { - type Pinning = EthConfigPinning; - - fn create_circuit( - self, - stage: CircuitBuilderStage, - pinning: Option, - _: &ParamsKZG, - ) -> impl PinnableCircuit { - let builder = RlcThreadBuilder::from_stage(stage); - let break_points = pinning.map(|p| p.break_points()); - EthPreCircuit::create_circuit(self, builder, break_points) - } -} - -impl AnyCircuit for C { - fn read_or_create_pk( - self, - params: &ParamsKZG, - pk_path: impl AsRef, - pinning_path: impl AsRef, - read_only: bool, - ) -> ProvingKey { - if read_only { - self.read_pk(params, pk_path) - } else { - self.create_pk(params, pk_path, pinning_path) - } - } - - fn gen_snark_shplonk( - self, - params: &ParamsKZG, - pk: &ProvingKey, - pinning_path: impl AsRef, - path: Option>, - ) -> Snark { - let pinning = C::Pinning::from_path(pinning_path); - let circuit = self.create_circuit(CircuitBuilderStage::Prover, Some(pinning), params); - gen_snark_shplonk(params, pk, circuit, path) - } - - #[cfg(feature = "evm")] - fn gen_evm_verifier_shplonk( - self, - params: &ParamsKZG, - pk: &ProvingKey, - yul_path: impl AsRef, - ) -> Vec { - let circuit = self.create_circuit(CircuitBuilderStage::Keygen, None, params); - custom_gen_evm_verifier_shplonk(params, pk.get_vk(), &circuit, Some(yul_path)) - } - - #[cfg(feature = "evm")] - fn gen_calldata( - self, - params: &ParamsKZG, - pk: &ProvingKey, - pinning_path: impl AsRef, - path: impl AsRef, - deployment_code: Option>, - ) -> String { - let pinning = C::Pinning::from_path(pinning_path); - let circuit = self.create_circuit(CircuitBuilderStage::Prover, Some(pinning), params); - write_calldata_generic(params, pk, circuit, path, deployment_code) - } -} - -/// Aggregates snarks and re-exposes previous public inputs. -/// -#[derive(Clone, Debug)] -pub struct PublicAggregationCircuit { - /// The previous snarks to aggregate. - /// `snarks` consists of a vector of `(snark, has_prev_accumulator)` pairs, where `snark` is [Snark] and `has_prev_accumulator` is boolean. If `has_prev_accumulator` is true, then it assumes `snark` is already an - /// aggregation circuit and does not re-expose the old accumulator from `snark` as public inputs. - pub snarks: Vec<(Snark, bool)>, -} - -impl PublicAggregationCircuit { - pub fn new(snarks: Vec<(Snark, bool)>) -> Self { - Self { snarks } - } - - // excludes old accumulators from prev instance - pub fn private( - self, - stage: CircuitBuilderStage, - break_points: Option, - lookup_bits: usize, - params: &ParamsKZG, - ) -> AggregationCircuit { - let (snarks, has_prev_acc): (Vec<_>, Vec<_>) = self.snarks.into_iter().unzip(); - let mut private = - AggregationCircuit::new::(stage, break_points, lookup_bits, params, snarks); - for (prev_instance, has_acc) in private.previous_instances.iter_mut().zip(has_prev_acc) { - let start = (has_acc as usize) * 4 * LIMBS; - *prev_instance = prev_instance.split_off(start); - } - private - } -} - -impl PreCircuit for PublicAggregationCircuit { - type Pinning = AggregationConfigPinning; - - fn create_circuit( - self, - stage: CircuitBuilderStage, - pinning: Option, - params: &ParamsKZG, - ) -> impl PinnableCircuit { - // look for lookup_bits either from pinning, if available, or from env var - let lookup_bits = pinning - .as_ref() - .map(|p| p.params.lookup_bits) - .or_else(|| var("LOOKUP_BITS").map(|v| v.parse().unwrap()).ok()) - .expect("LOOKUP_BITS is not set"); - let break_points = pinning.map(|p| p.break_points()); - let mut private = self.private(stage, break_points, lookup_bits, params); - for prev in &private.previous_instances { - private.inner.assigned_instances.extend_from_slice(prev); - } - - #[cfg(not(feature = "production"))] - match stage { - CircuitBuilderStage::Prover => {} - _ => { - private.config( - params.k(), - Some(var("MINIMUM_ROWS").unwrap_or_else(|_| "10".to_string()).parse().unwrap()), - ); - } - } - private - } -} - -#[cfg(feature = "evm")] -pub fn write_calldata_generic>( - params: &ParamsKZG, - pk: &ProvingKey, - circuit: ConcreteCircuit, - path: impl AsRef, - deployment_code: Option>, -) -> String { - use ethers_core::utils::hex::encode; - use snark_verifier::loader::evm::encode_calldata; - use snark_verifier_sdk::evm::evm_verify; - use std::fs; - - let instances = circuit.instances(); - let proof = gen_evm_proof_shplonk(params, pk, circuit, instances.clone()); - // calldata as hex string - let calldata = encode(encode_calldata(&instances, &proof)); - fs::write(path, &calldata).expect("write calldata should not fail"); - if let Some(deployment_code) = deployment_code { - evm_verify(deployment_code, instances, proof); - } - calldata -} - -// need to trick rust into inferring type of the circuit because `C` involves closures -// this is not ideal... -fn custom_read_pk(fname: P, _: &C) -> ProvingKey -where - C: Circuit, - P: AsRef, -{ - read_pk::(fname.as_ref()).expect("proving key should exist") -} - -// also for type inference -#[cfg(feature = "evm")] -pub fn custom_gen_evm_verifier_shplonk>( - params: &ParamsKZG, - vk: &VerifyingKey, - circuit: &C, - path: Option>, -) -> Vec { - gen_evm_verifier_shplonk::( - params, - vk, - circuit.num_instance(), - path.as_ref().map(|p| p.as_ref()), - ) -} diff --git a/axiom-eth/src/util/mod.rs b/axiom-eth/src/util/mod.rs deleted file mode 100644 index fd16f36a..00000000 --- a/axiom-eth/src/util/mod.rs +++ /dev/null @@ -1,502 +0,0 @@ -use super::Field; -use crate::{rlp::builder::RlcThreadBreakPoints, ETH_LOOKUP_BITS}; -use ethers_core::{ - types::{Address, H256, U256}, - utils::keccak256, -}; -use halo2_base::{ - gates::{ - builder::{FlexGateConfigParams, MultiPhaseThreadBreakPoints}, - flex_gate::GateStrategy, - GateInstructions, RangeChip, RangeInstructions, - }, - utils::{bit_length, decompose, decompose_fe_to_u64_limbs, BigPrimeField, ScalarField}, - AssignedValue, Context, - QuantumCell::{Constant, Existing, Witness}, -}; -use itertools::Itertools; -use serde::{Deserialize, Serialize}; -use snark_verifier_sdk::halo2::aggregation::AggregationConfigParams; -use std::{ - env::{set_var, var}, - fs::File, - iter, - path::Path, -}; - -#[cfg(feature = "aggregation")] -pub mod circuit; -#[cfg(feature = "aggregation")] -pub mod scheduler; - -pub(crate) const NUM_BYTES_IN_U128: usize = 16; - -pub type AssignedH256 = [AssignedValue; 2]; // H256 as hi-lo (u128, u128) - -#[derive(Default, Clone, Debug, Serialize, Deserialize)] -pub struct EthConfigParams { - pub degree: u32, - // number of SecondPhase advice columns used in RlcConfig - pub num_rlc_columns: usize, - // the number of advice columns in phase _ without lookup enabled that RangeConfig uses - pub num_range_advice: Vec, - // the number of advice columns in phase _ with lookup enabled that RangeConfig uses - pub num_lookup_advice: Vec, - pub num_fixed: usize, - // for keccak chip you should know the number of unusable rows beforehand - pub unusable_rows: usize, - pub keccak_rows_per_round: usize, - #[serde(skip_serializing_if = "Option::is_none")] - pub lookup_bits: Option, -} - -impl EthConfigParams { - pub fn from_path>(path: P) -> Self { - serde_json::from_reader(File::open(&path).expect("path does not exist")).unwrap() - } -} - -pub trait Halo2ConfigPinning: Serialize { - type BreakPoints; - /// Loads configuration parameters from a file and sets environmental variables. - fn from_path>(path: P) -> Self; - /// Loads configuration parameters into environment variables. - fn set_var(&self); - /// Returns break points - fn break_points(self) -> Self::BreakPoints; - /// Constructs `Self` from environmental variables and break points - fn from_var(break_points: Self::BreakPoints) -> Self; - /// Degree of the circuit, log_2(number of rows) - fn degree(&self) -> u32; -} - -#[derive(Default, Clone, Debug, Serialize, Deserialize)] -pub struct EthConfigPinning { - pub params: EthConfigParams, - pub break_points: RlcThreadBreakPoints, -} - -impl Halo2ConfigPinning for EthConfigPinning { - type BreakPoints = RlcThreadBreakPoints; - - fn from_path>(path: P) -> Self { - let pinning: Self = serde_json::from_reader( - File::open(&path) - .unwrap_or_else(|e| panic!("{:?} does not exist: {e:?}", path.as_ref())), - ) - .unwrap(); - pinning.set_var(); - pinning - } - - fn set_var(&self) { - set_var("ETH_CONFIG_PARAMS", serde_json::to_string(&self.params).unwrap()); - set_var("KECCAK_ROWS", self.params.keccak_rows_per_round.to_string()); - let bits = self.params.lookup_bits.unwrap_or(ETH_LOOKUP_BITS); - set_var("LOOKUP_BITS", bits.to_string()); - } - - fn break_points(self) -> RlcThreadBreakPoints { - self.break_points - } - - fn from_var(break_points: RlcThreadBreakPoints) -> Self { - let params: EthConfigParams = - serde_json::from_str(&var("ETH_CONFIG_PARAMS").unwrap()).unwrap(); - Self { params, break_points } - } - - fn degree(&self) -> u32 { - self.params.degree - } -} - -#[derive(Serialize, Deserialize)] -pub struct AggregationConfigPinning { - pub params: AggregationConfigParams, - pub break_points: MultiPhaseThreadBreakPoints, -} - -impl Halo2ConfigPinning for AggregationConfigPinning { - type BreakPoints = MultiPhaseThreadBreakPoints; - - fn from_path>(path: P) -> Self { - let pinning: Self = serde_json::from_reader( - File::open(&path) - .unwrap_or_else(|e| panic!("{:?} does not exist: {e:?}", path.as_ref())), - ) - .unwrap(); - pinning.set_var(); - pinning - } - - fn set_var(&self) { - let gate_params = FlexGateConfigParams { - k: self.params.degree as usize, - num_advice_per_phase: vec![self.params.num_advice], - num_lookup_advice_per_phase: vec![self.params.num_lookup_advice], - strategy: GateStrategy::Vertical, - num_fixed: self.params.num_fixed, - }; - set_var("FLEX_GATE_CONFIG_PARAMS", serde_json::to_string(&gate_params).unwrap()); - set_var("LOOKUP_BITS", self.params.lookup_bits.to_string()); - } - - fn break_points(self) -> MultiPhaseThreadBreakPoints { - self.break_points - } - - fn from_var(break_points: MultiPhaseThreadBreakPoints) -> Self { - let params: FlexGateConfigParams = - serde_json::from_str(&var("FLEX_GATE_CONFIG_PARAMS").unwrap()).unwrap(); - let lookup_bits = var("LOOKUP_BITS").unwrap().parse().unwrap(); - Self { - params: AggregationConfigParams { - degree: params.k as u32, - num_advice: params.num_advice_per_phase[0], - num_lookup_advice: params.num_lookup_advice_per_phase[0], - num_fixed: params.num_fixed, - lookup_bits, - }, - break_points, - } - } - - fn degree(&self) -> u32 { - self.params.degree - } -} - -pub fn get_merkle_mountain_range(leaves: &[H256], max_depth: usize) -> Vec { - let num_leaves = leaves.len(); - let mut merkle_roots = Vec::with_capacity(max_depth + 1); - let mut start_idx = 0; - for depth in (0..max_depth + 1).rev() { - if (num_leaves >> depth) & 1 == 1 { - merkle_roots.push(h256_tree_root(&leaves[start_idx..start_idx + (1 << depth)])); - start_idx += 1 << depth; - } else { - merkle_roots.push(H256::zero()); - } - } - merkle_roots -} - -/// # Assumptions -/// * `leaves` should not be empty -pub fn h256_tree_root(leaves: &[H256]) -> H256 { - assert!(!leaves.is_empty(), "leaves should not be empty"); - let depth = leaves.len().ilog2(); - assert_eq!(leaves.len(), 1 << depth); - if depth == 0 { - return leaves[0]; - } - keccak256_tree_root(leaves.iter().map(|leaf| leaf.as_bytes().to_vec()).collect()) -} - -pub fn keccak256_tree_root(mut leaves: Vec>) -> H256 { - assert!(leaves.len() > 1); - let depth = leaves.len().ilog2(); - assert_eq!(leaves.len(), 1 << depth, "leaves.len() must be a power of 2"); - for d in (0..depth).rev() { - for i in 0..(1 << d) { - leaves[i] = keccak256([&leaves[2 * i][..], &leaves[2 * i + 1][..]].concat()).to_vec(); - } - } - H256::from_slice(&leaves[0]) -} - -pub fn u256_to_bytes32_be(input: &U256) -> Vec { - let mut bytes = vec![0; 32]; - input.to_big_endian(&mut bytes); - bytes -} - -// Field is has PrimeField -/// Takes `hash` as `bytes32` and returns `(hash[..16], hash[16..])` represented as big endian numbers in the prime field -pub fn encode_h256_to_field(hash: &H256) -> [F; 2] { - let mut bytes = hash.as_bytes().to_vec(); - bytes.reverse(); - // repr is in little endian - let mut repr = [0u8; 32]; - repr[..16].copy_from_slice(&bytes[16..]); - let val1 = F::from_bytes_le(&repr); - let mut repr = [0u8; 32]; - repr[..16].copy_from_slice(&bytes[..16]); - let val2 = F::from_bytes_le(&repr); - [val1, val2] -} - -pub fn decode_field_to_h256(fe: &[F]) -> H256 { - assert_eq!(fe.len(), 2); - let mut bytes = [0u8; 32]; - bytes[..16].copy_from_slice(&fe[1].to_bytes_le()[..16]); - bytes[16..].copy_from_slice(&fe[0].to_bytes_le()[..16]); - bytes.reverse(); - H256(bytes) -} - -/// Takes U256, converts to bytes32 (big endian) and returns (hash[..16], hash[16..]) represented as big endian numbers in the prime field -pub fn encode_u256_to_field(input: &U256) -> [F; 2] { - let mut bytes = vec![0; 32]; - input.to_little_endian(&mut bytes); - // repr is in little endian - let mut repr = [0u8; 32]; - repr[..16].copy_from_slice(&bytes[16..]); - let val1 = F::from_bytes_le(&repr); - let mut repr = [0u8; 32]; - repr[..16].copy_from_slice(&bytes[..16]); - let val2 = F::from_bytes_le(&repr); - [val1, val2] -} - -pub fn decode_field_to_u256(fe: &[F]) -> U256 { - assert_eq!(fe.len(), 2); - let mut bytes = [0u8; 32]; - bytes[16..].copy_from_slice(&fe[0].to_bytes_le()[..16]); - bytes[..16].copy_from_slice(&fe[1].to_bytes_le()[..16]); - U256::from_little_endian(&bytes) -} - -pub fn encode_addr_to_field(input: &Address) -> F { - let mut bytes = input.as_bytes().to_vec(); - bytes.reverse(); - let mut repr = [0u8; 32]; - repr[..20].copy_from_slice(&bytes); - F::from_bytes_le(&repr) -} - -pub fn decode_field_to_addr(fe: &F) -> Address { - let mut bytes = [0u8; 20]; - bytes.copy_from_slice(&fe.to_bytes_le()[..20]); - bytes.reverse(); - Address::from_slice(&bytes) -} - -// circuit utils: - -/// Loads boolean `val` as witness and asserts it is a bit. -pub fn load_bool( - ctx: &mut Context, - gate: &impl GateInstructions, - val: bool, -) -> AssignedValue { - let bit = ctx.load_witness(F::from(val)); - gate.assert_bit(ctx, bit); - bit -} - -/// Enforces `lhs` equals `rhs` only if `cond` is true. -/// -/// Assumes that `cond` is a bit. -pub fn enforce_conditional_equality( - ctx: &mut Context, - gate: &impl GateInstructions, - lhs: AssignedValue, - rhs: AssignedValue, - cond: AssignedValue, -) { - let [lhs, rhs] = [lhs, rhs].map(|x| gate.mul(ctx, x, cond)); - ctx.constrain_equal(&lhs, &rhs); -} - -/// `array2d` is an array of fixed length arrays. -/// Assumes: -/// * `array2d[i].len() = array2d[j].len()` for all `i,j`. -/// * the values of `indicator` are boolean and that `indicator` has at most one `1` bit. -/// * the lengths of `array2d` and `indicator` are the same. -/// -/// Returns the "dot product" of `array2d` with `indicator` as a fixed length (1d) array of length `array2d[0].len()`. -pub fn select_array_by_indicator( - ctx: &mut Context, - gate: &impl GateInstructions, - array2d: &[impl AsRef<[AssignedValue]>], - indicator: &[AssignedValue], -) -> Vec> { - (0..array2d[0].as_ref().len()) - .map(|j| { - gate.select_by_indicator( - ctx, - array2d.iter().map(|array_i| array_i.as_ref()[j]), - indicator.iter().copied(), - ) - }) - .collect() -} - -/// Assumes that `bytes` have witnesses that are bytes. -pub fn bytes_be_to_u128( - ctx: &mut Context, - gate: &impl GateInstructions, - bytes: &[AssignedValue], -) -> Vec> { - limbs_be_to_u128(ctx, gate, bytes, 8) -} - -pub(crate) fn limbs_be_to_u128( - ctx: &mut Context, - gate: &impl GateInstructions, - limbs: &[AssignedValue], - limb_bits: usize, -) -> Vec> { - assert!(!limbs.is_empty(), "limbs must not be empty"); - assert_eq!(128 % limb_bits, 0); - limbs - .chunks(128 / limb_bits) - .map(|chunk| { - gate.inner_product( - ctx, - chunk.iter().rev().copied(), - (0..chunk.len()).map(|idx| Constant(gate.pow_of_two()[limb_bits * idx])), - ) - }) - .collect_vec() -} - -/// Decomposes `num` into `num_bytes` bytes in big endian and constrains the decomposition holds. -/// -/// Assumes `num` has value in `u64`. -pub fn num_to_bytes_be( - ctx: &mut Context, - range: &RangeChip, - num: &AssignedValue, - num_bytes: usize, -) -> Vec> { - let mut bytes = Vec::with_capacity(num_bytes); - // mostly copied from RangeChip::range_check - let pows = range.gate.pow_of_two().iter().step_by(8).take(num_bytes).map(|x| Constant(*x)); - let byte_vals = - decompose_fe_to_u64_limbs(num.value(), num_bytes, 8).into_iter().map(F::from).map(Witness); - let row_offset = ctx.advice.len() as isize; - let acc = range.gate.inner_product(ctx, byte_vals, pows); - ctx.constrain_equal(&acc, num); - - for i in (0..num_bytes - 1).rev().map(|i| 1 + 3 * i as isize).chain(iter::once(0)) { - let byte = ctx.get(row_offset + i); - range.range_check(ctx, byte, 8); - bytes.push(byte); - } - bytes -} - -/// Takes a fixed length array `bytes` and returns a length `out_len` array equal to -/// `[[0; out_len - len], bytes[..len]].concat()`, i.e., we take `bytes[..len]` and -/// zero pad it on the left. -/// -/// Assumes `0 < len <= max_len <= out_len`. -pub fn bytes_be_var_to_fixed( - ctx: &mut Context, - gate: &impl GateInstructions, - bytes: &[AssignedValue], - len: AssignedValue, - out_len: usize, -) -> Vec> { - debug_assert!(bytes.len() <= out_len); - debug_assert!(bit_length(out_len as u64) < F::CAPACITY as usize); - - // If `bytes` is an RLP field, then `len <= bytes.len()` was already checked during `decompose_rlp_array_phase0` so we don't need to do it again: - // range.range_check(ctx, len, bit_length(bytes.len() as u64)); - let mut padded_bytes = bytes.to_vec(); - padded_bytes.resize(out_len, padded_bytes[0]); - // We use a barrel shifter to shift `bytes` to the right by `out_len - len` bits. - let shift = gate.sub(ctx, Constant(gate.get_field_element(out_len as u64)), len); - let shift_bits = gate.num_to_bits(ctx, shift, bit_length(out_len as u64)); - for (i, shift_bit) in shift_bits.into_iter().enumerate() { - let shifted_bytes = (0..out_len) - .map(|j| { - if j >= (1 << i) { - Existing(padded_bytes[j - (1 << i)]) - } else { - Constant(F::zero()) - } - }) - .collect_vec(); - padded_bytes = padded_bytes - .into_iter() - .zip(shifted_bytes) - .map(|(noshift, shift)| gate.select(ctx, shift, noshift, shift_bit)) - .collect_vec(); - } - padded_bytes -} - -/// Decomposes `uint` into `num_bytes` bytes and constrains the decomposition. -/// Here `uint` can be any uint that fits into `F`. -pub fn uint_to_bytes_be( - ctx: &mut Context, - range: &RangeChip, - uint: &AssignedValue, - num_bytes: usize, -) -> Vec> { - let mut bytes = Vec::with_capacity(num_bytes); - // mostly copied from RangeChip::range_check - let pows = range.gate.pow_of_two().iter().step_by(8).take(num_bytes).map(|x| Constant(*x)); - let byte_vals = decompose(uint.value(), num_bytes, 8).into_iter().map(Witness); - let row_offset = ctx.advice.len() as isize; - let acc = range.gate.inner_product(ctx, byte_vals, pows); - ctx.constrain_equal(&acc, uint); - - for i in (0..num_bytes - 1).rev().map(|i| 1 + 3 * i as isize).chain(iter::once(0)) { - let byte = ctx.get(row_offset + i); - range.range_check(ctx, byte, 8); - bytes.push(byte); - } - bytes -} - -/// See [`num_to_bytes_be`] for details. Here `uint` can now be any uint that fits into `F`. -pub fn uint_to_bytes_le( - ctx: &mut Context, - range: &RangeChip, - uint: &AssignedValue, - num_bytes: usize, -) -> Vec> { - let mut bytes = Vec::with_capacity(num_bytes); - // mostly copied from RangeChip::range_check - let pows = range.gate.pow_of_two().iter().step_by(8).take(num_bytes).map(|x| Constant(*x)); - let byte_vals = decompose(uint.value(), num_bytes, 8).into_iter().map(Witness); - let row_offset = ctx.advice.len() as isize; - let acc = range.gate.inner_product(ctx, byte_vals, pows); - ctx.constrain_equal(&acc, uint); - - for i in iter::once(0).chain((0..num_bytes - 1).map(|i| 1 + 3 * i as isize)) { - let byte = ctx.get(row_offset + i); - range.range_check(ctx, byte, 8); - bytes.push(byte); - } - bytes -} - -pub fn bytes_be_to_uint( - ctx: &mut Context, - gate: &impl GateInstructions, - input: &[AssignedValue], - num_bytes: usize, -) -> AssignedValue { - gate.inner_product( - ctx, - input[..num_bytes].iter().rev().copied(), - (0..num_bytes).map(|idx| Constant(gate.pow_of_two()[8 * idx])), - ) -} - -/// Converts a fixed length array of `u128` values into a fixed length array of big endian bytes. -pub fn u128s_to_bytes_be( - ctx: &mut Context, - range: &RangeChip, - u128s: &[AssignedValue], -) -> Vec> { - u128s.iter().map(|u128| uint_to_bytes_be(ctx, range, u128, 16)).concat() -} - -/// Returns 1 if all entries of `input` are zero, 0 otherwise. -pub fn is_zero_vec( - ctx: &mut Context, - gate: &impl GateInstructions, - input: &[AssignedValue], -) -> AssignedValue { - let is_zeros = input.iter().map(|x| gate.is_zero(ctx, *x)).collect_vec(); - let sum = gate.sum(ctx, is_zeros); - let total_len = gate.get_field_element(input.len() as u64); - gate.is_equal(ctx, sum, Constant(total_len)) -} diff --git a/axiom-eth/src/util/scheduler/mod.rs b/axiom-eth/src/util/scheduler/mod.rs deleted file mode 100644 index bee574ca..00000000 --- a/axiom-eth/src/util/scheduler/mod.rs +++ /dev/null @@ -1,272 +0,0 @@ -use ethers_core::types::U256; -use ethers_providers::{Http, Middleware, Provider, RetryClient}; -use halo2_base::{ - halo2_proofs::{ - halo2curves::bn256::{Bn256, G1Affine}, - plonk::ProvingKey, - poly::kzg::commitment::ParamsKZG, - }, - utils::fs::{gen_srs, read_params}, -}; -#[cfg(feature = "halo2-axiom")] -use snark_verifier_sdk::halo2::read_snark; -use snark_verifier_sdk::Snark; -use std::{ - collections::HashMap, - env::var, - fmt::Debug, - fs, - hash::Hash, - marker::PhantomData, - path::{Path, PathBuf}, - sync::{Arc, RwLock}, -}; - -use crate::Network; - -use super::circuit::AnyCircuit; - -/// This is a tag for the type of a circuit, independent of the circuit's inputs. -/// For example, it can be used to fetch the proving key for the circuit. -pub trait CircuitType: Clone + Debug + Eq + Hash { - fn name(&self) -> String; - fn get_degree_from_pinning(&self, pinning_path: impl AsRef) -> u32; -} - -/// This is an identifier for a specific proof request, consisting of the circuit type together with any data necessary to create the circuit inputs. -/// It should be thought of as a node in a DAG (directed acyclic graph), where the edges specify previous SNARKs this one depends on. -pub trait Task: Clone + Debug { - type CircuitType: CircuitType; - - fn circuit_type(&self) -> Self::CircuitType; - fn name(&self) -> String; - - /// The previous tasks this task depends on (i.e., the edges of the DAG that point to this node). - fn dependencies(&self) -> Vec; -} - -pub trait SchedulerCommon { - type CircuitType: CircuitType; - - fn config_dir(&self) -> &Path; - fn data_dir(&self) -> &Path; - fn pkey_readonly(&self) -> bool; - fn srs_readonly(&self) -> bool; - /// The path to the file with the circuit configuration pinning. - fn pinning_path(&self, circuit_type: &Self::CircuitType) -> PathBuf { - self.config_dir().join(format!("{}.json", circuit_type.name())) - } - /// Returns the degree of the circuit from file. - /// - /// Recommended: use `HashMap` to cache this. - fn get_degree(&self, circuit_type: &Self::CircuitType) -> u32; - /// Read (or generate) the universal trusted setup by reading configuration file. - /// - /// Recommended: Cache the params in a hashmap if they are not already cached. - fn get_params(&self, k: u32) -> Arc>; - /// Fetch pkey from cache (intended to be from HashMap). - fn get_pkey(&self, circuit_type: &Self::CircuitType) -> Option>>; - /// Assumes this uses the same HashMap as `get_pkey`. - fn insert_pkey(&self, circuit_type: Self::CircuitType, pkey: ProvingKey); -} - -/// A basic implementation of `SchedulerCommon` with support for ETH JSON-RPC requests. -pub struct EthScheduler { - /// Specifies if universal trusted setup should only be read (production mode) or randomly generated (UNSAFE non-production mode) - srs_read_only: bool, - /// Specifies if new proving keys should be generated or not. If `read_only` is true, then `srs_read_only` is also force to be true. - read_only: bool, - config_dir: PathBuf, - data_dir: PathBuf, - - pub pkeys: RwLock>>>, - pub degree: RwLock>, - pub params: RwLock>>>, - pub provider: Arc>>, - pub network: Network, - - _marker: PhantomData, -} - -impl EthScheduler { - pub fn new( - network: Network, - mut srs_read_only: bool, - read_only: bool, - config_dir: PathBuf, - data_dir: PathBuf, - ) -> Self { - let provider_url = var("JSON_RPC_URL").expect("JSON_RPC_URL not found"); - let provider = Provider::new_client(&provider_url, 10, 500) - .expect("could not instantiate HTTP Provider"); - tokio::runtime::Runtime::new().unwrap().block_on(async { - let chain_id = provider.get_chainid().await.unwrap(); - match network { - Network::Mainnet => { - assert_eq!(chain_id, U256::from(1)); - } - Network::Goerli => { - assert_eq!(chain_id, U256::from(5)); - } - } - }); - fs::create_dir_all(&config_dir).expect("could not create config directory"); - fs::create_dir_all(&data_dir).expect("could not create data directory"); - srs_read_only = srs_read_only || read_only; - #[cfg(feature = "production")] - { - srs_read_only = true; - } - Self { - srs_read_only, - read_only, - config_dir, - data_dir, - pkeys: Default::default(), - degree: Default::default(), - params: Default::default(), - provider: Arc::new(provider), - network, - _marker: PhantomData, - } - } -} - -impl SchedulerCommon for EthScheduler { - type CircuitType = T::CircuitType; - - fn config_dir(&self) -> &Path { - self.config_dir.as_path() - } - fn data_dir(&self) -> &Path { - self.data_dir.as_path() - } - fn srs_readonly(&self) -> bool { - self.srs_read_only - } - fn pkey_readonly(&self) -> bool { - self.read_only - } - fn get_degree(&self, circuit_type: &Self::CircuitType) -> u32 { - if let Some(k) = self.degree.read().unwrap().get(circuit_type) { - return *k; - } - let path = self.pinning_path(circuit_type); - let k = CircuitType::get_degree_from_pinning(circuit_type, path); - self.degree.write().unwrap().insert(circuit_type.clone(), k); - k - } - fn get_params(&self, k: u32) -> Arc> { - if let Some(params) = self.params.read().unwrap().get(&k) { - return Arc::clone(params); - } - let params = if self.srs_readonly() { read_params(k) } else { gen_srs(k) }; - let params = Arc::new(params); - self.params.write().unwrap().insert(k, Arc::clone(¶ms)); - params - } - fn get_pkey(&self, circuit_type: &Self::CircuitType) -> Option>> { - self.pkeys.read().unwrap().get(circuit_type).map(Arc::clone) - } - fn insert_pkey(&self, circuit_type: Self::CircuitType, pkey: ProvingKey) { - self.pkeys.write().unwrap().insert(circuit_type, Arc::new(pkey)); - } -} - -/// A scheduler that can recursively generate SNARKs for a DAG of tasks. The directed acyclic graph (DAG) is implicitly -/// defined by the `Task` implementation and `get_circuit`. For any task, either a snark or calldata + on-chain verifier can be generated. -pub trait Scheduler: SchedulerCommon::CircuitType> { - type Task: Task; - /// The intended use is that `CircuitRouter` is an enum containing the different `PreCircuit`s that `Task` can become in the `get_circuit` function. - // TODO: better way to do this with macros? `PreCircuit` is not object safe so cannot use `dyn` - type CircuitRouter: AnyCircuit + Clone; - - /// `prev_snarks` is assumed to be the SNARKs associated to the previous tasks in `task.dependencies()`. From those snarks (if any), this - /// function constructs the pre-circuit for this task. - fn get_circuit(&self, task: Self::Task, prev_snarks: Vec) -> Self::CircuitRouter; - - // ==== automatically generated functions below ==== - - fn pkey_path(&self, circuit_type: &::CircuitType) -> PathBuf { - self.data_dir().join(format!("{}.pk", circuit_type.name())) - } - fn yul_path(&self, circuit_type: &::CircuitType) -> PathBuf { - self.data_dir().join(format!("{}.yul", circuit_type.name())) - } - fn snark_path(&self, task: &Self::Task) -> PathBuf { - self.data_dir().join(format!("{}.snark", task.name())) - } - fn calldata_path(&self, task: &Self::Task) -> PathBuf { - self.data_dir().join(format!("{}.calldata", task.name())) - } - - // recursively generates necessary circuits and snarks to create snark - fn get_snark(&self, task: Self::Task) -> Snark { - let snark_path = self.snark_path(&task); - #[cfg(feature = "halo2-axiom")] - if let Ok(snark) = read_snark(snark_path.clone()) { - return snark; - } - let read_only = self.pkey_readonly(); - - // Recursively generate the SNARKs for the dependencies of this task. - let dep_snarks: Vec = - task.dependencies().into_iter().map(|dep| self.get_snark(dep)).collect(); - - let circuit_type = task.circuit_type(); - let k = self.get_degree(&circuit_type); - let params = &self.get_params(k); - // Construct the pre-circuit for this task from the dependency SNARKs. - let pre_circuit = self.get_circuit(task, dep_snarks); - let pk_path = self.pkey_path(&circuit_type); - let pinning_path = self.pinning_path(&circuit_type); - - let pk = if let Some(pk) = self.get_pkey(&circuit_type) { - pk - } else { - let pk = - pre_circuit.clone().read_or_create_pk(params, pk_path, &pinning_path, read_only); - self.insert_pkey(circuit_type.clone(), pk); - self.get_pkey(&circuit_type).unwrap() - }; - let snark_path = Some(snark_path); - pre_circuit.gen_snark_shplonk(params, &pk, &pinning_path, snark_path) - } - - #[cfg(feature = "evm")] - fn get_calldata(&self, task: Self::Task, generate_smart_contract: bool) -> String { - // TODO: some shared code with `get_snark`; clean up somehow - - let calldata_path = self.calldata_path(&task); - if let Ok(calldata) = fs::read_to_string(&calldata_path) { - // calldata is serialized as a hex string - return calldata; - } - - let dep_snarks: Vec = - task.dependencies().into_iter().map(|dep| self.get_snark(dep)).collect(); - - let circuit_type = task.circuit_type(); - let k = self.get_degree(&circuit_type); - let params = &self.get_params(k); - // Construct the pre-circuit for this task from the dependency SNARKs. - let pre_circuit = self.get_circuit(task, dep_snarks); - let pk_path = self.pkey_path(&circuit_type); - let pinning_path = self.pinning_path(&circuit_type); - - let pk = if let Some(pk) = self.get_pkey(&circuit_type) { - pk - } else { - let read_only = self.pkey_readonly(); - let pk = - pre_circuit.clone().read_or_create_pk(params, pk_path, &pinning_path, read_only); - self.insert_pkey(circuit_type.clone(), pk); - self.get_pkey(&circuit_type).unwrap() - }; - - let deployment_code = generate_smart_contract.then(|| { - pre_circuit.clone().gen_evm_verifier_shplonk(params, &pk, self.yul_path(&circuit_type)) - }); - pre_circuit.gen_calldata(params, &pk, pinning_path, calldata_path, deployment_code) - } -} diff --git a/axiom-eth/src/utils/README.md b/axiom-eth/src/utils/README.md new file mode 100644 index 00000000..d8492d62 --- /dev/null +++ b/axiom-eth/src/utils/README.md @@ -0,0 +1,71 @@ +# Component Framework + +We introduce a new circuit design concept of **Component Circuits** in this crate. + +## Definition of Component Circuit + +A component circuit is a circuit that is designed so that it can: + +- Output a _virtual table_ of `(key, value)` pairs that can then be used by an external circuit + - The output table is virtual because the circuit only outputs a commitment (some kind of hash) of the virtual table, and not the table itself +- Load the virtual output table from another component circuit and be able to look up the value for any given key in the table. + - The loaded virtual table depends on the verification of the other component circuit that generated the table, so we call this loading and lookup process a **promise call**. + - The circuit making the promise call is provided the virtual table as unconstrained private witnesses. It then commits to the table in exactly the same way as the circuit that generated the table, and exposes the commitment as a public instance. The aggregation circuit that verifies both the caller circuit and callee circuit must check that the **promise commitment** and **output commitment** are equal. + - To actually use the loaded table in-circuit, the caller circuit must use a dynamic lookup table: the promise table is loaded into advice columns, and whenever one wants to use a `(key, value)` pair elsewhere in the circuit, a dynamic lookup must be done of `(key, value)` into the assigned advice table. + +Concretely, any component circuit will have a corresponding struct `ComponentType{Name}` that implements the [`ComponentType`](./component/mod.rs) trait. + +### Shards + +The `ComponentType` only specifies the format of the virtual table output by the component circuit - it does not specify how the output commit of the virtual table should be computed. This is because we allow multiple concrete circuit implementations associated to a single component type. + +Concretely, we implement a component circuit as either a **shard** circuit or an **aggregation** circuit. The shard circuit is what actually constrains the correctness of the virtual table. It then commits to the virtual table by performing a flat Poseidon hash of the entire concatenated table. Multiple shard circuits can be aggregated in an aggregation circuit, which will compute the merkle root of the output commitments of the shard circuits. We provide a generic aggregation circuit implementation that does this in [`merkle_aggregation`](./merkle_aggregation.rs). + +In summary, in all of our concrete implementations of component circuits, the output commitment to the virtual table is always a Merkle root of flat hashes of subsections of the table. + +### `component` module + +The [component](./component/) module is designed to automate the above process as much as possible. + +Above, we have not specified the types of `(key, value)`. We provide traits so that `key` can be a variable length array of field elements, and `value` is a fixed length array of field elements (fixed length known at compile time). If one were to directly assign such a virtual table of `(key, value)`s, one would need multiple columns for the length of each `key, value`. While the Halo2 backend will RLC these columns together in the prover backend before performing dynamic lookups, this is only worth it if your table size, or number of lookups performed, is comparable to the number of total rows in your circuit. For our use cases we have found that to rarely be the case, so instead we do not directly assign the virtual table to raw advice columns. We first RLC the `key, value` into a single value (using `RlcCircuitBuilder`) and then assign the RLCed value to a single raw advice column. Then when you need to look up a `(key, value)` pair, you must perform the same RLC on this pair and then dynamically look up the RLCed value into the previously assigned "table". + +One creates a concrete Component Circuit implementation by creating a `ComponentCircuitImpl, P: PromiseBuilder>` struct. One can think of this struct as a combination of three circuit builders (in the sense of the previous section): + +1. `RlcCircuitBuilder`: we have enshrined this circuit builder in this crate as it is integral and used everywhere. +2. `CoreBuilder`: trait +3. `PromiseBuilder`: trait + +### Promise Builder + +The `PromiseBuilder` trait is an interface for any concrete implementation of the circuit builder that controls and automates the promise call process described above: the `PromiseBuilder` controls the process of actually loading the virtual table into this circuit, performing RLCs, and also adding dynamic lookups. It owns new circuit columns corresponding to both the lookup table and columns for values-to-lookup. Because `PromiseBuilder` needs to perform RLCs, the `virtual_assign_*` functions have access to `RlcCircuitBuilder`. + +We have four concrete implementations of `PromiseBuilder` in this crate: + +- `EmptyPromiseLoader`: does nothing, just so you have something to stick into `ComponentCircuitImpl`. This is for component circuits that do not need to make promise calls -- the component circuit only outputs a commitment. +- `PromiseLoader`: the most commonly used. Does exactly what is described above for the virtual promise table output by a _single_ component circuit. +- `MultiPromiseLoader`: if you load _multiple_ virtual tables from separate component circuits, with possibly different `(key, value)` types, but want to concatenate and assign all these tables into the same raw table (with some tag for the component type). +- `PromiseBuilderCombo`: boilerplate for combining two `PromiseBuilder`s and auto-implement `PromiseBuilder` again. + +### Core Builder + +The `CoreBuilder` is where you specify the main business logic of what your circuit should do. +In the main logic, you are also allowed to make promise calls to other component circuits: these requests are relayed to the `PromiseBuilder` via [`PromiseCaller`](./component/promise_collector.rs). We emphasize that `PromiseCaller` is **not** a circuit concept. The `PromiseCaller` is really just a struct that collects promise calls and relays them to the `PromiseBuilder` to actually adds the dynamic lookups. (This is necessary for example to _collect_ the requests that actually need to be sent as inputs to the called component circuit.) +The `PromiseCaller` is a shared thread-safe `PromiseCollector`. +The `PromiseCollector` implements another trait `PromiseCallsGetter`: this trait is exposed to `PromiseBuilder` as the way to get the promise calls to be looked up. + +The `CoreBuilder::virtual_assign_*` gets access to `RlcCircuitBuilder` and `PromiseCaller`. +The `CoreBuilder` can own its own columns/gates, and it does have the ability to raw assign +to these columns, but a common use of `CoreBuilder` is to only specify the `virtual_assign_*` +logic using `RlcCircuitBuilder` and do no additional raw assignments of its own. + +### Component Circuit Implementation + +The synthesis of the above pieces is done in the actual [implementation](./component/circuit/mod.rs) +of the Halo2 `Circuit` trait for `ComponentCircuitImpl`. This will call `virtual_assign_*` for both +`CoreBuilder` and `PromiseBuilder`, and then it will call `raw_synthesize_*` for `CoreBuilder`, `PromiseBuilder`, **and** `RlcCircuitBuilder`. + +In addition, `ComponentCircuitImpl` controls the assignment of public instances, because there are special instances (2, one output commitment, one promise commitment) reserved for Component Circuit usage. The `CoreBuilder` can specify additional public instances to be exposed in the return of `CoreBuilder::virtual_assign_phase0`. + +## Notes + +Many parts of the implementation of the Component Circuit framework (and circuit builders in general) could likely be streamlined with the use of runtime `dyn` and type inference. One main blocker to this is that many circuit related traits are not object-safe, so one cannot use `dyn` on them. In particular, the `Circuit` trait itself is not object safe because one needs to specify `Circuit::Config`. Moreover, there is no way to create `Circuit::Config` without access to a `ConstraintSystem` object. diff --git a/axiom-eth/src/utils/build_utils/aggregation.rs b/axiom-eth/src/utils/build_utils/aggregation.rs new file mode 100644 index 00000000..37a64988 --- /dev/null +++ b/axiom-eth/src/utils/build_utils/aggregation.rs @@ -0,0 +1,20 @@ +use snark_verifier_sdk::{halo2::aggregation::AggregationCircuit, CircuitExt}; + +use crate::utils::snark_verifier::AggregationCircuitParams; + +pub trait CircuitMetadata { + const HAS_ACCUMULATOR: bool; + + fn accumulator_indices() -> Option> { + if Self::HAS_ACCUMULATOR { + AggregationCircuit::accumulator_indices() + } else { + None + } + } + fn num_instance(&self) -> Vec; +} + +pub fn get_dummy_aggregation_params(k: usize) -> AggregationCircuitParams { + AggregationCircuitParams { degree: k as u32, lookup_bits: k - 1, ..Default::default() } +} diff --git a/axiom-eth/src/utils/build_utils/dummy.rs b/axiom-eth/src/utils/build_utils/dummy.rs new file mode 100644 index 00000000..fcafa121 --- /dev/null +++ b/axiom-eth/src/utils/build_utils/dummy.rs @@ -0,0 +1,6 @@ +/// Generates a dummy of this type from a seed. +/// This is used to generate dummy inputs for the circuit. +pub trait DummyFrom { + /// Dummy from a seed. + fn dummy_from(seed: S) -> Self; +} diff --git a/axiom-eth/src/utils/build_utils/keygen.rs b/axiom-eth/src/utils/build_utils/keygen.rs new file mode 100644 index 00000000..a74d5eb5 --- /dev/null +++ b/axiom-eth/src/utils/build_utils/keygen.rs @@ -0,0 +1,118 @@ +use std::{ + fs::File, + io::{BufReader, BufWriter}, + path::Path, +}; + +use anyhow::Context; +use halo2_base::halo2_proofs::poly::commitment::ParamsProver; +use snark_verifier::{util::arithmetic::Domain, verifier::plonk::PlonkProtocol}; +use snark_verifier_sdk::halo2::utils::{ + AggregationDependencyIntent, AggregationDependencyIntentOwned, +}; + +use crate::{ + halo2_base::gates::circuit::BaseCircuitParams, + halo2_proofs::{ + plonk::{ProvingKey, VerifyingKey}, + poly::{commitment::Params, kzg::commitment::ParamsKZG}, + SerdeFormat, + }, + halo2curves::bn256::{Bn256, Fr, G1Affine}, + rlc::circuit::RlcCircuitParams, + utils::keccak::decorator::RlcKeccakCircuitParams, +}; + +pub fn get_dummy_rlc_circuit_params(k: usize, lookup_bits: usize) -> RlcCircuitParams { + RlcCircuitParams { + base: BaseCircuitParams { + k, + lookup_bits: Some(lookup_bits), + num_instance_columns: 1, + ..Default::default() + }, + num_rlc_columns: 0, + } +} + +pub fn get_dummy_rlc_keccak_params(k: usize, lookup_bits: usize) -> RlcKeccakCircuitParams { + let rlc = get_dummy_rlc_circuit_params(k, lookup_bits); + RlcKeccakCircuitParams { rlc, keccak_rows_per_round: 20 } +} + +pub fn get_circuit_id(vk: &VerifyingKey) -> String { + let buf = vk.to_bytes(crate::halo2_proofs::SerdeFormat::RawBytes); + format!("{}", blake3::hash(&buf)) +} + +/// Write vk to separate file just for ease of inspection. +pub fn write_pk_and_pinning( + dir: &Path, + pk: &ProvingKey, + pinning: &impl serde::Serialize, +) -> anyhow::Result { + let circuit_id = get_circuit_id(pk.get_vk()); + let pk_path = dir.join(format!("{circuit_id}.pk")); + let vk_path = dir.join(format!("{circuit_id}.vk")); + let pinning_path = dir.join(format!("{circuit_id}.json")); + + serde_json::to_writer_pretty( + File::create(&pinning_path) + .with_context(|| format!("Failed to create file {}", pinning_path.display()))?, + pinning, + ) + .context("Failed to serialize pinning")?; + pk.get_vk() + .write( + &mut File::create(&vk_path) + .with_context(|| format!("Failed to create file {}", vk_path.display()))?, + SerdeFormat::RawBytes, + ) + .context("Failed to serialize vk")?; + + let mut writer = BufWriter::with_capacity( + 128 * 1024 * 1024, + File::create(&pk_path) + .with_context(|| format!("Failed to create file {}", pk_path.display()))?, + ); // 128 MB capacity + pk.write(&mut writer, SerdeFormat::RawBytes).context("Failed to serialize pk")?; + + Ok(circuit_id) +} + +pub fn read_srs_from_dir(params_dir: &Path, k: u32) -> anyhow::Result> { + let srs_path = params_dir.join(format!("kzg_bn254_{k}.srs")); + let params = ParamsKZG::::read(&mut BufReader::new( + File::open(srs_path.clone()) + .with_context(|| format!("Failed to read SRS from {}", srs_path.display()))?, + ))?; + Ok(params) +} + +/// Converts `intent` into `PlonkProtocol` for pinning. +/// If `universal_agg == true`, that means this intent is going to be a dependency in a universal aggregation circuit. +/// In that case, the domain and preprocessed commitments are loaded as witnesses, so we clear them so they are not stored in the pinning. +pub fn compile_agg_dep_to_protocol( + kzg_params: &ParamsKZG, + intent: &AggregationDependencyIntentOwned, + universal_agg: bool, +) -> PlonkProtocol { + let k = intent.vk.get_domain().k(); + // BAD UX: `compile` for `Config::kzg()` only uses `params` for `params.k()`, so we will just generate a random params with the correct `k`. + // We provide the correct deciding key just for safety + let dummy_params = kzg_params.from_parts( + k, + vec![kzg_params.get_g()[0]], + Some(vec![]), + kzg_params.g2(), + kzg_params.s_g2(), + ); + let mut protocol = AggregationDependencyIntent::from(intent).compile(&dummy_params); + if universal_agg { + protocol.domain = Domain::new(0, Fr::one()); + protocol.preprocessed.clear(); + protocol.transcript_initial_state = None; + } + protocol.domain_as_witness = None; + protocol +} diff --git a/axiom-eth/src/utils/build_utils/mod.rs b/axiom-eth/src/utils/build_utils/mod.rs new file mode 100644 index 00000000..dce8d152 --- /dev/null +++ b/axiom-eth/src/utils/build_utils/mod.rs @@ -0,0 +1,8 @@ +/// Instructions for aggregating a circuit using `snark-verifier` SDK +#[cfg(feature = "aggregation")] +pub mod aggregation; +pub mod dummy; +#[cfg(feature = "keygen")] +pub mod keygen; +/// Circut pinning instructions +pub mod pinning; diff --git a/axiom-eth/src/utils/build_utils/pinning.rs b/axiom-eth/src/utils/build_utils/pinning.rs new file mode 100644 index 00000000..d70cb91c --- /dev/null +++ b/axiom-eth/src/utils/build_utils/pinning.rs @@ -0,0 +1,254 @@ +use halo2_base::{ + gates::{ + circuit::{builder::BaseCircuitBuilder, BaseCircuitParams}, + flex_gate::MultiPhaseThreadBreakPoints, + }, + halo2_proofs::{ + halo2curves::bn256::{Bn256, Fr, G1Affine}, + plonk::{Circuit, ProvingKey}, + poly::kzg::commitment::ParamsKZG, + }, + utils::ScalarField, +}; +use serde::{Deserialize, Serialize}; +use snark_verifier_sdk::gen_pk; +use std::{fs::File, io, path::Path}; + +use crate::{ + rlc::{circuit::RlcCircuitParams, virtual_region::RlcThreadBreakPoints}, + utils::eth_circuit::ETH_LOOKUP_BITS, +}; + +/// Perhaps `CircuitPinning` and `CircuitParams` are the same thing. +/// For now the distinction is that `CircuitParams` is what is needed +/// in `configure` to build the `Circuit::Config` and the pinning can be +/// derived from that by running `synthesize` during keygen. +/// The difference +pub trait Halo2CircuitPinning: Serialize + Sized + for<'de> Deserialize<'de> { + type CircuitParams; + type BreakPoints; + /// Constructor + fn new(params: Self::CircuitParams, break_points: Self::BreakPoints) -> Self; + /// Returns the configuration parameters + fn params(&self) -> Self::CircuitParams; + /// Returns break points + fn break_points(&self) -> Self::BreakPoints; + /// Degree of the circuit, log_2(number of rows) + fn k(&self) -> usize; + /// Loads from a file + fn from_path>(path: P) -> anyhow::Result { + let p = serde_json::from_reader(File::open(&path)?)?; + Ok(p) + } + /// Writes to file + fn write>(&self, path: P) -> anyhow::Result<()> { + serde_json::to_writer_pretty(File::create(path)?, self)?; + Ok(()) + } +} + +pub trait CircuitPinningInstructions { + type Pinning: Halo2CircuitPinning; + + fn pinning(&self) -> Self::Pinning; +} + +pub trait PinnableCircuit: CircuitPinningInstructions + Sized + Circuit { + /// Reads the proving key for the pre-circuit. + fn read_pk( + path: impl AsRef, + circuit_params: >::Params, + ) -> io::Result> { + snark_verifier_sdk::read_pk::(path.as_ref(), circuit_params) + } + + /// Creates the proving key for the pre-circuit if file at `pk_path` is not found. + /// If a new proving key is created, the new pinning data is written to `pinning_path`. + fn create_pk( + &self, + params: &ParamsKZG, + pk_path: impl AsRef, + pinning_path: impl AsRef, + ) -> anyhow::Result<(ProvingKey, Self::Pinning)> { + let circuit_params = self.params(); + if let Ok(pk) = Self::read_pk(pk_path.as_ref(), circuit_params) { + let pinning = Self::Pinning::from_path(pinning_path.as_ref())?; + Ok((pk, pinning)) + } else { + let pk = gen_pk(params, self, Some(pk_path.as_ref())); + // should only write pinning data if we created a new pkey + let pinning = self.pinning(); + pinning.write(pinning_path)?; + Ok((pk, pinning)) + } + } +} + +impl PinnableCircuit for C where C: CircuitPinningInstructions + Sized + Circuit {} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct RlcCircuitPinning { + pub params: RlcCircuitParams, + pub break_points: RlcThreadBreakPoints, +} + +impl Halo2CircuitPinning for RlcCircuitPinning { + type CircuitParams = RlcCircuitParams; + type BreakPoints = RlcThreadBreakPoints; + + fn new(params: Self::CircuitParams, break_points: Self::BreakPoints) -> Self { + Self { params, break_points } + } + + fn from_path>(path: P) -> anyhow::Result { + let mut pinning: Self = serde_json::from_reader(File::open(&path)?)?; + if pinning.params.base.lookup_bits.is_none() { + pinning.params.base.lookup_bits = Some(ETH_LOOKUP_BITS); + } + Ok(pinning) + } + + fn params(&self) -> Self::CircuitParams { + self.params.clone() + } + + fn break_points(&self) -> RlcThreadBreakPoints { + self.break_points.clone() + } + + fn k(&self) -> usize { + self.params.base.k + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Default, Hash)] +pub struct BaseCircuitPinning { + pub params: BaseCircuitParams, + pub break_points: MultiPhaseThreadBreakPoints, +} + +impl Halo2CircuitPinning for BaseCircuitPinning { + type CircuitParams = BaseCircuitParams; + type BreakPoints = MultiPhaseThreadBreakPoints; + + fn new(params: Self::CircuitParams, break_points: Self::BreakPoints) -> Self { + Self { params, break_points } + } + + fn params(&self) -> Self::CircuitParams { + self.params.clone() + } + + fn break_points(&self) -> MultiPhaseThreadBreakPoints { + self.break_points.clone() + } + + fn k(&self) -> usize { + self.params.k + } +} + +impl CircuitPinningInstructions for BaseCircuitBuilder { + type Pinning = BaseCircuitPinning; + fn pinning(&self) -> Self::Pinning { + let break_points = self.break_points(); + let params = self.params(); + Self::Pinning::new(params, break_points) + } +} + +#[cfg(feature = "aggregation")] +pub mod aggregation { + use halo2_base::{ + gates::flex_gate::MultiPhaseThreadBreakPoints, + halo2_proofs::{ + halo2curves::bn256::{Bn256, G1Affine}, + plonk::Circuit, + }, + }; + use serde::{Deserialize, Serialize}; + use serde_with::serde_as; + use snark_verifier::{pcs::kzg::KzgDecidingKey, verifier::plonk::PlonkProtocol}; + use snark_verifier_sdk::halo2::aggregation::{AggregationCircuit, AggregationConfigParams}; + + use super::{CircuitPinningInstructions, Halo2CircuitPinning}; + + #[derive(Clone, Debug, Serialize, Deserialize, Default)] + pub struct AggregationCircuitPinning { + pub params: AggregationConfigParams, + pub break_points: MultiPhaseThreadBreakPoints, + } + + impl Halo2CircuitPinning for AggregationCircuitPinning { + type CircuitParams = AggregationConfigParams; + type BreakPoints = MultiPhaseThreadBreakPoints; + + fn new(params: Self::CircuitParams, break_points: Self::BreakPoints) -> Self { + Self { params, break_points } + } + + fn params(&self) -> Self::CircuitParams { + self.params + } + + fn break_points(&self) -> MultiPhaseThreadBreakPoints { + self.break_points.clone() + } + + fn k(&self) -> usize { + self.params.degree as usize + } + } + + impl CircuitPinningInstructions for AggregationCircuit { + type Pinning = AggregationCircuitPinning; + fn pinning(&self) -> Self::Pinning { + let break_points = self.break_points(); + let params = self.params(); + AggregationCircuitPinning::new(params, break_points) + } + } + + /// Generic aggregation pinning + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct GenericAggPinning { + /// Aggregation configuration parameters + pub params: AggParams, + /// Number of instances in each instance column + pub num_instance: Vec, + /// The indices of the KZG accumulator in the public instance column(s) + pub accumulator_indices: Vec<(usize, usize)>, + /// Aggregate vk hash, if universal aggregation circuit. + /// * ((i, j), agg_vkey_hash), where the hash is located at (i, j) in the public instance columns + pub agg_vk_hash_data: Option<((usize, usize), String)>, + /// g1 generator, g2 generator, s_g2 (s is generator of trusted setup). + /// Together with domain size `2^k`, this commits to the trusted setup used. + /// This is all that's needed to verify the final ecpairing check on the KZG proof. + pub dk: KzgDecidingKey, + /// Break points. Should only have phase0, so MultiPhase is extraneous. + pub break_points: MultiPhaseThreadBreakPoints, + } + + /// Generic aggregation circuit configuration parameters + #[serde_as] + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct GenericAggParams { + /// The compiled verification key of each dependency circuit to be aggregated. + #[serde_as(as = "Vec")] + pub to_agg: Vec>, + pub agg_params: AggregationConfigParams, + } + + /// The circuit IDs of the aggregation tree + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + pub struct AggTreeId { + /// Root circuit ID + pub circuit_id: String, + /// Children aggregation tree IDs + pub children: Vec, + /// If the root circuit is a universal aggregation circuit, this is the aggregate vkey hash. + /// None otherwise. + #[serde(skip_serializing_if = "Option::is_none")] + pub aggregate_vk_hash: Option, + } +} diff --git a/axiom-eth/src/utils/circuit_utils/bytes.rs b/axiom-eth/src/utils/circuit_utils/bytes.rs new file mode 100644 index 00000000..d248d9e9 --- /dev/null +++ b/axiom-eth/src/utils/circuit_utils/bytes.rs @@ -0,0 +1,126 @@ +use crate::Field; +use crate::{ + mpt::MPTProof, + utils::{bytes_be_to_u128, bytes_be_to_uint}, +}; +use halo2_base::{ + gates::GateInstructions, + safe_types::{SafeBool, SafeByte, SafeBytes32, SafeTypeChip}, + utils::ScalarField, + AssignedValue, Context, +}; + +use crate::utils::hilo::HiLo; + +/// Takes `bytes` as fixed length byte array, left pads with 0s, and then converts +/// to HiLo form. Optmization where if `bytes` is less than 16 bytes, it can +/// skip the Hi part. +pub fn pack_bytes_to_hilo( + ctx: &mut Context, + gate: &impl GateInstructions, + bytes: &[SafeByte], +) -> HiLo> { + let len = bytes.len(); + assert!(len <= 32); + let hi = if len > 16 { + let hi_bytes = &bytes[0..len - 16]; + bytes_be_to_uint(ctx, gate, hi_bytes, hi_bytes.len()) + } else { + ctx.load_zero() + }; + let lo = { + let lo_len = if len > 16 { 16 } else { len }; + let lo_bytes = &bytes[len - lo_len..len]; + bytes_be_to_uint(ctx, gate, lo_bytes, lo_len) + }; + HiLo::from_hi_lo([hi, lo]) +} + +pub fn select_hi_lo( + ctx: &mut Context, + gate: &impl GateInstructions, + if_true: &HiLo>, + if_false: &HiLo>, + condition: SafeBool, +) -> HiLo> { + let condition = *condition.as_ref(); + let hi = gate.select(ctx, if_true.hi(), if_false.hi(), condition); + let lo = gate.select(ctx, if_true.lo(), if_false.lo(), condition); + HiLo::from_hi_lo([hi, lo]) +} + +pub fn select_hi_lo_by_indicator( + ctx: &mut Context, + gate: &impl GateInstructions, + values: &[HiLo>], + indicator: Vec>, +) -> HiLo> { + let his = values.iter().map(|hilo| hilo.hi()); + let los = values.iter().map(|hilo| hilo.lo()); + let hi = gate.select_by_indicator(ctx, his, indicator.clone()); + let lo = gate.select_by_indicator(ctx, los, indicator); + HiLo::from_hi_lo([hi, lo]) +} + +// Is there a more Rust way to do this? +/// Conversion from `&[SafeByte]` to `Vec>` +pub fn safe_bytes_vec_into(bytes: &[SafeByte]) -> Vec> { + bytes.iter().map(|b| *b.as_ref()).collect() +} + +/// Conversion from [SafeBytes32] to [HiLo] +pub fn safe_bytes32_to_hi_lo( + ctx: &mut Context, + gate: &impl GateInstructions, + bytes: &SafeBytes32, +) -> HiLo> { + let bytes = SafeTypeChip::unsafe_to_fix_len_bytes_vec(bytes.value().to_vec(), 32); + HiLo::from_hi_lo(bytes_be_to_u128(ctx, gate, bytes.bytes()).try_into().unwrap()) +} + +/// Conversion from the MPT root as bytes32 to [HiLo]. Unsafe because this assumes that +/// the root bytes are constrained to be bytes somewhere else. +pub fn unsafe_mpt_root_to_hi_lo( + ctx: &mut Context, + gate: &impl GateInstructions, + proof: &MPTProof, +) -> HiLo> { + let bytes = SafeTypeChip::unsafe_to_fix_len_bytes_vec(proof.root_hash_bytes.clone(), 32); + HiLo::from_hi_lo(bytes_be_to_u128(ctx, gate, bytes.bytes()).try_into().unwrap()) +} + +pub fn encode_const_u8_to_safe_bytes( + ctx: &mut Context, + constant: u8, +) -> [SafeByte; 1] { + let encoded = constant.to_be_bytes().map(|b| F::from(b as u64)); + let raw = ctx.load_constants(&encoded).try_into().unwrap(); + SafeTypeChip::unsafe_to_fix_len_bytes::<1>(raw).into_bytes() +} + +pub fn encode_const_u16_to_safe_bytes( + ctx: &mut Context, + constant: u16, +) -> [SafeByte; 2] { + let encoded = constant.to_be_bytes().map(|b| F::from(b as u64)); + let raw = ctx.load_constants(&encoded).try_into().unwrap(); + SafeTypeChip::unsafe_to_fix_len_bytes::<2>(raw).into_bytes() +} + +pub fn encode_const_u32_to_safe_bytes( + ctx: &mut Context, + constant: u32, +) -> [SafeByte; 4] { + let encoded = constant.to_be_bytes().map(|b| F::from(b as u64)); + let raw = ctx.load_constants(&encoded).try_into().unwrap(); + SafeTypeChip::unsafe_to_fix_len_bytes::<4>(raw).into_bytes() +} + +pub fn encode_const_u64_to_safe_bytes( + ctx: &mut Context, + constant: u64, +) -> [SafeByte; 8] { + let encoded = constant.to_be_bytes().map(|b| F::from(b as u64)); + let raw = ctx.load_constants(&encoded).try_into().unwrap(); + SafeTypeChip::unsafe_to_fix_len_bytes::<8>(raw).into_bytes() +} diff --git a/axiom-eth/src/utils/circuit_utils/mod.rs b/axiom-eth/src/utils/circuit_utils/mod.rs new file mode 100644 index 00000000..068a25c8 --- /dev/null +++ b/axiom-eth/src/utils/circuit_utils/mod.rs @@ -0,0 +1,217 @@ +use std::{cmp::max, ops::Range}; + +use halo2_base::{ + gates::{GateInstructions, RangeInstructions}, + safe_types::{SafeBool, SafeTypeChip}, + utils::{bit_length, ScalarField}, + AssignedValue, Context, + QuantumCell::Constant, +}; + +use itertools::Itertools; + +pub mod bytes; + +// save typing.. +/// See [GateInstructions::is_equal] +pub fn is_equal_usize( + ctx: &mut Context, + gate: &impl GateInstructions, + a: AssignedValue, + b: usize, +) -> SafeBool { + SafeTypeChip::unsafe_to_bool(gate.is_equal(ctx, a, Constant(F::from(b as u64)))) +} + +/// See [RangeInstructions::is_less_than] +pub fn is_lt_usize( + ctx: &mut Context, + range: &impl RangeInstructions, + a: AssignedValue, + b: usize, + max_bits: usize, +) -> SafeBool { + let bit = range.is_less_than(ctx, a, Constant(F::from(b as u64)), max_bits); + SafeTypeChip::unsafe_to_bool(bit) +} + +/// See [RangeInstructions::is_less_than] +/// `a >= b` iff `b - 1 < a` +pub fn is_gte_usize( + ctx: &mut Context, + range: &impl RangeInstructions, + a: AssignedValue, + b: usize, + max_bits: usize, +) -> SafeBool { + let bit = if b == 0 { + ctx.load_constant(F::ONE) + } else { + range.is_less_than(ctx, Constant(F::from(b as u64 - 1)), a, max_bits) + }; + SafeTypeChip::unsafe_to_bool(bit) +} + +/// Returns whether `a` is in the range `[range.start, range.end)`. +/// Assumes `a` and `range.end` are both less than `2^max_bits`. +pub fn is_in_range( + ctx: &mut Context, + range_chip: &impl RangeInstructions, + a: AssignedValue, + range: Range, + max_bits: usize, +) -> SafeBool { + let is_gte = is_gte_usize(ctx, range_chip, a, range.start, max_bits); + let is_lt = is_lt_usize(ctx, range_chip, a, range.end, max_bits); + let is_in_range = range_chip.gate().and(ctx, *is_gte.as_ref(), *is_lt.as_ref()); + SafeTypeChip::unsafe_to_bool(is_in_range) +} + +/// Returns `min(a, b)`. +/// Assumes `a` and `b` are both less than `2^max_bits`. +pub fn min_with_usize( + ctx: &mut Context, + range: &impl RangeInstructions, + a: AssignedValue, + b: usize, + max_bits: usize, +) -> AssignedValue { + let const_b = Constant(F::from(b as u64)); + let lt = range.is_less_than(ctx, a, const_b, max_bits); + range.gate().select(ctx, a, const_b, lt) +} + +/// Creates the length `len` array `mask` with `mask[i] = i < threshold ? 1 : 0`. +/// +/// Denoted `unsafe` because it assumes that `threshold <= len`. +pub fn unsafe_lt_mask( + ctx: &mut Context, + gate: &impl GateInstructions, + threshold: AssignedValue, + len: usize, +) -> Vec> { + let t = threshold.value().get_lower_64() as usize; + let mut last = None; + let mask = (0..len) + .map(|i| { + let mut bit = ctx.load_witness(F::from(i < t)); + gate.assert_bit(ctx, bit); + // constrain the list goes 1, ..., 1, 0, 0, ..., 0 + if let Some(last) = last { + bit = gate.and(ctx, bit, last); + } + last = Some(bit); + bit + }) + .collect_vec(); + let sum = gate.sum(ctx, mask.clone()); + ctx.constrain_equal(&sum, &threshold); + mask.into_iter().map(|x| SafeTypeChip::unsafe_to_bool(x)).collect() +} + +/// Constrains that `array[i] = 0` for `i > len`. +/// +/// ## Assumptions +/// - Marked unsafe because we assume `len <= array.len()` +pub fn unsafe_constrain_trailing_zeros( + ctx: &mut Context, + gate: &impl GateInstructions, + array: &mut [AssignedValue], + len: AssignedValue, +) { + let mask = unsafe_lt_mask(ctx, gate, len, array.len()); + for (byte, mask) in array.iter_mut().zip(mask) { + *byte = gate.mul(ctx, *byte, mask); + } +} + +pub fn log2_ceil( + ctx: &mut Context, + gate: &impl GateInstructions, + x: AssignedValue, + max_bits: usize, +) -> AssignedValue { + let mut bits = gate.num_to_bits(ctx, x, max_bits); + let total_bits = gate.sum(ctx, bits.clone()); + for i in (0..max_bits - 1).rev() { + bits[i] = gate.or(ctx, bits[i], bits[i + 1]); + } + let bit_length = gate.sum(ctx, bits); + let is_pow2 = gate.is_equal(ctx, total_bits, Constant(F::ONE)); + gate.sub(ctx, bit_length, is_pow2) +} + +/// Returns `array[chunk_size * idx.. chunk_size * (idx+1)]`. +/// +/// Assumes that `chunk_size * idx < array.len()`. Otherwise will return all zeros. +pub fn extract_array_chunk( + ctx: &mut Context, + gate: &impl GateInstructions, + array: &[AssignedValue], + idx: AssignedValue, + chunk_size: usize, +) -> Vec> { + let num_chunks = (array.len() + chunk_size - 1) / chunk_size; + let indicator = gate.idx_to_indicator(ctx, idx, num_chunks); + let const_zero = ctx.load_zero(); + (0..chunk_size) + .map(|i| { + let entries = + (0..num_chunks).map(|j| *array.get(chunk_size * j + i).unwrap_or(&const_zero)); + gate.select_by_indicator(ctx, entries, indicator.clone()) + }) + .collect() +} + +/// Returns `array[chunk_size * idx.. chunk_size * (idx+1)]` and constrains that any entries beyond `len` must be zero. +/// +/// Also returns `chunk_size * idx < len` as [SafeBool]. +/// +/// ## Assumptions +/// - `array.len()` is fixed at compile time +/// - `len <= array.len()` +/// - `idx` has been range checked to have at most `idx_max_bits` bits +pub fn extract_array_chunk_and_constrain_trailing_zeros( + ctx: &mut Context, + range: &impl RangeInstructions, + array: &[AssignedValue], + len: AssignedValue, + idx: AssignedValue, + chunk_size: usize, + idx_max_bits: usize, +) -> (Vec>, SafeBool) { + let gate = range.gate(); + let mut chunk = extract_array_chunk(ctx, gate, array, idx, chunk_size); + let chunk_size = chunk_size as u64; + let start = gate.mul(ctx, idx, Constant(F::from(chunk_size))); + let max_len_bits = bit_length(array.len() as u64); + // not worth optimizing: + let max_bits = max(max_len_bits, idx_max_bits + bit_length(chunk_size)); + let is_lt = range.is_less_than(ctx, start, len, max_bits); + // chunk_len = min(len - idx * chunk_size, chunk_size) + let mut chunk_len = gate.sub(ctx, len, start); + chunk_len = gate.mul(ctx, chunk_len, is_lt); + chunk_len = min_with_usize(ctx, range, chunk_len, chunk_size as usize, max_len_bits); + unsafe_constrain_trailing_zeros(ctx, gate, &mut chunk, chunk_len); + + (chunk, SafeTypeChip::unsafe_to_bool(is_lt)) +} + +/// Given fixed length array `array` checks that either `array[0]` is nonzero or `var_len == 0`. Does nothing if `array` is empty. +/// +/// There technically does not need to be any relation between `array` and `var_len` in this implementation. However the usual use case is where `array` is a fixed length array that is meant to represent a variable length array of length `var_len`. +pub fn constrain_no_leading_zeros( + ctx: &mut Context, + gate: &impl GateInstructions, + array: &[AssignedValue], + var_len: AssignedValue, +) { + if array.is_empty() { + return; + } + let leading_zero = gate.is_zero(ctx, array[0]); + let mut no_leading_zero = gate.not(ctx, leading_zero); + let len_is_zero = gate.is_zero(ctx, var_len); + no_leading_zero = gate.or(ctx, no_leading_zero, len_is_zero); + gate.assert_is_const(ctx, &no_leading_zero, &F::ONE); +} diff --git a/axiom-eth/src/utils/component/circuit/comp_circuit_impl.rs b/axiom-eth/src/utils/component/circuit/comp_circuit_impl.rs new file mode 100644 index 00000000..f5f487f0 --- /dev/null +++ b/axiom-eth/src/utils/component/circuit/comp_circuit_impl.rs @@ -0,0 +1,441 @@ +use std::{ + any::Any, + borrow::BorrowMut, + cell::RefCell, + ops::DerefMut, + sync::{Arc, Mutex}, +}; + +#[cfg(feature = "aggregation")] +use crate::utils::build_utils::aggregation::CircuitMetadata; +use crate::{ + rlc::circuit::{builder::RlcCircuitBuilder, RlcCircuitParams, RlcConfig}, + rlc::virtual_region::RlcThreadBreakPoints, + utils::{ + build_utils::pinning::{ + CircuitPinningInstructions, Halo2CircuitPinning, RlcCircuitPinning, + }, + component::{ + circuit::{CoreBuilder, CoreBuilderOutput, PromiseBuilder}, + promise_collector::{PromiseCaller, PromiseCollector, SharedPromiseCollector}, + promise_loader::comp_loader::ComponentCommiter, + types::ComponentPublicInstances, + utils::create_hasher, + ComponentCircuit, ComponentPromiseResultsInMerkle, ComponentType, ComponentTypeId, + GroupedPromiseCalls, GroupedPromiseResults, LogicalInputValue, PromiseShardMetadata, + }, + DEFAULT_RLC_CACHE_BITS, + }, +}; +use anyhow::anyhow; +use halo2_base::{ + gates::{circuit::CircuitBuilderStage, GateChip}, + halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner}, + plonk::{Circuit, ConstraintSystem, Error}, + }, + virtual_region::manager::VirtualRegionManager, + AssignedValue, +}; +use itertools::Itertools; + +use crate::Field; +#[cfg(feature = "aggregation")] +use snark_verifier_sdk::CircuitExt; + +#[derive(Clone, Debug)] +pub struct ComponentCircuitImpl, P: PromiseBuilder> { + pub rlc_builder: RefCell>, + pub promise_collector: SharedPromiseCollector, + pub core_builder: RefCell, + pub promise_builder: RefCell

, + pub val_public_instances: RefCell>>, +} + +impl, P: PromiseBuilder> ComponentCircuitImpl { + /// Create a new component circuit. + pub fn new( + core_builder_params: C::Params, + promise_builder_params: P::Params, + prompt_rlc_params: RlcCircuitParams, + ) -> Self { + Self::new_impl(false, core_builder_params, promise_builder_params, prompt_rlc_params) + } + /// Create a new component circuit, with special prover-only optimizations + /// when `witness_gen_only` is true. + pub fn new_impl( + witness_gen_only: bool, + core_builder_params: C::Params, + promise_builder_params: P::Params, + prompt_rlc_params: RlcCircuitParams, + ) -> Self { + let stage = + if witness_gen_only { CircuitBuilderStage::Prover } else { CircuitBuilderStage::Mock }; + Self::new_from_stage(stage, core_builder_params, promise_builder_params, prompt_rlc_params) + } + /// Create a new component circuit, with special prover-only optimizations + /// when `witness_gen_only` is true. When `stage` is `Keygen`, `use_unknown` + /// is set to true in [RlcCircuitBuilder], and `promise_collector` does not + /// check promise results have been provided before fulfilling. + pub fn new_from_stage( + stage: CircuitBuilderStage, + core_builder_params: C::Params, + promise_builder_params: P::Params, + prompt_rlc_params: RlcCircuitParams, + ) -> Self { + let mut rlc_builder = RlcCircuitBuilder::from_stage(stage, DEFAULT_RLC_CACHE_BITS) + .use_params(prompt_rlc_params); + // Public instances are fully managed by ComponentCircuitImpl. + rlc_builder.base.set_instance_columns(1); + Self { + rlc_builder: RefCell::new(rlc_builder), + promise_collector: Arc::new(Mutex::new(PromiseCollector::new( + P::get_component_type_dependencies(), + ))), + core_builder: RefCell::new(C::new(core_builder_params)), + promise_builder: RefCell::new(P::new(promise_builder_params)), + val_public_instances: RefCell::new(None), + } + } + pub fn use_break_points(self, break_points: RlcThreadBreakPoints) -> Self { + self.rlc_builder.borrow_mut().set_break_points(break_points); + self + } + pub fn prover( + core_builder_params: C::Params, + promise_builder_params: P::Params, + prompt_rlc_pinning: RlcCircuitPinning, + ) -> Self { + Self::new_impl(true, core_builder_params, promise_builder_params, prompt_rlc_pinning.params) + .use_break_points(prompt_rlc_pinning.break_points) + } + + /// Calculate params. This should be called only after all promise results are fulfilled. + pub fn calculate_params(&mut self) -> as Circuit>::Params { + self.virtual_assign_phase0().expect("virtual assign phase0 failed"); + self.virtual_assign_phase1(); + + let result = ( + self.core_builder.borrow_mut().calculate_params(), + self.promise_builder.borrow_mut().calculate_params(), + self.rlc_builder.borrow_mut().calculate_params(Some(9)), + ); + + // clear in case synthesize is called multiple times + self.clear_witnesses(); + + self.rlc_builder.borrow_mut().set_params(result.2.clone()); + + result + } + + pub fn virtual_assign_phase0(&self) -> Result<(), Error> { + let mut borrowed_rlc_builder = self.rlc_builder.borrow_mut(); + let rlc_builder = borrowed_rlc_builder.deref_mut(); + + let mut core_builder = self.core_builder.borrow_mut(); + let mut promise_builder = self.promise_builder.borrow_mut(); + + { + let mut borrowed_promise_collector = self.promise_collector.lock().unwrap(); + let promise_collector = borrowed_promise_collector.deref_mut(); + promise_builder.virtual_assign_phase0(rlc_builder, promise_collector); + } + + let CoreBuilderOutput { public_instances: other_pis, virtual_table: vt, .. } = core_builder + .virtual_assign_phase0(rlc_builder, PromiseCaller::new(self.promise_collector.clone())); + let output_commit = + <>::CompType as ComponentType>::Commiter::compute_commitment( + &mut rlc_builder.base, + &vt, + ); + + let mut borrowed_promise_collector = self.promise_collector.lock().unwrap(); + let promise_collector = borrowed_promise_collector.deref_mut(); + let public_instances = self.generate_public_instances( + rlc_builder, + promise_collector, + &P::get_component_type_dependencies(), + output_commit, + other_pis, + )?; + + let pis = rlc_builder.public_instances(); + pis[0] = public_instances.into(); + Ok(()) + } + + fn virtual_assign_phase1(&self) { + let mut rlc_builder = self.rlc_builder.borrow_mut(); + + // Load promise results first in case the core builder depends on them. + { + let mut promise_collector = self.promise_collector.lock().unwrap(); + self.promise_builder + .borrow_mut() + .virtual_assign_phase1(&mut rlc_builder, promise_collector.deref_mut()); + } + self.core_builder.borrow_mut().virtual_assign_phase1(&mut rlc_builder); + } + + fn generate_public_instances( + &self, + rlc_builder: &mut RlcCircuitBuilder, + promise_collector: &PromiseCollector, + dependencies: &[ComponentTypeId], + output_commit: AssignedValue, + other_pis: Vec>, + ) -> Result>, Error> { + let mut promise_commits = Vec::with_capacity(dependencies.len()); + for component_type_id in dependencies { + if let Some(commit) = + promise_collector.get_commit_by_component_type_id(component_type_id) + { + promise_commits.push(commit); + } + } + let gate_chip = GateChip::new(); + + let ctx = rlc_builder.base.main(0); + let mut hasher = create_hasher::(); + hasher.initialize_consts(ctx, &gate_chip); + let promise_commit = hasher.hash_fix_len_array(ctx, &gate_chip, &promise_commits); + + let public_instances = ComponentPublicInstances::> { + output_commit, + promise_result_commit: promise_commit, + other: other_pis, + }; + if promise_collector.promise_results_ready() { + *self.val_public_instances.borrow_mut() = Some(public_instances.clone().into()); + } + Ok(public_instances) + } +} + +impl, P: PromiseBuilder> ComponentCircuit + for ComponentCircuitImpl +{ + fn clear_witnesses(&self) { + self.rlc_builder.borrow_mut().clear(); + self.promise_collector.lock().unwrap().clear_witnesses(); + self.core_builder.borrow_mut().clear_witnesses(); + self.promise_builder.borrow_mut().clear_witnesses(); + } + /// **Warning:** the returned deduped calls ordering is not deterministic. + fn compute_promise_calls(&self) -> anyhow::Result { + let mut borrowed_rlc_builder = self.rlc_builder.borrow_mut(); + let rlc_builder = borrowed_rlc_builder.deref_mut(); + let mut borrowed_core_builder = self.core_builder.borrow_mut(); + let core_builder = borrowed_core_builder.deref_mut(); + + core_builder + .virtual_assign_phase0(rlc_builder, PromiseCaller::new(self.promise_collector.clone())); + let mut borrowed_promise_collector = self.promise_collector.lock().unwrap(); + let deduped_calls = borrowed_promise_collector.get_deduped_calls(); + + // clear in case synthesize is called multiple times + core_builder.clear_witnesses(); + borrowed_promise_collector.clear_witnesses(); + rlc_builder.clear(); + + Ok(deduped_calls) + } + + /// Feed inputs into the core builder. The `input` type should be the `::CoreInput` type. + /// It is the caller's responsibility to ensure that the capacity of the input + /// is equal to the configured capacity of the component circuit. This function + /// does **not** check this. + fn feed_input(&self, input: Box) -> anyhow::Result<()> { + let typed_input = input + .as_ref() + .downcast_ref::<>::CoreInput>() + .ok_or_else(|| anyhow!("invalid input type"))? + .clone(); + self.core_builder.borrow_mut().feed_input(typed_input)?; + Ok(()) + } + + fn fulfill_promise_results( + &self, + promise_results: &GroupedPromiseResults, + ) -> anyhow::Result<()> { + let mut borrowed_promise_collector = self.promise_collector.lock().unwrap(); + let promise_collector = borrowed_promise_collector.deref_mut(); + promise_collector.fulfill(promise_results); + + self.promise_builder.borrow_mut().fulfill_promise_results(promise_collector); + Ok(()) + } + + /// When inputs and promise results are ready, we can generate outputs of this component. + /// + /// Return logical outputs and the output commit of the virtual table. The output commit is calculated using the configured capacity in the params. + /// But the returned metadata capacity is the true used capacity of the component based on the inputs, **not** the configured capacity. + fn compute_outputs(&self) -> anyhow::Result> { + self.promise_collector.lock().unwrap().set_promise_results_ready(true); + + let mut borrowed_rlc_builder = self.rlc_builder.borrow_mut(); + let rlc_builder = borrowed_rlc_builder.deref_mut(); + let mut borrowed_core_builder = self.core_builder.borrow_mut(); + let core_builder = borrowed_core_builder.deref_mut(); + + let CoreBuilderOutput { virtual_table: vt, logical_results, .. } = core_builder + .virtual_assign_phase0(rlc_builder, PromiseCaller::new(self.promise_collector.clone())); + let capacity: usize = logical_results.iter().map(|lr| lr.input.get_capacity()).sum(); + + let vt = vt.into_iter().map(|(v_i, v_o)| (v_i.into(), v_o.into())).collect_vec(); + let output_commit_val = + <>::CompType as ComponentType>::Commiter::compute_native_commitment( + &vt, + ); + // Release RefCell for clear_witnesses later. + drop(borrowed_rlc_builder); + drop(borrowed_core_builder); + self.clear_witnesses(); + + Ok(ComponentPromiseResultsInMerkle::::new( + vec![PromiseShardMetadata { commit: output_commit_val, capacity }], + vec![(0, logical_results.into_iter().map(|lr| lr.into()).collect())], + )) + } + + fn get_public_instances(&self) -> ComponentPublicInstances { + let has_pi_value = self.val_public_instances.borrow().is_some(); + if !has_pi_value { + self.promise_collector.lock().unwrap().set_promise_results_ready(true); + self.virtual_assign_phase0().unwrap(); + self.clear_witnesses(); + } + self.val_public_instances.borrow().as_ref().unwrap().clone() + } +} + +impl, P: PromiseBuilder> Circuit + for ComponentCircuitImpl +{ + type Config = (C::Config, P::Config, RlcConfig); + type Params = (C::Params, P::Params, RlcCircuitParams); + type FloorPlanner = SimpleFloorPlanner; + + fn params(&self) -> Self::Params { + ( + self.core_builder.borrow().get_params(), + self.promise_builder.borrow().get_params(), + self.rlc_builder.borrow().params(), + ) + } + + fn without_witnesses(&self) -> Self { + unimplemented!() + } + + fn configure_with_params(meta: &mut ConstraintSystem, params: Self::Params) -> Self::Config { + let k = params.2.base.k; + let core_config = C::configure_with_params(meta, params.0); + let mut rlc_config = RlcConfig::configure(meta, params.2); + // There must be some phase 0 columns before creating phase 1 columns. + let promise_config = P::configure_with_params(meta, params.1); + // This is really tricky.. + let usable_rows = (1 << k) - meta.minimum_rows(); + rlc_config.set_usable_rows(usable_rows); + (core_config, promise_config, rlc_config) + } + + fn configure(_: &mut ConstraintSystem) -> Self::Config { + unreachable!() + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + // Promise results must be ready at this point, unless in Keygen mode. + if !self.rlc_builder.borrow().base.core().use_unknown() { + self.promise_collector.lock().unwrap().set_promise_results_ready(true); + } + config.2.base.initialize(&mut layouter); + self.virtual_assign_phase0()?; + { + let mut core_builder = self.core_builder.borrow_mut(); + let mut promise_builder = self.promise_builder.borrow_mut(); + let rlc_builder = self.rlc_builder.borrow_mut(); + + let mut phase0_layouter = layouter.namespace(|| "raw synthesize phase0"); + core_builder.borrow_mut().raw_synthesize_phase0(&config.0, &mut phase0_layouter); + promise_builder.raw_synthesize_phase0(&config.1, &mut phase0_layouter); + rlc_builder.raw_synthesize_phase0(&config.2, phase0_layouter); + } + #[cfg(feature = "halo2-axiom")] + { + layouter.next_phase(); + } + self.rlc_builder + .borrow_mut() + .load_challenge(&config.2, layouter.namespace(|| "load challenges")); + + self.virtual_assign_phase1(); + { + let mut core_builder = self.core_builder.borrow_mut(); + let mut promise_builder = self.promise_builder.borrow_mut(); + let rlc_builder = self.rlc_builder.borrow(); + + let mut phase1_layouter = + layouter.namespace(|| "Core + RlcCircuitBuilder raw synthesize phase1"); + core_builder.raw_synthesize_phase1(&config.0, &mut phase1_layouter); + rlc_builder.raw_synthesize_phase1(&config.2, phase1_layouter, false); + promise_builder.raw_synthesize_phase1(&config.1, &mut layouter); + } + + let rlc_builder = self.rlc_builder.borrow(); + if !rlc_builder.witness_gen_only() { + layouter.assign_region( + || "copy constraints", + |mut region| { + let constant_cols = config.2.base.constants(); + rlc_builder.copy_manager().assign_raw(constant_cols, &mut region); + Ok(()) + }, + )?; + } + drop(rlc_builder); + + // clear in case synthesize is called multiple times + self.clear_witnesses(); + + Ok(()) + } +} + +// TODO: Maybe change? +impl, P: PromiseBuilder> CircuitPinningInstructions + for ComponentCircuitImpl +{ + type Pinning = RlcCircuitPinning; + fn pinning(&self) -> Self::Pinning { + let break_points = self.rlc_builder.borrow().break_points(); + let params = self.rlc_builder.borrow().params(); + RlcCircuitPinning::new(params, break_points) + } +} + +#[cfg(feature = "aggregation")] +impl, P: PromiseBuilder> CircuitExt + for ComponentCircuitImpl +where + C: CircuitMetadata, +{ + fn accumulator_indices() -> Option> { + C::accumulator_indices() + } + + fn instances(&self) -> Vec> { + let res = vec![self.get_public_instances().into()]; + res + } + + fn num_instance(&self) -> Vec { + vec![self.instances()[0].len()] + } +} diff --git a/axiom-eth/src/utils/component/circuit/mod.rs b/axiom-eth/src/utils/component/circuit/mod.rs new file mode 100644 index 00000000..672998ff --- /dev/null +++ b/axiom-eth/src/utils/component/circuit/mod.rs @@ -0,0 +1,139 @@ +use crate::Field; +use crate::{rlc::circuit::builder::RlcCircuitBuilder, utils::build_utils::dummy::DummyFrom}; +use getset::Getters; +use halo2_base::{ + halo2_proofs::{circuit::Layouter, plonk::ConstraintSystem}, + AssignedValue, +}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +use super::{ + promise_collector::{ + PromiseCaller, PromiseCallsGetter, PromiseCommitSetter, PromiseResultsGetter, + }, + promise_loader::comp_loader::SingleComponentLoaderParams, + types::FixLenLogical, + ComponentType, ComponentTypeId, FlattenVirtualTable, LogicalResult, +}; + +mod comp_circuit_impl; +pub use comp_circuit_impl::ComponentCircuitImpl; + +pub trait ComponentBuilder { + type Config: Clone = (); + type Params: Clone + Default = (); + + /// Create Self. + fn new(params: Self::Params) -> Self; + /// Get params for this module. + fn get_params(&self) -> Self::Params; + /// Clear all stored witnesses. Data should be untouched. + fn clear_witnesses(&mut self) {} + /// Configure this module with params. + // Alas we cannot have default implementation only for Self::Config = (). + fn configure_with_params(meta: &mut ConstraintSystem, params: Self::Params) -> Self::Config; + /// Calculate params for this module. + fn calculate_params(&mut self) -> Self::Params; +} + +/// CoreBuilder must specify its output format. +pub trait CoreBuilderParams { + /// Return the output capacity. + fn get_output_params(&self) -> CoreBuilderOutputParams; +} +#[derive(Clone, Default, Getters)] +pub struct CoreBuilderOutputParams { + /// Capacities for each shard. + #[getset(get = "pub")] + cap_per_shard: Vec, +} +impl CoreBuilderOutputParams { + /// create a CoreBuilderOutputParams + pub fn new(cap_per_shard: Vec) -> Self { + assert!(cap_per_shard.is_empty() || cap_per_shard.len().is_power_of_two()); + Self { cap_per_shard } + } +} +impl CoreBuilderParams for CoreBuilderOutputParams { + fn get_output_params(&self) -> CoreBuilderOutputParams { + self.clone() + } +} +/// Input for CoreBuilder. +/// TODO: specify its output capacity. +pub trait CoreBuilderInput = Serialize + DeserializeOwned + Clone + 'static; + +/// Output for CoreBuilder which is determined at phase0. +pub struct CoreBuilderOutput> { + /// Public instances except the output commit. + pub public_instances: Vec>, + /// Flatten output virtual table. + pub virtual_table: FlattenVirtualTable>, + /// Value of logical results. + pub logical_results: Vec>, +} + +pub trait CoreBuilder: ComponentBuilder { + type CompType: ComponentType; + type PublicInstanceValue: FixLenLogical; + type PublicInstanceWitness: FixLenLogical>; + type CoreInput: CoreBuilderInput + DummyFrom; + /// Feed inputs to this module. + fn feed_input(&mut self, input: Self::CoreInput) -> anyhow::Result<()>; + /// Generate witnesses for phase0. Any data passing to other steps should be stored inside `self`. + /// Return `(, )`. + fn virtual_assign_phase0( + &mut self, + // TODO: This could be replaced with a more generic CircuitBuilder. Question: can be CircuitBuilder treated as something like PromiseCircuit? + builder: &mut RlcCircuitBuilder, + // Core circuits can make promise calls. + promise_caller: PromiseCaller, + // TODO: Output commitmment + ) -> CoreBuilderOutput; + /// Synthesize for phase0. Any data passing to other steps should be stored inside `self`. + #[allow(unused_variables)] + fn raw_synthesize_phase0(&mut self, config: &Self::Config, layouter: &mut impl Layouter) {} + /// Generate witnesses for phase1. Any data passing to other steps should be stored inside `self`. + #[allow(unused_variables)] + fn virtual_assign_phase1(&mut self, builder: &mut RlcCircuitBuilder) {} + /// Synthesize for phase1. Any data passing to other steps should be stored inside `self`. + #[allow(unused_variables)] + fn raw_synthesize_phase1(&mut self, config: &Self::Config, layouter: &mut impl Layouter) {} +} + +#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug)] +pub struct LoaderParamsPerComponentType { + pub component_type_id: ComponentTypeId, + pub loader_params: SingleComponentLoaderParams, +} + +/// A ComponentModule load promise results from other components and owns some columns and corresponding gates. +/// All ComponentModules in a single circuit share a RlcCircuitBuilder and they communicate with each other through PromiseCollector. +pub trait PromiseBuilder: ComponentBuilder { + /// Get component type dependencies of this ComponentBuilder in a determinstic order. + fn get_component_type_dependencies() -> Vec; + /// Extract loader params per component type from circuit params. + /// Assumption: Return value is in a deterministic order which we use to compute the promise commit. + fn extract_loader_params_per_component_type( + params: &Self::Params, + ) -> Vec; + /// Fulfill promise results. + fn fulfill_promise_results(&mut self, promise_results_getter: &impl PromiseResultsGetter); + /// Generate witnesses for phase0. Any data passing to other steps should be stored inside `self`. + /// Also need to set promise result commits. + fn virtual_assign_phase0( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_commit_setter: &mut impl PromiseCommitSetter, + ); + /// Synthesize for phase0. Any data passing to other steps should be stored inside `self`. + fn raw_synthesize_phase0(&mut self, config: &Self::Config, layouter: &mut impl Layouter); + /// Generate witnesses for phase1. Any data passing to other steps should be stored inside `self`. + fn virtual_assign_phase1( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_calls_getter: &mut impl PromiseCallsGetter, + ); + /// Synthesize for phase1. Any data passing to other steps should be stored inside `self`. + fn raw_synthesize_phase1(&mut self, config: &Self::Config, layouter: &mut impl Layouter); +} diff --git a/axiom-eth/src/utils/component/mod.rs b/axiom-eth/src/utils/component/mod.rs new file mode 100644 index 00000000..d7f74e67 --- /dev/null +++ b/axiom-eth/src/utils/component/mod.rs @@ -0,0 +1,279 @@ +use std::{any::Any, collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData}; + +use crate::{Field, RawField}; +use getset::Getters; +use halo2_base::{ + gates::{circuit::builder::BaseCircuitBuilder, GateInstructions, RangeChip}, + halo2_proofs::halo2curves::bn256::Fr, + AssignedValue, Context, +}; +use itertools::Itertools; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use static_assertions::assert_impl_all; + +use crate::rlc::chip::RlcChip; + +use self::{ + promise_loader::{ + comp_loader::{BasicComponentCommiter, ComponentCommiter}, + flatten_witness_to_rlc, + }, + types::{ComponentPublicInstances, FixLenLogical, Flatten}, + utils::{into_key, try_from_key}, +}; + +pub mod circuit; +pub mod param; +pub mod promise_collector; +pub mod promise_loader; +#[cfg(test)] +mod tests; +pub mod types; +pub mod utils; + +pub type ComponentId = u64; +pub type ComponentTypeId = String; +pub const USER_COMPONENT_ID: ComponentId = 0; + +/// Unified representation of a logical input of a component type. +/// TODO: Can this be extended to variable length output? +/// In the caller end, there could be multiple formats of promise calls for a component type. e.g. +/// fix/var length array to keccak. But in the receiver end, we only need to know the logical input. +/// In the receiver end, a logical input could take 1(fix len input) or multiple virtual rows(var len). +/// The number of virtual rows a logical input take is "capacity". +pub trait LogicalInputValue: + Debug + Send + Sync + Clone + Eq + Serialize + DeserializeOwned + 'static +{ + /// Get the capacity of this logical input. + /// The default implementaion is for the fixed length case. + fn get_capacity(&self) -> usize; +} +/// A format of a promise call to component type T. +pub trait PromiseCallWitness: Debug + Send + Sync + 'static { + /// The component type this promise call is for. + fn get_component_type_id(&self) -> ComponentTypeId; + /// Get the capacity of this promise call. + fn get_capacity(&self) -> usize; + /// Encode the promise call into RLC. + /// TODO: maybe pass builder here for better flexiability? but constructing chips are slow. + fn to_rlc( + &self, + ctx_pair: (&mut Context, &mut Context), + range_chip: &RangeChip, + rlc_chip: &RlcChip, + ) -> AssignedValue; + /// Get the logical input of this promise call. + fn to_typeless_logical_input(&self) -> TypelessLogicalInput; + /// Get dummy output of this promise call. + fn get_mock_output(&self) -> Flatten; + /// Enable downcasting + fn as_any(&self) -> &dyn Any; +} + +/// The flatten version of output of a component. +pub type FlattenVirtualTable = Vec>; +/// A flatten virtual row in a virtual table. +pub type FlattenVirtualRow = (Flatten, Flatten); + +/// Logical result of a component type. +#[derive(Clone)] +pub struct LogicalResult> { + pub input: T::LogicalInput, + pub output: T::OutputValue, + pub _marker: PhantomData, +} +impl> LogicalResult { + /// Create LogicalResult + pub fn new(input: T::LogicalInput, output: T::OutputValue) -> Self { + Self { input, output, _marker: PhantomData } + } +} +impl> TryFrom> for LogicalResult { + type Error = anyhow::Error; + fn try_from(value: ComponentPromiseResult) -> Result { + let (input, output) = value; + let input = try_from_key::(&input)?; + Ok(Self::new(input, T::OutputValue::try_from_raw(output)?)) + } +} +impl> From> for ComponentPromiseResult { + fn from(value: LogicalResult) -> Self { + let LogicalResult { input, output, .. } = value; + (into_key(input), output.into_raw()) + } +} +impl> From> for Vec> { + fn from(value: LogicalResult) -> Self { + let logical_virtual_rows = T::logical_result_to_virtual_rows(&value); + logical_virtual_rows + .into_iter() + .map(|(input, output)| (input.into(), output.into())) + .collect_vec() + } +} +/// Specify the logical types of a component type. +pub trait ComponentType: 'static + Sized { + type InputValue: FixLenLogical; + type InputWitness: FixLenLogical>; + type OutputValue: FixLenLogical; + type OutputWitness: FixLenLogical>; + type LogicalInput: LogicalInputValue; + type Commiter: ComponentCommiter = BasicComponentCommiter; + + /// Get ComponentTypeId of this component type. + fn get_type_id() -> ComponentTypeId; + /// Get ComponentTypeName for logging/debugging. + fn get_type_name() -> &'static str { + std::any::type_name::() + } + + /// Wrap logical_result_to_virtual_rows_impl with sanity check. + fn logical_result_to_virtual_rows( + ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + let v_rows = Self::logical_result_to_virtual_rows_impl(ins); + assert_eq!(v_rows.len(), ins.input.get_capacity()); + v_rows + } + /// Convert a logical result to 1 or multiple virtual rows. + fn logical_result_to_virtual_rows_impl( + ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)>; + + /// Wrap logical_input_to_virtual_rows_impl with sanity check. + /// TODO: we are not using this. + fn logical_input_to_virtual_rows(li: &Self::LogicalInput) -> Vec { + let v_rows = Self::logical_input_to_virtual_rows_impl(li); + assert_eq!(v_rows.len(), li.get_capacity()); + v_rows + } + /// Real implementation to convert a logical input to virtual rows. + fn logical_input_to_virtual_rows_impl(li: &Self::LogicalInput) -> Vec; + + /// RLC virtual rows. A logical input might take multiple virtual rows. + /// The default implementation is for the fixed length case. + fn rlc_virtual_rows( + (gate_ctx, rlc_ctx): (&mut Context, &mut Context), + range_chip: &RangeChip, + rlc_chip: &RlcChip, + inputs: &[(Self::InputWitness, Self::OutputWitness)], + ) -> Vec> { + let input_multiplier = rlc_chip.rlc_pow_fixed( + gate_ctx, + &range_chip.gate, + Self::OutputWitness::get_num_fields(), + ); + + inputs + .iter() + .map(|(input, output)| { + let i_rlc = flatten_witness_to_rlc(rlc_ctx, rlc_chip, &input.clone().into()); + let o_rlc = flatten_witness_to_rlc(rlc_ctx, rlc_chip, &output.clone().into()); + range_chip.gate.mul_add(gate_ctx, i_rlc, input_multiplier, o_rlc) + }) + .collect_vec() + } +} + +// ============= Data types passed between components ============= +pub type TypelessLogicalInput = Vec; +#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct TypelessPromiseCall { + pub capacity: usize, + pub logical_input: TypelessLogicalInput, +} + +/// (Receiver ComponentType, serialized logical input) +pub type GroupedPromiseCalls = HashMap>; +/// (typeless logical input, output) +pub type ComponentPromiseResult = (TypelessLogicalInput, Vec); + +/// Metadata for a promise shard +#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)] +pub struct PromiseShardMetadata { + pub commit: F, + pub capacity: usize, +} +/// (shard index, shard data) +pub type SelectedDataShard = (usize, S); +/// (shard index, vec of ComponentPromiseResult) +pub type SelectedPromiseResultShard = SelectedDataShard>>; + +#[derive(Debug, Clone, Getters, Serialize, Deserialize, Hash, PartialEq, Eq)] +pub struct SelectedDataShardsInMerkle { + // metadata of leaves of this Merkle tree + #[getset(get = "pub")] + leaves: Vec>, + /// Selected shards. + #[getset(get = "pub")] + shards: Vec>, +} + +impl SelectedDataShardsInMerkle { + // create SelectedDataShardsInMerkle + pub fn new(leaves: Vec>, shards: Vec>) -> Self { + assert!(leaves.len().is_power_of_two()); + // TODO: check capacity of each shard. + Self { leaves, shards } + } + /// Map data into another type. + pub fn map_data(self, f: impl Fn(S) -> NS) -> SelectedDataShardsInMerkle { + SelectedDataShardsInMerkle::new( + self.leaves, + self.shards.into_iter().map(|(i, s)| (i, f(s))).collect(), + ) + } +} + +/// Each shard is a virtual table, so shards is a vector of virtual tables. +pub type ComponentPromiseResultsInMerkle = + SelectedDataShardsInMerkle>>; + +impl ComponentPromiseResultsInMerkle { + /// Helper function to create ComponentPromiseResults from a single shard. + pub fn from_single_shard>(lr: Vec>) -> Self { + let vt = lr.iter().flat_map(T::logical_result_to_virtual_rows).collect_vec(); + let mut mock_builder = BaseCircuitBuilder::::new(true).use_k(18).use_lookup_bits(8); + let ctx = mock_builder.main(0); + let witness_vt = vt + .into_iter() + .map(|(v_i, v_o)| (v_i.into().assign(ctx), v_o.into().assign(ctx))) + .collect_vec(); + let witness_commit = T::Commiter::compute_commitment(&mut mock_builder, &witness_vt); + let commit = *witness_commit.value(); + mock_builder.clear(); // prevent drop warning + Self { + leaves: vec![PromiseShardMetadata:: { commit, capacity: witness_vt.len() }], + shards: vec![(0, lr.into_iter().map(|lr| lr.into()).collect())], + } + } +} +pub type GroupedPromiseResults = HashMap>; + +assert_impl_all!(ComponentPromiseResultsInMerkle: Serialize, DeserializeOwned); + +pub const NUM_COMPONENT_OWNED_INSTANCES: usize = 2; + +pub trait ComponentCircuit { + fn clear_witnesses(&self); + /// Compute promise calls. + fn compute_promise_calls(&self) -> anyhow::Result; + /// Feed inputs into the core builder. The `input` type should be the `CoreInput` type specified by the `CoreBuilder`. + /// It is the caller's responsibility to ensure that the capacity of the input + /// is equal to the configured capacity of the component circuit. This function + /// does **not** check this. + fn feed_input(&self, input: Box) -> anyhow::Result<()>; + /// Fulfill promise results. + fn fulfill_promise_results( + &self, + promise_results: &GroupedPromiseResults, + ) -> anyhow::Result<()>; + /// When inputs and promise results are ready, we can generate outputs of this component. + /// * When you call `compute_outputs`, `feed_inputs` must have already be called. + /// * Input capacity checking should happen when calling `feed_inputs`, not in this function. This function assumes that the input capacity is equal to the configured capacity of the component circuit. + // We don't have padding in the framework level because we don't have a formal interface to get a dummy input with capacity = 1. But even if we want to pad, it should happen when `feed_input`. + /// * The only goal of `compute_outputs` is to return the virtual table and its commit. + fn compute_outputs(&self) -> anyhow::Result>; + // Get public instances of this component. + fn get_public_instances(&self) -> ComponentPublicInstances; +} diff --git a/axiom-eth/src/utils/component/param.rs b/axiom-eth/src/utils/component/param.rs new file mode 100644 index 00000000..5512cd8f --- /dev/null +++ b/axiom-eth/src/utils/component/param.rs @@ -0,0 +1,4 @@ +// re-export constants, these also match constants from snark-verifier Poseidon +pub use zkevm_hashes::keccak::component::param::{ + POSEIDON_RATE, POSEIDON_R_F, POSEIDON_R_P, POSEIDON_SECURE_MDS, POSEIDON_T, +}; diff --git a/axiom-eth/src/utils/component/promise_collector.rs b/axiom-eth/src/utils/component/promise_collector.rs new file mode 100644 index 00000000..6d64401d --- /dev/null +++ b/axiom-eth/src/utils/component/promise_collector.rs @@ -0,0 +1,229 @@ +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + sync::{Arc, Mutex}, +}; + +use crate::Field; +use anyhow::anyhow; +use getset::{CopyGetters, Setters}; +use halo2_base::{AssignedValue, Context, ContextTag}; +use itertools::Itertools; + +use super::{ + types::Flatten, ComponentPromiseResultsInMerkle, ComponentType, ComponentTypeId, + GroupedPromiseCalls, GroupedPromiseResults, PromiseCallWitness, TypelessLogicalInput, + TypelessPromiseCall, +}; + +// (flatten input value, flatten input witness, flatten output witness) +// is for dedup. +pub type PromiseResultWitness = (Box>, Flatten>); + +pub type SharedPromiseCollector = Arc>>; +/// Newtype for [SharedPromiseCollector] with promise `call` functionality. +#[derive(Clone, Debug)] +pub struct PromiseCaller(pub SharedPromiseCollector); +impl PromiseCaller { + /// Create a PromiseCallerWrap + pub fn new(shared_promise_collector: SharedPromiseCollector) -> Self { + Self(shared_promise_collector) + } + /// Make a promise call. + pub fn call, B: ComponentType>( + &self, + ctx: &mut Context, + input_witness: P, + ) -> anyhow::Result { + assert_eq!(input_witness.get_component_type_id(), B::get_type_id()); + let witness_output_flatten = + self.0.lock().unwrap().call_impl(ctx, Box::new(input_witness))?; + B::OutputWitness::try_from(witness_output_flatten) + } +} + +#[derive(CopyGetters, Setters, Debug)] +pub struct PromiseCollector { + dependencies_lookup: HashSet, + dependencies: Vec, + // TypeId -> (ContextTag -> Vec) + // ContextTag is to support multi-threaded calls while maintaining deterministic in-circuit assignment order. + witness_grouped_calls: + HashMap>>>, + // Promise results for each component type. + value_results: HashMap>, + // map version of `value_results` for quick lookup promise results. + value_results_lookup: HashMap>>, + witness_commits: HashMap>, + #[getset(get_copy = "pub", set = "pub")] + promise_results_ready: bool, +} + +/// This is to limit PromiseCollector's interfaces exposed to implementaion. +pub trait PromiseCallsGetter { + /// Get promise calls by component type id. This is used to add these promises into lookup columns. + /// TODO: This should return `Vec>` because the order is determined at that time. But it's tricky to + /// flatten the BTreeMap without cloning Box. + fn get_calls_by_component_type_id( + &self, + component_type_id: &ComponentTypeId, + ) -> Option<&BTreeMap>>>; +} + +/// This is to limit PromiseCollector's interfaces exposed to implementaion. +pub trait PromiseResultsGetter { + /// Get promise results by component type id. This is used to add these promises into lookup columns. + fn get_results_by_component_type_id( + &self, + component_type_id: &ComponentTypeId, + ) -> Option<&ComponentPromiseResultsInMerkle>; +} + +/// This is to limit PromiseCollector's interfaces exposed to implementaion. +pub trait PromiseCommitSetter { + /// Get promise results by component type id. This is used to add these promises into lookup columns. + fn set_commit_by_component_type_id( + &mut self, + component_type_id: ComponentTypeId, + commit: AssignedValue, + ); +} + +impl PromiseCollector { + pub fn new(dependencies: Vec) -> Self { + Self { + dependencies_lookup: dependencies.clone().into_iter().collect(), + dependencies, + witness_grouped_calls: Default::default(), + value_results: Default::default(), + value_results_lookup: Default::default(), + witness_commits: Default::default(), + promise_results_ready: false, + } + } + + pub fn clear_witnesses(&mut self) { + self.witness_grouped_calls.clear(); + self.witness_commits.clear(); + // self.result should not be cleared because it comes from external. + } + + /// Get promise commit by component type id. + pub fn get_commit_by_component_type_id( + &self, + component_type_id: &ComponentTypeId, + ) -> Option> { + self.witness_commits.get(component_type_id).copied() + } + + /// Return dedup promises. + /// For each component type, the calls are sorted and deduped so that the returned order is deterministic. + pub fn get_deduped_calls(&self) -> GroupedPromiseCalls { + self.witness_grouped_calls + .iter() + .map(|(type_id, calls)| { + ( + type_id.clone(), + calls + .iter() + .flat_map(|(_, calls_per_context)| { + calls_per_context.iter().map(|(p, _)| TypelessPromiseCall { + capacity: p.get_capacity(), + logical_input: p.to_typeless_logical_input(), + }) + }) + .sorted() // sorting to ensure the order is deterministic. + // Note: likely not enough promise calls to be worth using par_sort + .dedup() + .collect_vec(), + ) + }) + .collect() + } + + /// Fulfill promise calls with the coressponding results. + pub fn fulfill(&mut self, results: &GroupedPromiseResults) { + assert!(!self.promise_results_ready); + for dep in &self.dependencies { + if let Some(results_per_comp) = results.get(dep) { + let results_per_comp = results_per_comp.clone(); + // TODO: check if it has already been fulfilled. + self.value_results_lookup.insert( + dep.clone(), + results_per_comp + .shards + .clone() + .into_iter() + .flat_map(|(_, data)| data) + .collect(), + ); + self.value_results.insert(dep.clone(), results_per_comp); + } + } + } + + pub(crate) fn call_impl( + &mut self, + ctx: &mut Context, + witness_input: Box>, + ) -> anyhow::Result>> { + // Special case: virtual call + let is_virtual = witness_input.get_capacity() == 0; + + let component_type_id = witness_input.get_component_type_id(); + if !is_virtual && !self.dependencies_lookup.contains(&component_type_id) { + return Err(anyhow!("Unsupport component type id {:?}.", component_type_id)); + } + + let value_serialized_input = witness_input.to_typeless_logical_input(); + + let call_results = self.value_results_lookup.get(&component_type_id); + // Virtual component type promise calls always go to the else branch. + let witness_output = if !is_virtual && self.promise_results_ready { + // Hack: there is no direct way to get field size information. + let mut flatten_output = witness_input.get_mock_output(); + // If promise results are fullfilled, use the results directly. + // Crash if the promise result is not fullfilled. + flatten_output.fields = + call_results.unwrap().get(&value_serialized_input).unwrap().clone(); + flatten_output.assign(ctx) + } else { + witness_input.get_mock_output().assign(ctx) + }; + self.witness_grouped_calls + .entry(component_type_id) + .or_default() + .entry(ctx.tag()) + .or_default() + .push((witness_input, witness_output.clone())); + Ok(witness_output) + } +} + +impl PromiseCallsGetter for PromiseCollector { + fn get_calls_by_component_type_id( + &self, + component_type_id: &ComponentTypeId, + ) -> Option<&BTreeMap>>> { + self.witness_grouped_calls.get(component_type_id) + } +} + +impl PromiseResultsGetter for PromiseCollector { + fn get_results_by_component_type_id( + &self, + component_type_id: &ComponentTypeId, + ) -> Option<&ComponentPromiseResultsInMerkle> { + self.value_results.get(component_type_id) + } +} + +impl PromiseCommitSetter for PromiseCollector { + fn set_commit_by_component_type_id( + &mut self, + component_type_id: ComponentTypeId, + commit: AssignedValue, + ) { + log::debug!("component_type_id: {} commit: {:?}", component_type_id, commit.value()); + self.witness_commits.insert(component_type_id, commit); + } +} diff --git a/axiom-eth/src/utils/component/promise_loader/combo.rs b/axiom-eth/src/utils/component/promise_loader/combo.rs new file mode 100644 index 00000000..f5fa3b6a --- /dev/null +++ b/axiom-eth/src/utils/component/promise_loader/combo.rs @@ -0,0 +1,98 @@ +use std::marker::PhantomData; + +use halo2_base::halo2_proofs::{circuit::Layouter, plonk::ConstraintSystem}; + +use crate::{ + rlc::circuit::builder::RlcCircuitBuilder, + utils::component::{ + circuit::{ComponentBuilder, LoaderParamsPerComponentType, PromiseBuilder}, + promise_collector::{PromiseCallsGetter, PromiseCommitSetter, PromiseResultsGetter}, + ComponentTypeId, + }, + Field, +}; + +pub struct PromiseBuilderCombo, SECOND: PromiseBuilder> { + pub to_combine: (FIRST, SECOND), + _phantom: PhantomData<(F, FIRST, SECOND)>, +} + +impl, SECOND: PromiseBuilder> + PromiseBuilderCombo +{ +} + +impl, SECOND: PromiseBuilder> ComponentBuilder + for PromiseBuilderCombo +{ + type Config = (FIRST::Config, SECOND::Config); + type Params = (FIRST::Params, SECOND::Params); + + fn new(params: Self::Params) -> Self { + Self { to_combine: (FIRST::new(params.0), SECOND::new(params.1)), _phantom: PhantomData } + } + fn get_params(&self) -> Self::Params { + (self.to_combine.0.get_params(), self.to_combine.1.get_params()) + } + fn clear_witnesses(&mut self) { + self.to_combine.0.clear_witnesses(); + self.to_combine.1.clear_witnesses(); + } + + fn calculate_params(&mut self) -> Self::Params { + (self.to_combine.0.calculate_params(), self.to_combine.1.calculate_params()) + } + + fn configure_with_params(meta: &mut ConstraintSystem, params: Self::Params) -> Self::Config { + ( + FIRST::configure_with_params(meta, params.0), + SECOND::configure_with_params(meta, params.1), + ) + } +} + +impl, SECOND: PromiseBuilder> PromiseBuilder + for PromiseBuilderCombo +{ + fn get_component_type_dependencies() -> Vec { + [FIRST::get_component_type_dependencies(), SECOND::get_component_type_dependencies()] + .concat() + } + fn extract_loader_params_per_component_type( + params: &Self::Params, + ) -> Vec { + FIRST::extract_loader_params_per_component_type(¶ms.0) + .into_iter() + .chain(SECOND::extract_loader_params_per_component_type(¶ms.1)) + .collect() + } + + fn fulfill_promise_results(&mut self, promise_results_getter: &impl PromiseResultsGetter) { + self.to_combine.0.fulfill_promise_results(promise_results_getter); + self.to_combine.1.fulfill_promise_results(promise_results_getter); + } + fn virtual_assign_phase0( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_commit_setter: &mut impl PromiseCommitSetter, + ) { + self.to_combine.0.virtual_assign_phase0(builder, promise_commit_setter); + self.to_combine.1.virtual_assign_phase0(builder, promise_commit_setter); + } + fn raw_synthesize_phase0(&mut self, config: &Self::Config, layouter: &mut impl Layouter) { + self.to_combine.0.raw_synthesize_phase0(&config.0, layouter); + self.to_combine.1.raw_synthesize_phase0(&config.1, layouter); + } + fn virtual_assign_phase1( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_calls_getter: &mut impl PromiseCallsGetter, + ) { + self.to_combine.0.virtual_assign_phase1(builder, promise_calls_getter); + self.to_combine.1.virtual_assign_phase1(builder, promise_calls_getter); + } + fn raw_synthesize_phase1(&mut self, config: &Self::Config, layouter: &mut impl Layouter) { + self.to_combine.0.raw_synthesize_phase1(&config.0, layouter); + self.to_combine.1.raw_synthesize_phase1(&config.1, layouter); + } +} diff --git a/axiom-eth/src/utils/component/promise_loader/comp_loader.rs b/axiom-eth/src/utils/component/promise_loader/comp_loader.rs new file mode 100644 index 00000000..6d77f458 --- /dev/null +++ b/axiom-eth/src/utils/component/promise_loader/comp_loader.rs @@ -0,0 +1,372 @@ +#![allow(clippy::type_complexity)] +use std::{iter, marker::PhantomData}; + +use crate::utils::component::utils::compute_poseidon; +use crate::Field; +use crate::{ + rlc::{chip::RlcChip, circuit::builder::RlcCircuitBuilder, RLC_PHASE}, + utils::component::{ + types::FixLenLogical, utils::compute_poseidon_merkle_tree, FlattenVirtualRow, + FlattenVirtualTable, LogicalResult, PromiseShardMetadata, SelectedDataShardsInMerkle, + }, +}; +use getset::{CopyGetters, Getters}; +use halo2_base::{ + gates::{circuit::builder::BaseCircuitBuilder, GateInstructions, RangeChip, RangeInstructions}, + AssignedValue, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use super::{ + super::{ + promise_collector::PromiseResultWitness, + types::Flatten, + utils::{compute_commitment_with_flatten, create_hasher}, + ComponentPromiseResultsInMerkle, ComponentType, ComponentTypeId, + }, + flatten_witness_to_rlc, +}; + +/// To seal SingleComponentLoader so no external implementation is allowed. +mod private { + pub trait Sealed {} +} + +#[derive(Clone, Debug, Hash, Getters, CopyGetters, Serialize, Deserialize, Eq, PartialEq)] +/// Specify what merkle tree of commits can be loaded. +pub struct SingleComponentLoaderParams { + /// The maximum height of the merkle tree this loader can load. + #[getset(get_copy = "pub")] + max_height: usize, + /// Specify the number of shards to be loaded and the capacity of each shard. + #[getset(get = "pub")] + shard_caps: Vec, +} + +impl SingleComponentLoaderParams { + /// Create SingleComponentLoaderParams + pub fn new(max_height: usize, shard_caps: Vec) -> Self { + // Tip: binary tree with only 1 node has height 0. + assert!(shard_caps.len() <= 1 << max_height); + Self { max_height, shard_caps } + } + /// Create SingleComponentLoaderParams for only 1 shard + pub fn new_for_one_shard(cap: usize) -> Self { + Self { max_height: 0, shard_caps: vec![cap] } + } +} + +impl Default for SingleComponentLoaderParams { + fn default() -> Self { + Self::new(0, vec![0]) + } +} + +/// Object safe trait for loading promises of a component type. +pub trait SingleComponentLoader: private::Sealed { + /// Get the component type id this loader is for. + fn get_component_type_id(&self) -> ComponentTypeId; + /// Get ComponentTypeName for logging/debugging. + fn get_component_type_name(&self) -> &'static str; + fn get_params(&self) -> &SingleComponentLoaderParams; + /// Check if promise results are ready. + fn promise_results_ready(&self) -> bool; + /// Load promise results from promise results getter. + fn load_promise_results(&mut self, promise_results: ComponentPromiseResultsInMerkle); + /// Load dummy promise results according to the loader params. + fn load_dummy_promise_results(&mut self); + /// Return (merkle_tree_root, concat_assigned_virtual_tables). Data is preloaded. + /// TODO: do we really need to return assigned virtual table? + fn assign_and_compute_commitment( + &self, + builder: &mut RlcCircuitBuilder, + ) -> (AssignedValue, FlattenVirtualTable>); + /// Return (to_lookup, lookup_table) + fn generate_lookup_rlc( + &self, + builder: &mut RlcCircuitBuilder, + promise_calls: &[&PromiseResultWitness], + promise_results: &[FlattenVirtualRow>], + ) -> (Vec>, Vec>); +} + +/// Promise results but each shard is in the virtual table format. +type PromiseVirtualTableResults = SelectedDataShardsInMerkle>; + +/// Implementation of SingleComponentLoader for a component type. +pub struct SingleComponentLoaderImpl> { + val_promise_results: Option>, + params: SingleComponentLoaderParams, + _phantom: PhantomData, +} + +impl> SingleComponentLoaderImpl { + /// Create SingleComponentLoaderImpl for T. + pub fn new(params: SingleComponentLoaderParams) -> Self { + Self { val_promise_results: None, params, _phantom: PhantomData } + } + /// Create dummy promise results based on params for CircuitBuilder params calculation. + fn create_dummy_promise_result_merkle(&self) -> PromiseVirtualTableResults { + let num_shards = self.params.shard_caps.len(); + let num_leaves = num_shards.next_power_of_two(); + let mut leaves = Vec::with_capacity(num_leaves); + for i in 0..num_leaves { + let commit = F::ZERO; + leaves.push(PromiseShardMetadata:: { + commit, + capacity: if i < num_shards { self.params.shard_caps[i] } else { 0 }, + }); + } + let shards = self + .params + .shard_caps + .iter() + .copied() + .enumerate() + .map(|(idx, shard_cap)| { + let dummy_input = Flatten:: { + fields: vec![F::ZERO; T::InputValue::get_num_fields()], + field_size: T::InputValue::get_field_size(), + }; + let dummy_output = Flatten:: { + fields: vec![F::ZERO; T::OutputValue::get_num_fields()], + field_size: T::OutputValue::get_field_size(), + }; + let shard = vec![(dummy_input, dummy_output); shard_cap]; + (idx, shard) + }) + .collect_vec(); + PromiseVirtualTableResults::::new(leaves, shards) + } +} + +impl> private::Sealed for SingleComponentLoaderImpl {} + +impl> SingleComponentLoader for SingleComponentLoaderImpl { + fn get_component_type_id(&self) -> ComponentTypeId { + T::get_type_id() + } + fn get_component_type_name(&self) -> &'static str { + T::get_type_name() + } + fn get_params(&self) -> &SingleComponentLoaderParams { + &self.params + } + fn promise_results_ready(&self) -> bool { + self.val_promise_results.is_some() + } + fn load_promise_results(&mut self, promise_results: ComponentPromiseResultsInMerkle) { + // Tip: binary tree with only 1 node has height 0. + assert!(promise_results.leaves().len() <= 1 << self.params.max_height); + assert_eq!(promise_results.shards().len(), self.params.shard_caps().len()); + + let merkle_vt = promise_results.map_data(|typeless_prs| { + typeless_prs + .into_iter() + .flat_map(|typeless_prs| { + FlattenVirtualTable::::from( + LogicalResult::::try_from(typeless_prs).unwrap(), + ) + }) + .collect_vec() + }); + + for (shard_idx, shard) in merkle_vt.shards() { + let shard_capacity = merkle_vt.leaves()[*shard_idx].capacity; + assert_eq!(shard_capacity, shard.len()); + } + self.val_promise_results = Some(merkle_vt); + } + + fn assign_and_compute_commitment( + &self, + builder: &mut RlcCircuitBuilder, + ) -> (AssignedValue, FlattenVirtualTable>) { + let val_promise_results = + if let Some(val_promise_results) = self.val_promise_results.as_ref() { + val_promise_results.clone() + } else { + self.create_dummy_promise_result_merkle() + }; + let leaves_to_load = val_promise_results.leaves(); + + let assigned_per_shard = val_promise_results + .shards() + .iter() + .map(|(_, vt)| { + let ctx = builder.base.main(0); + let witness_vt = + vt.iter().map(|(v_i, v_o)| (v_i.assign(ctx), v_o.assign(ctx))).collect_vec(); + let commit = T::Commiter::compute_commitment(&mut builder.base, &witness_vt); + (commit, witness_vt) + }) + .collect_vec(); + + let range_chip = &builder.range_chip(); + let gate_chip = &range_chip.gate; + let ctx = builder.base.main(0); + // Indexes of selected shards. The length is deterministic because we had + // checked selected_shard.len() == params.shard_caps.len() in load_promise_results. + let selected_shards = ctx.assign_witnesses( + val_promise_results.shards().iter().map(|(shard_idx, _)| F::from(*shard_idx as u64)), + ); + + // The circuit has a fixed `self.params.max_height`. This is the maximum height merkle tree supported. + // However the private inputs of the circuit will dictate the actual heigh of the merkle tree of shards that this circuit is using. + // Example: + // We have max height is 3. + // However, this circuit will only get `leaves_to_load` for 4 shard commitments: [a, b, c, d] + // It will compute the merkle root of [a, b, c, d] where `4` is a private witness. + // Then it may have `selected_shards = [0, 2]`, meaning it only de-commits the shards for a, c. It does this by using `select_from_idx` on `[a, b, c, d]` to get `a, c`. + // Because we always decommit the leaves, meaning we dictate that the leaves much be flat hashes of virtual tables of fixed size (given by `shard_caps`), the + // private witness for the true height (in this example `4`), is commited to by the merkle root we generate. + // In other words, our definition of shard commitment provides domain separation for the merkle leaves. + + // The loader's behavior should not depend on inputs. So the loader always computes a merkle tree with a pre-defined height. + // Then we put the merkle tree to load in the left-bottom of the pre-defined merkle tree. The rest of the leaves are filled with zeros. + // The root of the merkle tree to load will be on the leftmost path of the pre-defined merkle tree. So we can select the root by + // the height of the merkle tree to load. + + let num_leaves = 1 << self.params.max_height; + let leaves_commits = ctx.assign_witnesses( + leaves_to_load.iter().map(|l| l.commit).chain(iter::repeat(F::ZERO)).take(num_leaves), + ); + let mut assigned_vts = Vec::with_capacity(assigned_per_shard.len()); + for (selected_shard, (shard_commit, assigned_vt)) in + selected_shards.into_iter().zip_eq(assigned_per_shard) + { + range_chip.check_less_than_safe(ctx, selected_shard, num_leaves as u64); + let leaf_commit = + gate_chip.select_from_idx(ctx, leaves_commits.clone(), selected_shard); + ctx.constrain_equal(&leaf_commit, &shard_commit); + + assigned_vts.push(assigned_vt); + } + let flatten_assigned_vts = assigned_vts.into_iter().flatten().collect_vec(); + + // Optimization: if there is only one shard, we don't need to compute the merkle tree so no need to create hasher. + if leaves_commits.len() == 1 { + return (leaves_commits[0], flatten_assigned_vts); + }; + + let mut hasher = create_hasher::(); + hasher.initialize_consts(ctx, gate_chip); + let nodes = compute_poseidon_merkle_tree(ctx, gate_chip, &hasher, leaves_commits); + + // Leftmost nodes of the pre-defined merkle tree from bottom to top. + let leftmost_nodes = + (0..=self.params.max_height).rev().map(|i| nodes[(1 << i) - 1]).collect_vec(); + // The height of the merkle tree to load. + let result_height: AssignedValue = + ctx.load_witness(F::from(leaves_to_load.len().ilog2() as u64)); + range_chip.check_less_than_safe(ctx, result_height, (self.params.max_height + 1) as u64); + + let output_commit = gate_chip.select_from_idx(ctx, leftmost_nodes, result_height); + + (output_commit, flatten_assigned_vts) + } + + fn generate_lookup_rlc( + &self, + builder: &mut RlcCircuitBuilder, + promise_calls: &[&PromiseResultWitness], + promise_results: &[FlattenVirtualRow>], + ) -> (Vec>, Vec>) { + let range_chip = &builder.range_chip(); + let rlc_chip = builder.rlc_chip(&range_chip.gate); + generate_lookup_rlcs_impl::( + builder, + range_chip, + &rlc_chip, + promise_calls, + promise_results, + ) + } + + fn load_dummy_promise_results(&mut self) { + let vt = self.create_dummy_promise_result_merkle(); + self.val_promise_results = Some(vt); + } +} + +/// Returns `(to_lookup_rlc, lookup_table_rlc)` +/// where `to_lookup_rlc` corresponds to `promise_calls` and +/// `lookup_table_rlc` corresponds to `promise_results`. +/// +/// This should only be called in phase1. +pub fn generate_lookup_rlcs_impl>( + builder: &mut RlcCircuitBuilder, + range_chip: &RangeChip, + rlc_chip: &RlcChip, + promise_calls: &[&PromiseResultWitness], + promise_results: &[(Flatten>, Flatten>)], +) -> (Vec>, Vec>) { + let gate_ctx = builder.base.main(RLC_PHASE); + + let input_multiplier = + rlc_chip.rlc_pow_fixed(gate_ctx, range_chip.gate(), T::OutputValue::get_num_fields()); + + let to_lookup_rlc = + builder.parallelize_phase1(promise_calls.to_vec(), |(gate_ctx, rlc_ctx), (f_i, f_o)| { + let i_rlc = f_i.to_rlc((gate_ctx, rlc_ctx), range_chip, rlc_chip); + let o_rlc = flatten_witness_to_rlc(rlc_ctx, rlc_chip, f_o); + range_chip.gate.mul_add(gate_ctx, i_rlc, input_multiplier, o_rlc) + }); + + let (gate_ctx, rlc_ctx) = builder.rlc_ctx_pair(); + + let lookup_table_rlc = T::rlc_virtual_rows( + (gate_ctx, rlc_ctx), + range_chip, + rlc_chip, + &promise_results + .iter() + .map(|(f_i, f_o)| { + ( + T::InputWitness::try_from(f_i.clone()).unwrap(), + T::OutputWitness::try_from(f_o.clone()).unwrap(), + ) + }) + .collect_vec(), + ); + (to_lookup_rlc, lookup_table_rlc) +} + +/// Trait for computing commit of ONE virtual table. +pub trait ComponentCommiter { + /// Compute the commitment of a virtual table. + fn compute_commitment( + builder: &mut BaseCircuitBuilder, + witness_promise_results: &[(Flatten>, Flatten>)], + ) -> AssignedValue; + /// The implementor **must** enforce that the output of this function + /// is the same as the output value of `compute_commitment`. + /// We allow a separate implemenation purely for performance, as the native commitmnt + /// computation is much faster than doing it in the circuit. + fn compute_native_commitment(witness_promise_results: &[(Flatten, Flatten)]) -> F; +} + +/// BasicComponentCommiter simply compute poseidon of all virtual rows. +pub struct BasicComponentCommiter(PhantomData); + +impl ComponentCommiter for BasicComponentCommiter { + fn compute_commitment( + builder: &mut BaseCircuitBuilder, + witness_promise_results: &[(Flatten>, Flatten>)], + ) -> AssignedValue { + let range_chip = &builder.range_chip(); + let ctx = builder.main(0); + + let mut hasher = create_hasher::(); + hasher.initialize_consts(ctx, &range_chip.gate); + compute_commitment_with_flatten(ctx, &range_chip.gate, &hasher, witness_promise_results) + } + fn compute_native_commitment(witness_promise_results: &[(Flatten, Flatten)]) -> F { + let to_commit = witness_promise_results + .iter() + .flat_map(|(i, o)| i.fields.iter().chain(o.fields.iter()).copied()) + .collect_vec(); + compute_poseidon(&to_commit) + } +} diff --git a/axiom-eth/src/utils/component/promise_loader/empty.rs b/axiom-eth/src/utils/component/promise_loader/empty.rs new file mode 100644 index 00000000..8d0e8741 --- /dev/null +++ b/axiom-eth/src/utils/component/promise_loader/empty.rs @@ -0,0 +1,71 @@ +use std::marker::PhantomData; + +use crate::rlc::circuit::builder::RlcCircuitBuilder; +use crate::utils::component::circuit::LoaderParamsPerComponentType; +use halo2_base::halo2_proofs::{circuit::Layouter, plonk::ConstraintSystem}; + +use crate::Field; + +use crate::utils::component::{ + circuit::{ComponentBuilder, PromiseBuilder}, + promise_collector::{PromiseCallsGetter, PromiseCommitSetter, PromiseResultsGetter}, + ComponentTypeId, +}; + +#[derive(Default)] +/// Empty promise loader. +pub struct EmptyPromiseLoader(PhantomData); + +impl ComponentBuilder for EmptyPromiseLoader { + type Config = (); + type Params = (); + + fn new(_params: Self::Params) -> Self { + Self(PhantomData) + } + + fn get_params(&self) -> Self::Params {} + + fn clear_witnesses(&mut self) {} + + fn configure_with_params( + _meta: &mut ConstraintSystem, + _params: Self::Params, + ) -> Self::Config { + } + fn calculate_params(&mut self) -> Self::Params {} +} + +impl PromiseBuilder for EmptyPromiseLoader { + fn get_component_type_dependencies() -> Vec { + vec![] + } + fn extract_loader_params_per_component_type( + _params: &Self::Params, + ) -> Vec { + vec![] + } + fn fulfill_promise_results(&mut self, _promise_results_getter: &impl PromiseResultsGetter) { + // Do nothing. + } + fn virtual_assign_phase0( + &mut self, + _builder: &mut RlcCircuitBuilder, + _promise_commit_setter: &mut impl PromiseCommitSetter, + ) { + // Do nothing. + } + fn raw_synthesize_phase0(&mut self, _config: &Self::Config, _layouter: &mut impl Layouter) { + // Do nothing. + } + fn virtual_assign_phase1( + &mut self, + _builder: &mut RlcCircuitBuilder, + _promise_calls_getter: &mut impl PromiseCallsGetter, + ) { + // Do nothing. + } + fn raw_synthesize_phase1(&mut self, _config: &Self::Config, _layouter: &mut impl Layouter) { + // Do nothing. + } +} diff --git a/axiom-eth/src/utils/component/promise_loader/mod.rs b/axiom-eth/src/utils/component/promise_loader/mod.rs new file mode 100644 index 00000000..9f59dc2a --- /dev/null +++ b/axiom-eth/src/utils/component/promise_loader/mod.rs @@ -0,0 +1,23 @@ +use crate::{rlc::chip::RlcChip, Field}; +use halo2_base::{AssignedValue, Context}; + +use super::types::Flatten; + +pub mod combo; +pub mod comp_loader; +pub mod empty; +pub mod multi; +pub mod single; +#[cfg(test)] +pub mod tests; +/// Utilities to help with creating dummy circuits for proving and verifying key generation. +pub mod utils; + +/// A helper function to compute RLC of (flatten input, flattne output). +pub fn flatten_witness_to_rlc( + rlc_ctx: &mut Context, + rlc_chip: &RlcChip, + f: &Flatten>, +) -> AssignedValue { + rlc_chip.compute_rlc_fixed_len(rlc_ctx, f.fields.clone()).rlc_val +} diff --git a/axiom-eth/src/utils/component/promise_loader/multi.rs b/axiom-eth/src/utils/component/promise_loader/multi.rs new file mode 100644 index 00000000..b97c5003 --- /dev/null +++ b/axiom-eth/src/utils/component/promise_loader/multi.rs @@ -0,0 +1,337 @@ +#![allow(clippy::type_complexity)] +use std::{collections::HashMap, marker::PhantomData}; + +use crate::{ + rlc::{ + chip::RlcChip, + circuit::builder::{RlcCircuitBuilder, RlcContextPair}, + }, + utils::component::{ + circuit::LoaderParamsPerComponentType, + promise_loader::comp_loader::SingleComponentLoaderImpl, + }, + Field, +}; +use getset::{CopyGetters, Setters}; +use halo2_base::{ + gates::GateInstructions, + halo2_proofs::{ + circuit::Layouter, + plonk::{ConstraintSystem, SecondPhase}, + }, + virtual_region::{ + copy_constraints::SharedCopyConstraintManager, lookups::basic::BasicDynLookupConfig, + }, + AssignedValue, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::utils::component::{ + circuit::{ComponentBuilder, PromiseBuilder}, + promise_collector::{PromiseCallsGetter, PromiseCommitSetter, PromiseResultsGetter}, + promise_loader::flatten_witness_to_rlc, + types::{FixLenLogical, Flatten, LogicalEmpty}, + ComponentType, ComponentTypeId, +}; + +use super::comp_loader::{SingleComponentLoader, SingleComponentLoaderParams}; + +pub trait ComponentTypeList { + fn get_component_type_ids() -> Vec; + fn build_component_loaders( + params_per_component: &HashMap, + ) -> Vec>>; +} +pub struct ComponentTypeListEnd { + _phantom: PhantomData, +} +impl ComponentTypeList for ComponentTypeListEnd { + fn get_component_type_ids() -> Vec { + vec![] + } + fn build_component_loaders( + _params_per_component: &HashMap, + ) -> Vec>> { + vec![] + } +} +pub struct ComponentTypeListImpl, LATER: ComponentTypeList> { + _phantom: PhantomData<(F, HEAD, LATER)>, +} +impl, LATER: ComponentTypeList> ComponentTypeList + for ComponentTypeListImpl +{ + fn get_component_type_ids() -> Vec { + let mut ret = vec![HEAD::get_type_id()]; + ret.extend(LATER::get_component_type_ids()); + ret + } + fn build_component_loaders( + params_per_component: &HashMap, + ) -> Vec>> { + type Loader = SingleComponentLoaderImpl; + let mut ret = Vec::new(); + if let Some(params) = params_per_component.get(&HEAD::get_type_id()) { + let comp_loader: Box> = + Box::new(Loader::::new(params.clone())); + ret.push(comp_loader); + } + ret.extend(LATER::build_component_loaders(params_per_component)); + ret + } +} +#[macro_export] +macro_rules! component_type_list { + ($field:ty, $comp_type:ty) => { + $crate::utils::component::promise_loader::multi::ComponentTypeListImpl<$field, $comp_type, $crate::utils::component::promise_loader::multi::ComponentTypeListEnd<$field>> + }; + ($field:ty, $comp_type:ty, $($comp_types:ty),+) => { + $crate::utils::component::promise_loader::multi::ComponentTypeListImpl<$field, $comp_type, $crate::component_type_list!($field, $($comp_types),+)> + } +} + +#[derive(Clone)] +pub struct MultiPromiseLoaderConfig { + pub dyn_lookup_config: BasicDynLookupConfig<1>, +} + +// TODO: this is useless now because comp_loaders already have the information. +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct MultiPromiseLoaderParams { + pub params_per_component: HashMap, +} + +/// Load promises of multiple component types which share the same lookup table. +/// The size of promise result it receives MUST match its capacity. +/// VT is a virtual component type which is used to generate lookup table. Its promise +/// results should not be fulfilled by external. +/// TODO: Currently we don't support promise calls for virtual component types so we enforce output to be empty. +/// TODO: remove virtual component type. +#[derive(CopyGetters, Setters)] +pub struct MultiPromiseLoader< + F: Field, + VT: ComponentType, OutputWitness = LogicalEmpty>>, + CLIST: ComponentTypeList, + A: RlcAdapter, +> { + params: MultiPromiseLoaderParams, + // ComponentTypeId -> (input, output) + witness_promise_results: Option< + HashMap>, Flatten>)>>, + >, + // (to lookup, lookup table) + witness_rlc_lookup: Option<(Vec>, Vec>)>, + // A bit hacky.. + witness_gen_only: bool, + copy_manager: Option>, + pub(super) comp_loaders: Vec>>, + _phantom: PhantomData<(VT, CLIST, A)>, +} + +pub trait RlcAdapter { + fn to_rlc( + ctx_pair: RlcContextPair, + gate: &impl GateInstructions, + rlc: &RlcChip, + type_id: &ComponentTypeId, + io_pairs: &[(Flatten>, Flatten>)], + ) -> Vec>; +} + +impl< + F: Field, + VT: ComponentType< + F, + OutputValue = LogicalEmpty, + OutputWitness = LogicalEmpty>, + >, + CLIST: ComponentTypeList, + A: RlcAdapter, + > ComponentBuilder for MultiPromiseLoader +{ + type Config = MultiPromiseLoaderConfig; + type Params = MultiPromiseLoaderParams; + + /// Create MultiPromiseLoader + fn new(params: MultiPromiseLoaderParams) -> Self { + let comp_loaders = CLIST::build_component_loaders(¶ms.params_per_component); + Self { + params, + witness_promise_results: None, + witness_rlc_lookup: None, + witness_gen_only: false, + copy_manager: None, + comp_loaders, + _phantom: PhantomData, + } + } + fn get_params(&self) -> Self::Params { + self.params.clone() + } + + fn clear_witnesses(&mut self) { + self.witness_promise_results = None; + self.witness_rlc_lookup = None; + self.copy_manager = None; + } + + fn configure_with_params( + meta: &mut ConstraintSystem, + _params: Self::Params, + ) -> Self::Config { + // TODO: adjust num of columns based on params. + let dyn_lookup_config = BasicDynLookupConfig::new(meta, || SecondPhase, 1); + Self::Config { dyn_lookup_config } + } + fn calculate_params(&mut self) -> Self::Params { + self.params.clone() + } +} + +impl< + F: Field, + VT: ComponentType< + F, + OutputValue = LogicalEmpty, + OutputWitness = LogicalEmpty>, + >, + CLIST: ComponentTypeList, + A: RlcAdapter, + > PromiseBuilder for MultiPromiseLoader +{ + // NOTE: the actual dependencies are based on the params. + fn get_component_type_dependencies() -> Vec { + CLIST::get_component_type_ids() + } + fn extract_loader_params_per_component_type( + params: &Self::Params, + ) -> Vec { + let mut ret = Vec::new(); + for type_id in Self::get_component_type_dependencies() { + if let Some(loader_params) = params.params_per_component.get(&type_id) { + ret.push(LoaderParamsPerComponentType { + component_type_id: type_id, + loader_params: loader_params.clone(), + }) + } + } + ret + } + fn fulfill_promise_results(&mut self, promise_results_getter: &impl PromiseResultsGetter) { + assert!( + promise_results_getter.get_results_by_component_type_id(&VT::get_type_id()).is_none(), + "promise results of the virtual component type should not be fulfilled" + ); + for comp_loader in &mut self.comp_loaders { + let component_type_id = comp_loader.get_component_type_id(); + let promise_results = promise_results_getter + .get_results_by_component_type_id(&component_type_id) + .unwrap_or_else(|| { + panic!("missing promise results for component type id {:?}", component_type_id) + }); + + comp_loader.load_promise_results(promise_results.clone()); + } + } + + fn virtual_assign_phase0( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_commit_setter: &mut impl PromiseCommitSetter, + ) { + assert!(self.witness_promise_results.is_none()); + self.witness_gen_only = builder.witness_gen_only(); + + let mut witness_promise_results = HashMap::new(); + + for comp_loader in &self.comp_loaders { + // TODO: Multi-thread here? + let (commit, witness_promise_results_per_type) = + comp_loader.assign_and_compute_commitment(builder); + let component_type_id = comp_loader.get_component_type_id(); + promise_commit_setter + .set_commit_by_component_type_id(component_type_id.clone(), commit); + witness_promise_results.insert(component_type_id, witness_promise_results_per_type); + } + + self.witness_promise_results = Some(witness_promise_results); + } + + fn raw_synthesize_phase0(&mut self, _config: &Self::Config, _layouter: &mut impl Layouter) { + // Do nothing. + } + + fn virtual_assign_phase1( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_calls_getter: &mut impl PromiseCallsGetter, + ) { + assert!(self.witness_promise_results.is_some()); + let range_chip = &builder.range_chip(); + let rlc_chip = builder.rlc_chip(&range_chip.gate); + let (gate_ctx, rlc_ctx) = builder.rlc_ctx_pair(); + + let input_multiplier = + rlc_chip.rlc_pow_fixed(gate_ctx, &range_chip.gate, VT::OutputValue::get_num_fields()); + + let component_type_id = VT::get_type_id(); + + let calls_per_context = + promise_calls_getter.get_calls_by_component_type_id(&component_type_id).unwrap(); + + let to_lookup_rlc = calls_per_context + .values() + .flatten() + .map(|(f_i, f_o)| { + let i_rlc = f_i.to_rlc((gate_ctx, rlc_ctx), range_chip, &rlc_chip); + let o_rlc = flatten_witness_to_rlc(rlc_ctx, &rlc_chip, f_o); + range_chip.gate.mul_add(gate_ctx, i_rlc, input_multiplier, o_rlc) + }) + .collect_vec(); + + let num_dependencies = self.comp_loaders.len(); + let mut lookup_table_rlc = Vec::with_capacity(num_dependencies); + + // **Order must be deterministic.** + for comp_loader in &self.comp_loaders { + let component_type_id = comp_loader.get_component_type_id(); + let ctx_pair = (&mut *gate_ctx, &mut *rlc_ctx); + let lookup_table_rlc_per_type = A::to_rlc( + ctx_pair, + &range_chip.gate, + &rlc_chip, + &component_type_id, + &self.witness_promise_results.as_ref().unwrap()[&component_type_id], + ); + lookup_table_rlc.push(lookup_table_rlc_per_type); + } + let lookup_table_rlc = lookup_table_rlc.concat(); + + self.witness_rlc_lookup = Some((to_lookup_rlc, lookup_table_rlc)); + self.copy_manager = Some(builder.copy_manager().clone()); + } + + fn raw_synthesize_phase1(&mut self, config: &Self::Config, layouter: &mut impl Layouter) { + assert!(self.witness_rlc_lookup.is_some()); + + let (to_lookup, lookup_table) = self.witness_rlc_lookup.as_ref().unwrap(); + let dyn_lookup_config = &config.dyn_lookup_config; + + let copy_manager = (!self.witness_gen_only).then(|| self.copy_manager.as_ref().unwrap()); + dyn_lookup_config.assign_virtual_table_to_raw( + layouter.namespace(|| { + format!("promise loader adds advice to lookup for {}", VT::get_type_name()) + }), + lookup_table.iter().map(|a| [*a; 1]), + copy_manager, + ); + + dyn_lookup_config.assign_virtual_to_lookup_to_raw( + layouter + .namespace(|| format!("promise loader loads lookup table {}", VT::get_type_name())), + to_lookup.iter().map(|a| [*a; 1]), + copy_manager, + ); + } +} diff --git a/axiom-eth/src/utils/component/promise_loader/single.rs b/axiom-eth/src/utils/component/promise_loader/single.rs new file mode 100644 index 00000000..ed6d47ae --- /dev/null +++ b/axiom-eth/src/utils/component/promise_loader/single.rs @@ -0,0 +1,205 @@ +#![allow(clippy::type_complexity)] +use std::marker::PhantomData; + +use getset::{CopyGetters, Setters}; +use halo2_base::{ + halo2_proofs::{ + circuit::Layouter, + plonk::{ConstraintSystem, SecondPhase}, + }, + virtual_region::{ + copy_constraints::SharedCopyConstraintManager, lookups::basic::BasicDynLookupConfig, + }, + AssignedValue, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::{ + rlc::circuit::builder::RlcCircuitBuilder, + utils::component::{ + circuit::{ComponentBuilder, LoaderParamsPerComponentType, PromiseBuilder}, + promise_collector::{PromiseCallsGetter, PromiseCommitSetter, PromiseResultsGetter}, + types::Flatten, + ComponentType, ComponentTypeId, + }, + Field, +}; + +use super::comp_loader::{ + SingleComponentLoader, SingleComponentLoaderImpl, SingleComponentLoaderParams, +}; + +#[derive(Clone)] +pub struct PromiseLoaderConfig { + pub dyn_lookup_config: BasicDynLookupConfig<1>, +} + +#[derive(Default, Clone, Debug, Hash, Serialize, Deserialize)] +pub struct PromiseLoaderParams { + pub comp_loader_params: SingleComponentLoaderParams, +} + +impl PromiseLoaderParams { + pub fn new(comp_loader_params: SingleComponentLoaderParams) -> Self { + Self { comp_loader_params } + } + pub fn new_for_one_shard(capacity: usize) -> Self { + Self { comp_loader_params: SingleComponentLoaderParams::new(0, vec![capacity]) } + } +} + +/// PromiseLoader loads promises of a component type. It owns a lookup table dedicated for the component type. +/// The size of promise result it receives MUST match its capacity. +#[derive(Setters, CopyGetters)] +pub struct PromiseLoader> { + params: PromiseLoaderParams, + witness_promise_results: Option>, Flatten>)>>, + // (to lookup, lookup table) + witness_rlc_lookup: Option<(Vec>, Vec>)>, + // A bit hacky.. + witness_gen_only: bool, + copy_manager: Option>, + pub(super) comp_loader: Box>, + _phantom: PhantomData, +} + +impl> ComponentBuilder for PromiseLoader { + type Config = PromiseLoaderConfig; + type Params = PromiseLoaderParams; + + // Create PromiseLoader + fn new(params: PromiseLoaderParams) -> Self { + Self { + params: params.clone(), + witness_promise_results: None, + witness_rlc_lookup: None, + witness_gen_only: false, + copy_manager: None, + comp_loader: Box::new(SingleComponentLoaderImpl::::new( + params.comp_loader_params, + )), + _phantom: PhantomData, + } + } + + fn get_params(&self) -> Self::Params { + self.params.clone() + } + + fn clear_witnesses(&mut self) { + self.witness_promise_results = None; + self.witness_rlc_lookup = None; + self.copy_manager = None; + } + + fn configure_with_params( + meta: &mut ConstraintSystem, + _params: Self::Params, + ) -> Self::Config { + // TODO: adjust num of columns based on params. + let dyn_lookup_config = BasicDynLookupConfig::new(meta, || SecondPhase, 1); + Self::Config { dyn_lookup_config } + } + fn calculate_params(&mut self) -> Self::Params { + self.params.clone() + } +} + +impl> PromiseBuilder for PromiseLoader { + fn get_component_type_dependencies() -> Vec { + vec![T::get_type_id()] + } + fn extract_loader_params_per_component_type( + params: &Self::Params, + ) -> Vec { + vec![LoaderParamsPerComponentType { + component_type_id: T::get_type_id(), + loader_params: params.comp_loader_params.clone(), + }] + } + fn fulfill_promise_results(&mut self, promise_results_getter: &impl PromiseResultsGetter) { + let component_type_id = self.comp_loader.get_component_type_id(); + let promise_results = promise_results_getter + .get_results_by_component_type_id(&component_type_id) + .unwrap_or_else(|| { + panic!("missing promise results for component type id {:?}", component_type_id) + }); + self.comp_loader.load_promise_results(promise_results.clone()); + // TODO: shard size check + } + + fn virtual_assign_phase0( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_commit_setter: &mut impl PromiseCommitSetter, + ) { + assert!(self.witness_promise_results.is_none()); + self.witness_gen_only = builder.witness_gen_only(); + + let (commit, witness_promise_results) = + self.comp_loader.assign_and_compute_commitment(builder); + + let component_type_id = self.comp_loader.get_component_type_id(); + promise_commit_setter.set_commit_by_component_type_id(component_type_id, commit); + + self.witness_promise_results = Some(witness_promise_results); + } + + fn raw_synthesize_phase0(&mut self, _config: &Self::Config, _layouter: &mut impl Layouter) { + // Do nothing. + } + + fn virtual_assign_phase1( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_calls_getter: &mut impl PromiseCallsGetter, + ) { + assert!(self.witness_promise_results.is_some()); + let calls = promise_calls_getter + .get_calls_by_component_type_id(&self.comp_loader.get_component_type_id()) + .unwrap() + .values() + .flatten() + .collect_vec(); + let (to_lookup_rlc, lookup_table_rlc) = self.comp_loader.generate_lookup_rlc( + builder, + &calls, + self.witness_promise_results.as_ref().unwrap(), + ); + + self.witness_rlc_lookup = Some((to_lookup_rlc, lookup_table_rlc)); + + self.copy_manager = Some(builder.copy_manager().clone()); + } + + fn raw_synthesize_phase1(&mut self, config: &Self::Config, layouter: &mut impl Layouter) { + assert!(self.witness_rlc_lookup.is_some()); + + let (to_lookup, lookup_table) = self.witness_rlc_lookup.as_ref().unwrap(); + let dyn_lookup_config = &config.dyn_lookup_config; + + let copy_manager = (!self.witness_gen_only).then(|| self.copy_manager.as_ref().unwrap()); + dyn_lookup_config.assign_virtual_table_to_raw( + layouter.namespace(|| { + format!( + "promise loader adds advice to lookup for {}", + self.comp_loader.get_component_type_name() + ) + }), + lookup_table.iter().map(|a| [*a; 1]), + copy_manager, + ); + + dyn_lookup_config.assign_virtual_to_lookup_to_raw( + layouter.namespace(|| { + format!( + "promise loader loads lookup table {}", + self.comp_loader.get_component_type_name() + ) + }), + to_lookup.iter().map(|a| [*a; 1]), + copy_manager, + ); + } +} diff --git a/axiom-eth/src/utils/component/promise_loader/tests/combo.rs b/axiom-eth/src/utils/component/promise_loader/tests/combo.rs new file mode 100644 index 00000000..7df48b5c --- /dev/null +++ b/axiom-eth/src/utils/component/promise_loader/tests/combo.rs @@ -0,0 +1,43 @@ +use crate::{ + halo2curves::bn256::Fr, + keccak::types::ComponentTypeKeccak, + utils::component::{ + circuit::{LoaderParamsPerComponentType, PromiseBuilder}, + promise_loader::{ + combo::PromiseBuilderCombo, + comp_loader::SingleComponentLoaderParams, + single::{PromiseLoader, PromiseLoaderParams}, + }, + tests::dummy_comp::ComponentTypeAdd, + ComponentType, + }, +}; + +#[test] +fn test_extract_loader_params_per_component_type() { + type Loader = PromiseBuilderCombo< + Fr, + PromiseLoader>, + PromiseLoader>, + >; + let comp_loader_params_1 = SingleComponentLoaderParams::new(3, vec![200]); + let comp_loader_params_2 = SingleComponentLoaderParams::new(2, vec![20]); + let params = ( + PromiseLoaderParams { comp_loader_params: comp_loader_params_1.clone() }, + PromiseLoaderParams { comp_loader_params: comp_loader_params_2.clone() }, + ); + let result = Loader::extract_loader_params_per_component_type(¶ms); + assert_eq!( + result, + vec![ + LoaderParamsPerComponentType { + component_type_id: ComponentTypeKeccak::::get_type_id(), + loader_params: comp_loader_params_1 + }, + LoaderParamsPerComponentType { + component_type_id: ComponentTypeAdd::::get_type_id(), + loader_params: comp_loader_params_2 + } + ] + ); +} diff --git a/axiom-eth/src/utils/component/promise_loader/tests/comp_loader.rs b/axiom-eth/src/utils/component/promise_loader/tests/comp_loader.rs new file mode 100644 index 00000000..14badced --- /dev/null +++ b/axiom-eth/src/utils/component/promise_loader/tests/comp_loader.rs @@ -0,0 +1,242 @@ +use super::super::comp_loader::{ + BasicComponentCommiter, ComponentCommiter, SingleComponentLoader, SingleComponentLoaderImpl, + SingleComponentLoaderParams, +}; +use crate::Field; +use crate::{ + halo2curves::bn256::Fr, + rlc::circuit::builder::RlcCircuitBuilder, + utils::component::{ + tests::{ + dummy_comp::{ComponentTypeAdd, LogicalInputAdd, LogicalOutputAdd}, + sum_comp::{ComponentTypeSum, SumLogicalInput}, + }, + utils::compute_poseidon, + ComponentPromiseResult, ComponentPromiseResultsInMerkle, ComponentType, FlattenVirtualRow, + FlattenVirtualTable, LogicalResult, PromiseShardMetadata, + }, +}; +use halo2_base::{gates::circuit::builder::BaseCircuitBuilder, AssignedValue}; +use itertools::Itertools; +use lazy_static::lazy_static; + +type AddLogicalResult = LogicalResult>; +lazy_static! { + static ref MOCK_GAMMA: Fr = Fr::from(100u64); + static ref ADD_LOGICAL_RESUTLS_1: Vec = vec![ + AddLogicalResult::new( + LogicalInputAdd { a: Fr::from(1u64), b: Fr::from(2u64) }, + LogicalOutputAdd { c: Fr::from(3u64) }, + ), + AddLogicalResult::new( + LogicalInputAdd { a: Fr::from(4u64), b: Fr::from(5u64) }, + LogicalOutputAdd { c: Fr::from(9u64) }, + ), + ]; + static ref ADD_RESULTS_1_SQUEZZED_VT: Vec = + vec![1u64, 2, 3, 4, 5, 9].into_iter().map(Fr::from).collect_vec(); + static ref ADD_RESULTS_1_COMMIT: Fr = compute_poseidon(&ADD_RESULTS_1_SQUEZZED_VT); + static ref ADD_RESULTS_1_RLC: Vec = vec![Fr::from(10203), Fr::from(40509)]; + static ref ADD_LOGICAL_RESUTLS_2: Vec = vec![AddLogicalResult::new( + LogicalInputAdd { a: Fr::from(8u64), b: Fr::from(9u64) }, + LogicalOutputAdd { c: Fr::from(17u64) }, + ),]; + static ref ADD_RESULTS_2_SQUEZZED_VT: Vec = + vec![8u64, 9, 17].into_iter().map(Fr::from).collect_vec(); + static ref ADD_RESULTS_2_COMMIT: Fr = compute_poseidon(&ADD_RESULTS_2_SQUEZZED_VT); + static ref ADD_RESULTS_2_RLC: Vec = vec![Fr::from(80917)]; + static ref ADD_RESULTS_LEAVES: Vec> = vec![ + PromiseShardMetadata:: { + commit: *ADD_RESULTS_1_COMMIT, + capacity: ADD_LOGICAL_RESUTLS_1.len() + }, + PromiseShardMetadata:: { + commit: *ADD_RESULTS_2_COMMIT, + capacity: ADD_LOGICAL_RESUTLS_2.len() + } + ]; + static ref ADD_RESULTS_ROOT: Fr = + compute_poseidon(&[*ADD_RESULTS_1_COMMIT, *ADD_RESULTS_2_COMMIT]); +} + +fn squeeze_vritual_table(vt: FlattenVirtualTable) -> Vec { + vt.into_iter().flat_map(|(f_in, f_out)| [f_in.fields, f_out.fields]).flatten().collect_vec() +} + +fn assigned_flatten_vt_to_value( + vt: FlattenVirtualTable>, +) -> FlattenVirtualTable { + vt.clone().into_iter().map(|(f_in, f_out)| (f_in.into(), f_out.into())).collect_vec() +} + +fn logical_results_to_component_results>( + lrs: Vec>, +) -> Vec> { + lrs.into_iter().map(|lr| lr.into()).collect_vec() +} + +fn verify_component_loader>( + promise_results: ComponentPromiseResultsInMerkle, + comp_loader_params: SingleComponentLoaderParams, + mock_gamma: F, + expected_squeezed_vt: Vec, + expected_commit: F, + expected_promise_rlcs: Vec, +) { + let mut comp_loader = SingleComponentLoaderImpl::::new(comp_loader_params); + comp_loader.load_promise_results(promise_results); + let mut mock_builder = RlcCircuitBuilder::::new(false, 32).use_k(18).use_lookup_bits(8); + let (commit, flatten_vt) = comp_loader.assign_and_compute_commitment(&mut mock_builder); + let flatten_vt_val = assigned_flatten_vt_to_value(flatten_vt.clone()); + let squeezed_vt = squeeze_vritual_table(flatten_vt_val); + assert_eq!(squeezed_vt, expected_squeezed_vt); + assert_eq!(*commit.value(), expected_commit); + // Mock gamma to test RLC + mock_builder.gamma = Some(mock_gamma); + let (_, vt_rlcs) = comp_loader.generate_lookup_rlc(&mut mock_builder, &[], &flatten_vt); + assert_eq!(vt_rlcs.into_iter().map(|rlc| *rlc.value()).collect_vec(), expected_promise_rlcs); + // Clear to avoid warning outputs + mock_builder.clear(); +} + +#[test] +fn test_component_loader_1_shard() { + let promise_results = + ComponentPromiseResultsInMerkle::from_single_shard(ADD_LOGICAL_RESUTLS_1.clone()); + + let comp_loader_params = SingleComponentLoaderParams::new(2, vec![2]); + verify_component_loader::>( + promise_results, + comp_loader_params, + *MOCK_GAMMA, + ADD_RESULTS_1_SQUEZZED_VT.clone(), + *ADD_RESULTS_1_COMMIT, + ADD_RESULTS_1_RLC.clone(), + ); +} + +#[test] +fn test_component_loader_1_shard_3_times() { + let promise_results = ComponentPromiseResultsInMerkle::new( + ADD_RESULTS_LEAVES.clone(), + // Read shard 0 three times. + vec![ + (0, logical_results_to_component_results(ADD_LOGICAL_RESUTLS_1.clone())), + (0, logical_results_to_component_results(ADD_LOGICAL_RESUTLS_1.clone())), + (0, logical_results_to_component_results(ADD_LOGICAL_RESUTLS_1.clone())), + ], + ); + + // Read 3 shards with capacity = 2. + let comp_loader_params = SingleComponentLoaderParams::new(2, vec![2, 2, 2]); + + verify_component_loader::>( + promise_results, + comp_loader_params, + *MOCK_GAMMA, + [ + ADD_RESULTS_1_SQUEZZED_VT.clone(), + ADD_RESULTS_1_SQUEZZED_VT.clone(), + ADD_RESULTS_1_SQUEZZED_VT.clone(), + ] + .concat(), + *ADD_RESULTS_ROOT, + [ADD_RESULTS_1_RLC.clone(), ADD_RESULTS_1_RLC.clone(), ADD_RESULTS_1_RLC.clone()].concat(), + ); +} + +#[test] +fn test_component_loader_2_shard() { + let promise_results = ComponentPromiseResultsInMerkle::new( + ADD_RESULTS_LEAVES.clone(), + // Read shard 0 twice times and shard 1 once. + vec![ + (0, logical_results_to_component_results(ADD_LOGICAL_RESUTLS_1.clone())), + (1, logical_results_to_component_results(ADD_LOGICAL_RESUTLS_2.clone())), + (0, logical_results_to_component_results(ADD_LOGICAL_RESUTLS_1.clone())), + ], + ); + + // Read 3 shards with capacity = [2,1,2]. + let comp_loader_params = SingleComponentLoaderParams::new(2, vec![2, 1, 2]); + + verify_component_loader::>( + promise_results, + comp_loader_params, + *MOCK_GAMMA, + [ + ADD_RESULTS_1_SQUEZZED_VT.clone(), + ADD_RESULTS_2_SQUEZZED_VT.clone(), + ADD_RESULTS_1_SQUEZZED_VT.clone(), + ] + .concat(), + *ADD_RESULTS_ROOT, + [ADD_RESULTS_1_RLC.clone(), ADD_RESULTS_2_RLC.clone(), ADD_RESULTS_1_RLC.clone()].concat(), + ); +} + +#[test] +fn test_basic_commiter() { + let flatten_add_lrs: Vec> = ADD_LOGICAL_RESUTLS_1 + .clone() + .into_iter() + .flat_map(Vec::>::from) + .collect_vec(); + let mut mock_builder = BaseCircuitBuilder::::new(false).use_k(18).use_lookup_bits(8); + let ctx = mock_builder.main(0); + let assigned_flatten_add_lrs = flatten_add_lrs + .into_iter() + .map(|(f_lr_in, f_lr_out)| (f_lr_in.assign(ctx), f_lr_out.assign(ctx))) + .collect_vec(); + let commit = BasicComponentCommiter::::compute_commitment( + &mut mock_builder, + &assigned_flatten_add_lrs, + ); + assert_eq!(*commit.value(), ADD_RESULTS_1_COMMIT.clone()); +} + +#[test] +fn test_basic_native_commiter() { + let flatten_add_lrs: Vec> = ADD_LOGICAL_RESUTLS_1 + .clone() + .into_iter() + .flat_map(Vec::>::from) + .collect_vec(); + let commit = BasicComponentCommiter::::compute_native_commitment(&flatten_add_lrs); + assert_eq!(commit, ADD_RESULTS_1_COMMIT.clone()); +} + +type SumLogicalResult = LogicalResult>; +lazy_static! { + static ref SUM_LOGICAL_RESUTLS_1: Vec = vec![ + SumLogicalResult::new( + SumLogicalInput { to_sum: vec![3u64, 4u64, 5u64] }, + LogicalOutputAdd { c: Fr::from(12u64) }, + ), + SumLogicalResult::new( + SumLogicalInput { to_sum: vec![] }, + LogicalOutputAdd { c: Fr::from(0) }, + ), + ]; + static ref SUM_RESULTS_1_SQUEZZED_VT: Vec = + vec![0u64, 3, 3, 0, 4, 7, 1, 5, 12, 1, 0, 0].into_iter().map(Fr::from).collect_vec(); + static ref SUM_RESULTS_1_COMMIT: Fr = compute_poseidon(&SUM_RESULTS_1_SQUEZZED_VT); + static ref SUM_RESULTS_1_RLC: Vec = + vec![Fr::from(0), Fr::from(0), Fr::from(3040512), Fr::from(0)]; +} + +#[test] +fn test_component_loader_var_len_1_shard() { + let promise_results = + ComponentPromiseResultsInMerkle::from_single_shard(SUM_LOGICAL_RESUTLS_1.clone()); + + let comp_loader_params = SingleComponentLoaderParams::new(2, vec![4]); + verify_component_loader::>( + promise_results, + comp_loader_params, + *MOCK_GAMMA, + SUM_RESULTS_1_SQUEZZED_VT.clone(), + *SUM_RESULTS_1_COMMIT, + SUM_RESULTS_1_RLC.clone(), + ); +} diff --git a/axiom-eth/src/utils/component/promise_loader/tests/empty.rs b/axiom-eth/src/utils/component/promise_loader/tests/empty.rs new file mode 100644 index 00000000..2dcf4a7c --- /dev/null +++ b/axiom-eth/src/utils/component/promise_loader/tests/empty.rs @@ -0,0 +1,10 @@ +use crate::{ + halo2curves::bn256::Fr, + utils::component::{circuit::PromiseBuilder, promise_loader::empty::EmptyPromiseLoader}, +}; + +#[test] +fn test_extract_loader_params_per_component_type() { + let result = EmptyPromiseLoader::::extract_loader_params_per_component_type(&()); + assert_eq!(result, vec![]); +} diff --git a/axiom-eth/src/utils/component/promise_loader/tests/mod.rs b/axiom-eth/src/utils/component/promise_loader/tests/mod.rs new file mode 100644 index 00000000..0b0b2403 --- /dev/null +++ b/axiom-eth/src/utils/component/promise_loader/tests/mod.rs @@ -0,0 +1,5 @@ +pub mod combo; +pub mod comp_loader; +pub mod empty; +pub mod multi; +pub mod single; diff --git a/axiom-eth/src/utils/component/promise_loader/tests/multi.rs b/axiom-eth/src/utils/component/promise_loader/tests/multi.rs new file mode 100644 index 00000000..f059329e --- /dev/null +++ b/axiom-eth/src/utils/component/promise_loader/tests/multi.rs @@ -0,0 +1,74 @@ +use halo2_base::{gates::GateInstructions, AssignedValue}; + +use crate::{ + component_type_list, + halo2curves::bn256::Fr, + keccak::types::ComponentTypeKeccak, + rlc::{chip::RlcChip, circuit::builder::RlcContextPair}, + utils::component::{ + circuit::{LoaderParamsPerComponentType, PromiseBuilder}, + promise_loader::{ + comp_loader::SingleComponentLoaderParams, + multi::{MultiPromiseLoader, MultiPromiseLoaderParams, RlcAdapter}, + }, + tests::dummy_comp::{ComponentTypeAdd, ComponentTypeAddMul}, + types::{EmptyComponentType, Flatten}, + ComponentType, ComponentTypeId, + }, +}; + +// Just for teseting purpose +struct DummyRlcAdapter {} +impl RlcAdapter for DummyRlcAdapter { + fn to_rlc( + _ctx_pair: RlcContextPair, + _gate: &impl GateInstructions, + _rlc: &RlcChip, + _type_id: &ComponentTypeId, + _io_pairs: &[(Flatten>, Flatten>)], + ) -> Vec> { + vec![] + } +} + +#[test] +fn test_extract_loader_params_per_component_type() { + type Dependencies = component_type_list!( + Fr, + ComponentTypeAdd, + ComponentTypeAddMul, + ComponentTypeKeccak + ); + type Loader = MultiPromiseLoader, Dependencies, DummyRlcAdapter>; + + let comp_loader_params_1 = SingleComponentLoaderParams::new(3, vec![200]); + let comp_loader_params_2 = SingleComponentLoaderParams::new(2, vec![500]); + let comp_loader_params_3 = SingleComponentLoaderParams::new(5, vec![20]); + + let expected_results = vec![ + LoaderParamsPerComponentType { + component_type_id: ComponentTypeAdd::::get_type_id(), + loader_params: comp_loader_params_1.clone(), + }, + LoaderParamsPerComponentType { + component_type_id: ComponentTypeAddMul::::get_type_id(), + loader_params: comp_loader_params_2.clone(), + }, + LoaderParamsPerComponentType { + component_type_id: ComponentTypeKeccak::::get_type_id(), + loader_params: comp_loader_params_3.clone(), + }, + ]; + + let params = MultiPromiseLoaderParams { + params_per_component: [ + (ComponentTypeKeccak::::get_type_id(), comp_loader_params_3), + (ComponentTypeAddMul::::get_type_id(), comp_loader_params_2), + (ComponentTypeAdd::::get_type_id(), comp_loader_params_1), + ] + .into_iter() + .collect(), + }; + let result = Loader::extract_loader_params_per_component_type(¶ms); + assert_eq!(result, expected_results); +} diff --git a/axiom-eth/src/utils/component/promise_loader/tests/single.rs b/axiom-eth/src/utils/component/promise_loader/tests/single.rs new file mode 100644 index 00000000..3e3f9dfc --- /dev/null +++ b/axiom-eth/src/utils/component/promise_loader/tests/single.rs @@ -0,0 +1,29 @@ +use crate::{ + halo2curves::bn256::Fr, + keccak::types::ComponentTypeKeccak, + utils::component::{ + circuit::{LoaderParamsPerComponentType, PromiseBuilder}, + promise_loader::{ + comp_loader::SingleComponentLoaderParams, + single::{PromiseLoader, PromiseLoaderParams}, + }, + ComponentType, + }, +}; + +#[test] +fn test_extract_loader_params_per_component_type() { + let comp_loader_params = SingleComponentLoaderParams::new(3, vec![200]); + let params = PromiseLoaderParams { comp_loader_params: comp_loader_params.clone() }; + let result = + PromiseLoader::>::extract_loader_params_per_component_type( + ¶ms, + ); + assert_eq!( + result, + vec![LoaderParamsPerComponentType { + component_type_id: ComponentTypeKeccak::::get_type_id(), + loader_params: comp_loader_params + }] + ); +} diff --git a/axiom-eth/src/utils/component/promise_loader/utils.rs b/axiom-eth/src/utils/component/promise_loader/utils.rs new file mode 100644 index 00000000..83cef5ce --- /dev/null +++ b/axiom-eth/src/utils/component/promise_loader/utils.rs @@ -0,0 +1,60 @@ +use halo2_base::AssignedValue; + +use crate::{ + utils::component::{circuit::PromiseBuilder, types::LogicalEmpty, ComponentType}, + Field, +}; + +use super::{ + combo::PromiseBuilderCombo, + empty::EmptyPromiseLoader, + multi::{ComponentTypeList, MultiPromiseLoader, RlcAdapter}, + single, +}; + +/// Sub-trait for dummy fulfillment. Only used to create dummy circuits for +/// the purpose of proving/verifying key generation. +pub trait DummyPromiseBuilder: PromiseBuilder { + /// This should be the same behavior as `fulfill_promise_results` but with + /// dummy results. The exact configuration of the results is determined by + /// the `loader_params` of the promise builder. + fn fulfill_dummy_promise_results(&mut self); +} + +impl DummyPromiseBuilder for EmptyPromiseLoader { + fn fulfill_dummy_promise_results(&mut self) {} +} + +impl> DummyPromiseBuilder for single::PromiseLoader { + fn fulfill_dummy_promise_results(&mut self) { + self.comp_loader.load_dummy_promise_results(); + } +} + +impl, SECOND: DummyPromiseBuilder> DummyPromiseBuilder + for PromiseBuilderCombo +{ + fn fulfill_dummy_promise_results(&mut self) { + let (first, second) = &mut self.to_combine; + first.fulfill_dummy_promise_results(); + second.fulfill_dummy_promise_results(); + } +} + +impl< + F: Field, + VT: ComponentType< + F, + OutputValue = LogicalEmpty, + OutputWitness = LogicalEmpty>, + >, + CLIST: ComponentTypeList, + A: RlcAdapter, + > DummyPromiseBuilder for MultiPromiseLoader +{ + fn fulfill_dummy_promise_results(&mut self) { + for comp_loader in &mut self.comp_loaders { + comp_loader.load_dummy_promise_results(); + } + } +} diff --git a/axiom-eth/src/utils/component/tests/collector.rs b/axiom-eth/src/utils/component/tests/collector.rs new file mode 100644 index 00000000..198b75f3 --- /dev/null +++ b/axiom-eth/src/utils/component/tests/collector.rs @@ -0,0 +1,114 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +use halo2_base::AssignedValue; +use lazy_static::lazy_static; + +use crate::{ + halo2_proofs::halo2curves::bn256::Fr, + rlc::circuit::builder::RlcCircuitBuilder, + utils::component::{ + promise_collector::{PromiseCaller, PromiseCollector}, + tests::dummy_comp::LogicalOutputAdd, + ComponentPromiseResultsInMerkle, ComponentType, LogicalResult, + }, +}; + +use super::dummy_comp::{ComponentTypeAdd, LogicalInputAdd}; + +type AddLogicalResult = LogicalResult>; +lazy_static! { + static ref ADD_LOGICAL_RESUTLS_1: Vec = vec![ + AddLogicalResult::new( + LogicalInputAdd { a: Fr::from(1u64), b: Fr::from(2u64) }, + // 1+2=3 but we give 4 here to check if the result is not computed in circuit. + LogicalOutputAdd { c: Fr::from(4u64) }, + ), + AddLogicalResult::new( + LogicalInputAdd { a: Fr::from(4u64), b: Fr::from(5u64) }, + LogicalOutputAdd { c: Fr::from(9u64) }, + ), + ]; + static ref ADD_LOGICAL_RESUTLS_2: Vec = vec![AddLogicalResult::new( + LogicalInputAdd { a: Fr::from(8u64), b: Fr::from(9u64) }, + LogicalOutputAdd { c: Fr::from(17u64) }, + ),]; +} + +#[test] +fn test_promise_call_happy_path() { + let pc = PromiseCollector::::new(vec![ComponentTypeAdd::::get_type_id()]); + let shared_pc = Arc::new(Mutex::new(pc)); + + let promise_results = + ComponentPromiseResultsInMerkle::from_single_shard(ADD_LOGICAL_RESUTLS_1.clone()); + let mut results = HashMap::new(); + results.insert(ComponentTypeAdd::::get_type_id(), promise_results); + shared_pc.lock().unwrap().fulfill(&results); + + shared_pc.lock().unwrap().set_promise_results_ready(true); + let mut mock_builder = RlcCircuitBuilder::::new(false, 32).use_k(18).use_lookup_bits(8); + let ctx = mock_builder.base.main(0); + let a = ctx.load_constant(Fr::from(1u64)); + let b = ctx.load_constant(Fr::from(2u64)); + + let caller = PromiseCaller::::new(shared_pc.clone()); + let call_result = caller + .call::>, ComponentTypeAdd>( + ctx, + LogicalInputAdd { a, b }, + ) + .unwrap(); + // 1+2=3 but we give 4 here to check if the result is not computed in circuit. + assert_eq!(*call_result.c.value(), Fr::from(4)); + // To avoid warning outputs. + mock_builder.clear(); +} + +#[test] +#[should_panic] +fn test_promise_call_not_fulfilled() { + let pc = PromiseCollector::::new(vec![ComponentTypeAdd::::get_type_id()]); + let shared_pc = Arc::new(Mutex::new(pc)); + let caller = PromiseCaller::::new(shared_pc.clone()); + shared_pc.lock().unwrap().set_promise_results_ready(true); + let mut mock_builder = RlcCircuitBuilder::::new(false, 32).use_k(18).use_lookup_bits(8); + let ctx = mock_builder.base.main(0); + let a = ctx.load_constant(Fr::from(1u64)); + let b = ctx.load_constant(Fr::from(2u64)); + caller + .call::>, ComponentTypeAdd>( + ctx, + LogicalInputAdd { a, b }, + ) + .unwrap(); +} + +#[test] +#[should_panic] +fn test_promise_call_missing_result() { + let pc = PromiseCollector::::new(vec![ComponentTypeAdd::::get_type_id()]); + let shared_pc = Arc::new(Mutex::new(pc)); + + let promise_results = + ComponentPromiseResultsInMerkle::from_single_shard(ADD_LOGICAL_RESUTLS_2.clone()); + let mut results = HashMap::new(); + results.insert(ComponentTypeAdd::::get_type_id(), promise_results); + shared_pc.lock().unwrap().fulfill(&results); + + shared_pc.lock().unwrap().set_promise_results_ready(true); + let mut mock_builder = RlcCircuitBuilder::::new(false, 32).use_k(18).use_lookup_bits(8); + let ctx = mock_builder.base.main(0); + let a = ctx.load_constant(Fr::from(1u64)); + let b = ctx.load_constant(Fr::from(2u64)); + + let caller = PromiseCaller::::new(shared_pc.clone()); + caller + .call::>, ComponentTypeAdd>( + ctx, + LogicalInputAdd { a, b }, + ) + .unwrap(); +} diff --git a/axiom-eth/src/utils/component/tests/dummy_comp.rs b/axiom-eth/src/utils/component/tests/dummy_comp.rs new file mode 100644 index 00000000..67fbc075 --- /dev/null +++ b/axiom-eth/src/utils/component/tests/dummy_comp.rs @@ -0,0 +1,300 @@ +use crate::{ + impl_flatten_conversion, impl_logical_input, + rlc::circuit::builder::RlcCircuitBuilder, + utils::{ + build_utils::dummy::DummyFrom, + component::{ + circuit::{CoreBuilderInput, CoreBuilderOutput, CoreBuilderOutputParams}, + utils::{get_logical_value, into_key}, + }, + }, +}; +use halo2_base::{ + gates::GateInstructions, + halo2_proofs::{circuit::Layouter, plonk::ConstraintSystem}, + AssignedValue, +}; + +use super::{ + circuit::{ComponentBuilder, CoreBuilder}, + promise_collector::PromiseCaller, + promise_loader::flatten_witness_to_rlc, + types::{FixLenLogical, LogicalEmpty}, + utils::load_logical_value, + *, +}; + +// =============== Add Component =============== + +const ADD_INPUT_FIELD_SIZE: [usize; 2] = [64, 64]; +const ADD_OUTPUT_FIELD_SIZE: [usize; 1] = [128]; + +/// a + b +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct LogicalInputAdd { + pub a: T, + pub b: T, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)] +pub struct LogicalOutputAdd { + pub c: T, +} + +impl TryFrom> for LogicalInputAdd { + type Error = anyhow::Error; + fn try_from(mut value: Vec) -> Result { + if value.len() != 2 { + return Err(anyhow::anyhow!("invalid length")); + } + let b = value.pop().unwrap(); + let a = value.pop().unwrap(); + + Ok(LogicalInputAdd:: { a, b }) + } +} + +impl LogicalInputAdd { + pub fn flatten(self) -> Vec { + vec![self.a, self.b] + } +} + +impl_flatten_conversion!(LogicalInputAdd, ADD_INPUT_FIELD_SIZE); +impl_logical_input!(LogicalInputAdd, 1); + +impl PromiseCallWitness for LogicalInputAdd> { + fn get_component_type_id(&self) -> ComponentTypeId { + ComponentTypeAdd::::get_type_id() + } + fn get_capacity(&self) -> usize { + 1 + } + fn to_rlc( + &self, + (_, rlc_ctx): (&mut Context, &mut Context), + _range_chip: &RangeChip, + rlc_chip: &RlcChip, + ) -> AssignedValue { + flatten_witness_to_rlc(rlc_ctx, rlc_chip, &self.clone().into()) + } + fn to_typeless_logical_input(&self) -> TypelessLogicalInput { + let f_a: Flatten> = self.clone().into(); + let f_v: Flatten = f_a.into(); + let l_v: LogicalInputAdd = f_v.try_into().unwrap(); + into_key(l_v) + } + fn get_mock_output(&self) -> Flatten { + let output_val: as ComponentType>::OutputValue = Default::default(); + output_val.into() + } + fn as_any(&self) -> &dyn Any { + self + } +} + +// This should be done by marco. +impl TryFrom> for LogicalOutputAdd { + type Error = anyhow::Error; + + fn try_from(value: Flatten) -> std::result::Result { + if value.field_size != ADD_OUTPUT_FIELD_SIZE { + return Err(anyhow::anyhow!("invalid field size for add output")); + } + Ok(LogicalOutputAdd:: { c: value.fields[0] }) + } +} +// This should be done by marco. +impl From> for Flatten { + fn from(val: LogicalOutputAdd) -> Self { + Flatten:: { fields: vec![val.c], field_size: &ADD_OUTPUT_FIELD_SIZE } + } +} +// This should be done by marco. +impl FixLenLogical for LogicalOutputAdd { + fn get_field_size() -> &'static [usize] { + &ADD_OUTPUT_FIELD_SIZE + } +} + +#[derive(Debug, Clone)] +pub struct ComponentTypeAdd { + _phantom: std::marker::PhantomData, +} + +impl ComponentType for ComponentTypeAdd { + type InputValue = LogicalInputAdd; + type InputWitness = LogicalInputAdd>; + type OutputValue = LogicalOutputAdd; + type OutputWitness = LogicalOutputAdd>; + type LogicalInput = LogicalInputAdd; + + fn get_type_id() -> ComponentTypeId { + "ComponentTypeAdd".to_string() + } + + fn logical_result_to_virtual_rows_impl( + ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + vec![(ins.input.clone(), ins.output.clone())] + } + fn logical_input_to_virtual_rows_impl(li: &Self::LogicalInput) -> Vec { + vec![li.clone()] + } +} + +const ADD_MUL_INPUT_FIELD_SIZE: [usize; 3] = [64, 64, 64]; + +/// a * b + c +#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct LogicalInputAddMul { + pub a: T, + pub b: T, + pub c: T, +} + +pub type LogicalOutputAddMul = LogicalOutputAdd; + +impl TryFrom> for LogicalInputAddMul { + type Error = anyhow::Error; + fn try_from(mut value: Vec) -> Result { + if value.len() != 3 { + return Err(anyhow::anyhow!("invalid length")); + } + let c = value.pop().unwrap(); + let b = value.pop().unwrap(); + let a = value.pop().unwrap(); + + Ok(LogicalInputAddMul:: { a, b, c }) + } +} + +impl LogicalInputAddMul { + pub fn flatten(self) -> Vec { + vec![self.a, self.b, self.c] + } +} + +impl_flatten_conversion!(LogicalInputAddMul, ADD_MUL_INPUT_FIELD_SIZE); +impl_logical_input!(LogicalInputAddMul, 1); + +// =============== AddMul Component =============== +#[derive(Debug, Clone)] +pub struct ComponentTypeAddMul { + _phantom: std::marker::PhantomData, +} + +impl ComponentType for ComponentTypeAddMul { + type InputValue = LogicalInputAddMul; + type InputWitness = LogicalInputAddMul>; + type OutputValue = LogicalOutputAddMul; + type OutputWitness = LogicalOutputAddMul>; + type LogicalInput = LogicalInputAddMul; + + fn get_type_id() -> ComponentTypeId { + "axiom-eth:ComponentTypeAddMul".to_string() + } + + fn logical_result_to_virtual_rows_impl( + ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + vec![(ins.input.clone(), ins.output.clone())] + } + fn logical_input_to_virtual_rows_impl(li: &Self::LogicalInput) -> Vec { + vec![li.clone()] + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct CoreInputAddMul { + pub inputs: Vec>, +} + +impl DummyFrom for CoreInputAddMul { + fn dummy_from(params: CoreBuilderOutputParams) -> Self { + Self { + inputs: params + .cap_per_shard() + .iter() + .flat_map(|c| { + vec![LogicalInputAddMul:: { a: F::ZERO, b: F::ZERO, c: F::ZERO }; *c] + }) + .collect(), + } + } +} + +pub struct BuilderAddMul { + input: Option>, + params: CoreBuilderOutputParams, +} + +impl ComponentBuilder for BuilderAddMul { + type Config = (); + type Params = CoreBuilderOutputParams; + + fn new(params: CoreBuilderOutputParams) -> Self { + Self { input: None, params } + } + fn get_params(&self) -> Self::Params { + self.params.clone() + } + fn clear_witnesses(&mut self) {} + fn configure_with_params( + _meta: &mut ConstraintSystem, + _params: Self::Params, + ) -> Self::Config { + } + fn calculate_params(&mut self) -> Self::Params { + self.params.clone() + } +} + +impl CoreBuilder for BuilderAddMul { + type CompType = ComponentTypeAddMul; + type PublicInstanceValue = LogicalEmpty; + type PublicInstanceWitness = LogicalEmpty>; + type CoreInput = CoreInputAddMul; + fn feed_input(&mut self, input: Self::CoreInput) -> anyhow::Result<()> { + self.input = Some(input); + Ok(()) + } + + fn virtual_assign_phase0( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_caller: PromiseCaller, + ) -> CoreBuilderOutput { + let range_chip = builder.range_chip(); + let ctx = builder.base.main(0); + let inputs: &Vec> = &self.input.as_ref().unwrap().inputs; + let (vt, lr): (FlattenVirtualTable>, Vec>) = inputs + .iter() + .map(|input| { + let witness_input = load_logical_value::< + F, + LogicalInputAddMul, + LogicalInputAddMul>, + >(ctx, input); + let witness_mul = range_chip.gate.mul(ctx, witness_input.a, witness_input.b); + let to_add = LogicalInputAdd { a: witness_mul, b: witness_input.c }; + let add_result = promise_caller + .call::>, ComponentTypeAdd>(ctx, to_add) + .unwrap(); + let add_result_val = get_logical_value(&add_result); + ( + (witness_input.into(), add_result.into()), + LogicalResult::::new(input.clone(), add_result_val), + ) + }) + .unzip(); + CoreBuilderOutput:: { + public_instances: vec![], + virtual_table: vt, + logical_results: lr, + } + } + fn raw_synthesize_phase0(&mut self, _config: &Self::Config, _layouter: &mut impl Layouter) {} + fn virtual_assign_phase1(&mut self, _builder: &mut RlcCircuitBuilder) {} + fn raw_synthesize_phase1(&mut self, _config: &Self::Config, _layouter: &mut impl Layouter) {} +} diff --git a/axiom-eth/src/utils/component/tests/mod.rs b/axiom-eth/src/utils/component/tests/mod.rs new file mode 100644 index 00000000..57641383 --- /dev/null +++ b/axiom-eth/src/utils/component/tests/mod.rs @@ -0,0 +1,276 @@ +use anyhow::Ok; +use ark_std::{end_timer, start_timer}; + +use crate::{ + rlc::circuit::RlcCircuitParams, + utils::{ + build_utils::pinning::CircuitPinningInstructions, + component::{ + circuit::CoreBuilderOutputParams, + promise_loader::comp_loader::SingleComponentLoaderParams, + }, + }, +}; +use halo2_base::{ + gates::circuit::BaseCircuitParams, + halo2_proofs::{ + halo2curves::bn256::{Bn256, Fr}, + plonk::{keygen_pk, keygen_vk}, + poly::kzg::commitment::ParamsKZG, + }, + utils::testing::{check_proof_with_instances, gen_proof_with_instances}, +}; +use lazy_static::lazy_static; +use rand_core::OsRng; + +use super::{ + circuit::ComponentCircuitImpl, + promise_loader::single::{PromiseLoader, PromiseLoaderParams}, + *, +}; + +pub mod collector; +/// Dummy components for testing. +pub mod dummy_comp; +pub mod sum_comp; +use dummy_comp::*; + +type CompCircuit = + ComponentCircuitImpl, PromiseLoader>>; + +fn build_dummy_component_circuit( + k: usize, + comp_loader_params: SingleComponentLoaderParams, + add_mul_cap: usize, + input: CoreInputAddMul, + promise_results: &GroupedPromiseResults, +) -> anyhow::Result { + let prompt_rlc_params = RlcCircuitParams { + base: BaseCircuitParams { + k, + lookup_bits: Some(8), + num_instance_columns: 1, + ..Default::default() + }, + num_rlc_columns: 1, + }; + let component_circuit: CompCircuit = ComponentCircuitImpl::new( + CoreBuilderOutputParams::new(vec![add_mul_cap]), + PromiseLoaderParams { comp_loader_params: comp_loader_params.clone() }, + prompt_rlc_params, + ); + component_circuit.feed_input(Box::new(input.clone()))?; + component_circuit.fulfill_promise_results(promise_results)?; + Ok(component_circuit) +} + +fn prover_test_dummy_component( + comp_loader_params: SingleComponentLoaderParams, + add_mul_cap: usize, + input: CoreInputAddMul, + promise_results: GroupedPromiseResults, +) -> anyhow::Result<()> { + let k = 16; + let mut component_circuit = build_dummy_component_circuit( + k, + comp_loader_params.clone(), + add_mul_cap, + input.clone(), + &promise_results, + )?; + component_circuit.calculate_params(); + + let mut rng = OsRng; + let params = ParamsKZG::::setup(k as u32, &mut rng); + let vk_time = start_timer!(|| "vk gen"); + let vk = keygen_vk(¶ms, &component_circuit).unwrap(); + end_timer!(vk_time); + let pk_time = start_timer!(|| "pk gen"); + let pk = keygen_pk(¶ms, vk, &component_circuit).unwrap(); + end_timer!(pk_time); + + // Reconstruct the circuit from pinning. + let pinning = component_circuit.pinning(); + component_circuit = CompCircuit::new( + CoreBuilderOutputParams::new(vec![add_mul_cap]), + PromiseLoaderParams { comp_loader_params: comp_loader_params.clone() }, + pinning.params.clone(), + ) + .use_break_points(pinning.break_points); + component_circuit.feed_input(Box::new(input))?; + component_circuit.fulfill_promise_results(&promise_results)?; + + let pf_time = start_timer!(|| "proof gen"); + let instances: Vec = component_circuit.get_public_instances().into(); + + let proof = gen_proof_with_instances(¶ms, &pk, component_circuit, &[&instances]); + end_timer!(pf_time); + + let verify_time = start_timer!(|| "verify"); + check_proof_with_instances(¶ms, pk.get_vk(), &proof, &[&instances], true); + end_timer!(verify_time); + + Ok(()) +} + +lazy_static! { + static ref ADD_MUL_INPUT: CoreInputAddMul = CoreInputAddMul:: { + inputs: vec![ + LogicalInputAddMul:: { a: Fr::from(1u64), b: Fr::from(2u64), c: Fr::from(3u64) }, + LogicalInputAddMul:: { a: Fr::from(4u64), b: Fr::from(5u64), c: Fr::from(6u64) }, + ] + }; + static ref ADD_MUL_RESULT: Vec>> = vec![ + LogicalResult::>::new( + LogicalInputAddMul:: { a: Fr::from(1u64), b: Fr::from(2u64), c: Fr::from(3u64) }, + LogicalOutputAddMul:: { c: Fr::from(5u64) }, + ), + LogicalResult::>::new( + LogicalInputAddMul:: { a: Fr::from(4u64), b: Fr::from(5u64), c: Fr::from(6u64) }, + LogicalOutputAddMul:: { c: Fr::from(26u64) }, + ) + ]; + static ref ADD_RESULT_SHARD1: Vec>> = + vec![LogicalResult::>::new( + LogicalInputAdd { a: Fr::from(7u64), b: Fr::from(8u64) }, + LogicalOutputAdd:: { c: Fr::from(15u64) }, + )]; + static ref ADD_RESULT_SHARD2: Vec>> = vec![ + LogicalResult::>::new( + LogicalInputAdd { a: Fr::from(2u64), b: Fr::from(3u64) }, + LogicalOutputAdd:: { c: Fr::from(5u64) }, + ), + LogicalResult::>::new( + LogicalInputAdd { a: Fr::from(20u64), b: Fr::from(6u64) }, + LogicalOutputAdd:: { c: Fr::from(26u64) }, + ), + ]; +} + +/// Helper function to create ComponentPromiseResults from multiple shards. +pub fn from_multi_shards>( + lrs: Vec>>, + selected_shards: Vec, +) -> ComponentPromiseResultsInMerkle { + let result_per_shard = + lrs.into_iter().map(ComponentPromiseResultsInMerkle::::from_single_shard).collect_vec(); + let leaves = result_per_shard.iter().map(|r| r.leaves[0].clone()).collect_vec(); + ComponentPromiseResultsInMerkle::::new( + leaves, + selected_shards + .into_iter() + .map(|idx| (idx, result_per_shard[idx].shards()[0].1.clone())) + .collect_vec(), + ) +} + +#[test] +fn test_input_height2_read1() -> anyhow::Result<()> { + // Read 1 shard from a merkle tree with height <= 2. + let comp_loader_params = SingleComponentLoaderParams::new(2, vec![2]); + let input = ADD_MUL_INPUT.clone(); + // mock add component. + let add_results_shard1 = ADD_RESULT_SHARD1.clone(); + let add_results_shard2 = ADD_RESULT_SHARD2.clone(); + let mut promise_results = HashMap::new(); + promise_results.insert( + ComponentTypeAdd::::get_type_id(), + from_multi_shards(vec![add_results_shard1, add_results_shard2], vec![1]), + ); + prover_test_dummy_component(comp_loader_params, input.inputs.len(), input, promise_results) +} + +#[test] +fn test_input_height2_read2() -> anyhow::Result<()> { + // Read 2 shard from a merkle tree with height <= 2. + let comp_loader_params = SingleComponentLoaderParams::new(2, vec![1, 2]); + let input = ADD_MUL_INPUT.clone(); + // mock add component. + let add_results_shard1 = ADD_RESULT_SHARD1.clone(); + let add_results_shard2 = ADD_RESULT_SHARD2.clone(); + let mut promise_results = HashMap::new(); + promise_results.insert( + ComponentTypeAdd::::get_type_id(), + from_multi_shards(vec![add_results_shard1, add_results_shard2], vec![0, 1]), + ); + prover_test_dummy_component(comp_loader_params, input.inputs.len(), input, promise_results) +} + +#[test] +fn test_input_height2_read_1shard_twice() -> anyhow::Result<()> { + // Read 2 shards from a merkle tree with height <= 2. + let comp_loader_params = SingleComponentLoaderParams::new(2, vec![2, 2]); + let input = ADD_MUL_INPUT.clone(); + // mock add component. + let add_results_shard1 = ADD_RESULT_SHARD1.clone(); + let add_results_shard2 = ADD_RESULT_SHARD2.clone(); + let mut promise_results = HashMap::new(); + promise_results.insert( + ComponentTypeAdd::::get_type_id(), + // Read shard 0 twice. + from_multi_shards(vec![add_results_shard1, add_results_shard2], vec![1, 1]), + ); + prover_test_dummy_component(comp_loader_params, input.inputs.len(), input, promise_results) +} + +#[test] +fn test_input_height0_read1() -> anyhow::Result<()> { + // Read 1 shard from a merkle tree with height = 0. + let comp_loader_params = SingleComponentLoaderParams::new(0, vec![2]); + let input = ADD_MUL_INPUT.clone(); + // mock add component. + let add_results_shard2 = ADD_RESULT_SHARD2.clone(); + let mut promise_results = HashMap::new(); + promise_results.insert( + ComponentTypeAdd::::get_type_id(), + from_multi_shards(vec![add_results_shard2], vec![0]), + ); + prover_test_dummy_component(comp_loader_params, input.inputs.len(), input, promise_results) +} + +#[test] +#[should_panic] +fn test_input_height2_missing_result() { + // Read 1 shard with cap=1 from a merkle tree with height <= 2. + let comp_loader_params = SingleComponentLoaderParams::new(2, vec![1]); + let input = ADD_MUL_INPUT.clone(); + // mock add component. + let add_results_shard1 = ADD_RESULT_SHARD1.clone(); + let add_results_shard2 = ADD_RESULT_SHARD2.clone(); + let mut promise_results = HashMap::new(); + promise_results.insert( + ComponentTypeAdd::::get_type_id(), + from_multi_shards( + vec![add_results_shard1, add_results_shard2], + // Shard 0 doesn't have all the promise results, so it should panic. + vec![0], + ), + ); + prover_test_dummy_component(comp_loader_params, input.inputs.len(), input, promise_results) + .unwrap(); +} + +#[test] +fn test_compute_outputs() -> anyhow::Result<()> { + // Read 1 shard from a merkle tree with height <= 2. + let comp_loader_params = SingleComponentLoaderParams::new(2, vec![2]); + let input = ADD_MUL_INPUT.clone(); + // mock add component. + let add_results_shard1 = ADD_RESULT_SHARD1.clone(); + let add_results_shard2 = ADD_RESULT_SHARD2.clone(); + let mut promise_results = HashMap::new(); + promise_results.insert( + ComponentTypeAdd::::get_type_id(), + from_multi_shards(vec![add_results_shard1, add_results_shard2], vec![1]), + ); + let circuit = build_dummy_component_circuit( + 16, + comp_loader_params, + input.inputs.len(), + input, + &promise_results, + )?; + let output = circuit.compute_outputs()?; + assert_eq!(output, SelectedDataShardsInMerkle::from_single_shard(ADD_MUL_RESULT.clone())); + Ok(()) +} diff --git a/axiom-eth/src/utils/component/tests/sum_comp.rs b/axiom-eth/src/utils/component/tests/sum_comp.rs new file mode 100644 index 00000000..44ffdb67 --- /dev/null +++ b/axiom-eth/src/utils/component/tests/sum_comp.rs @@ -0,0 +1,135 @@ +use crate::impl_flatten_conversion; +use halo2_base::{gates::GateInstructions, AssignedValue}; +use itertools::Itertools; + +use super::*; + +// An example of variable length component. +// =============== Sum Component =============== + +/// SumLogicalInput is the logical input of SumLogicalInput Component. +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct SumLogicalInput { + pub to_sum: Vec, +} + +impl LogicalInputValue for SumLogicalInput { + fn get_capacity(&self) -> usize { + if self.to_sum.is_empty() { + 1 + } else { + self.to_sum.len() + } + } +} + +const SUM_INPUT_FIELD_SIZE: [usize; 2] = [1, 64]; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct FixLenLogicalInputSum { + pub is_final: T, + pub to_add: T, +} +impl TryFrom> for FixLenLogicalInputSum { + type Error = anyhow::Error; + fn try_from(mut value: Vec) -> Result { + if value.len() != 2 { + return Err(anyhow::anyhow!("invalid length")); + } + let to_add = value.pop().unwrap(); + let is_final = value.pop().unwrap(); + + Ok(FixLenLogicalInputSum:: { is_final, to_add }) + } +} + +impl FixLenLogicalInputSum { + pub fn flatten(self) -> Vec { + vec![self.is_final, self.to_add] + } +} + +impl_flatten_conversion!(FixLenLogicalInputSum, SUM_INPUT_FIELD_SIZE); + +pub type FixLenLogicalOutputSum = LogicalOutputAdd; + +#[derive(Debug, Clone)] +pub struct ComponentTypeSum { + _phantom: std::marker::PhantomData, +} + +impl ComponentType for ComponentTypeSum { + type InputValue = FixLenLogicalInputSum; + type InputWitness = FixLenLogicalInputSum>; + type OutputValue = FixLenLogicalOutputSum; + type OutputWitness = FixLenLogicalOutputSum>; + type LogicalInput = SumLogicalInput; + + fn get_type_id() -> ComponentTypeId { + "axiom-eth:ComponentTypeSum".to_string() + } + + fn logical_result_to_virtual_rows_impl( + ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + let input = Self::logical_input_to_virtual_rows_impl(&ins.input); + let mut to_sum = ins.input.to_sum.clone(); + if to_sum.is_empty() { + to_sum.push(0); + } + let mut prefix_sum = Vec::with_capacity(to_sum.len()); + let mut curr = 0u128; + for x in to_sum { + curr += x as u128; + prefix_sum.push(curr); + } + let output = prefix_sum + .into_iter() + .map(|ps| Self::OutputValue { c: F::from_u128(ps) }) + .collect_vec(); + input.into_iter().zip_eq(output).collect_vec() + } + fn logical_input_to_virtual_rows_impl(li: &Self::LogicalInput) -> Vec { + let len = li.to_sum.len(); + if len == 0 { + vec![FixLenLogicalInputSum { is_final: F::ONE, to_add: F::ZERO }] + } else { + li.to_sum + .iter() + .enumerate() + .map(|(idx, x)| FixLenLogicalInputSum { + is_final: if idx + 1 == len { F::ONE } else { F::ZERO }, + to_add: F::from(*x), + }) + .collect_vec() + } + } + + fn rlc_virtual_rows( + (gate_ctx, _rlc_ctx): (&mut Context, &mut Context), + range_chip: &RangeChip, + rlc_chip: &RlcChip, + inputs: &[(Self::InputWitness, Self::OutputWitness)], + ) -> Vec> { + // Overview: + // 1. if is_final == 0, rlc = 0. + // 2. if is_final == 1, rlc = rlc([input,result]) + // we don't need length information in RLC because zeros in the end will not affect the result. + let gate = &range_chip.gate; + + let gamma = rlc_chip.rlc_pow_fixed(gate_ctx, gate, 1); + let zero = gate_ctx.load_zero(); + + let mut ret = Vec::with_capacity(inputs.len()); + let mut curr_rlc = zero; + for (input, output) in inputs { + curr_rlc = gate.mul_add(gate_ctx, curr_rlc, gamma, input.to_add); + // rlc if is_final == 1 + let row_rlc = gate.mul_add(gate_ctx, curr_rlc, gamma, output.c); + let to_push = gate.select(gate_ctx, row_rlc, zero, input.is_final); + ret.push(to_push); + curr_rlc = gate.select(gate_ctx, zero, curr_rlc, input.is_final); + } + ret + } +} diff --git a/axiom-eth/src/utils/component/types.rs b/axiom-eth/src/utils/component/types.rs new file mode 100644 index 00000000..8c02e6c7 --- /dev/null +++ b/axiom-eth/src/utils/component/types.rs @@ -0,0 +1,266 @@ +use std::{hash::Hash, marker::PhantomData}; + +use crate::Field; +use halo2_base::{AssignedValue, Context}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use super::param::{POSEIDON_RATE, POSEIDON_T}; +use super::{ComponentType, ComponentTypeId, LogicalInputValue, LogicalResult}; + +pub type PoseidonHasher = + halo2_base::poseidon::hasher::PoseidonHasher; + +/// Flatten represents a flatten fixed-len logical input/output/public instances. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Flatten { + pub fields: Vec, + pub field_size: &'static [usize], +} + +impl From>> for Flatten { + fn from(f: Flatten>) -> Self { + Flatten:: { + fields: f.fields.into_iter().map(|v| *v.value()).collect_vec(), + field_size: f.field_size, + } + } +} + +impl Flatten { + /// Assign Flatten. + pub fn assign(&self, ctx: &mut Context) -> Flatten> { + Flatten::> { + fields: ctx.assign_witnesses(self.fields.clone()), + field_size: self.field_size, + } + } +} + +impl From> for Vec { + fn from(val: Flatten) -> Self { + val.fields + } +} + +/// A logical input/output should be able to convert to a flatten logical input/ouptut. +pub trait FixLenLogical: + TryFrom, Error = anyhow::Error> + Into> + Clone +{ + /// Get field size of this logical. + fn get_field_size() -> &'static [usize]; + /// Get number of fields of this logical. + fn get_num_fields() -> usize { + Self::get_field_size().len() + } + /// From raw vec to logical. + fn try_from_raw(fields: Vec) -> anyhow::Result { + // TODO: we should auto generate this as Into> + let flatten = Flatten:: { fields, field_size: Self::get_field_size() }; + Self::try_from(flatten) + } + /// Into raw vec. + fn into_raw(self) -> Vec { + // TODO: we should auto generate this as Into> + self.into().fields + } +} + +#[derive(Clone, Debug)] +pub struct ComponentPublicInstances { + pub output_commit: T, + pub promise_result_commit: T, + pub other: Vec, +} + +type V = Vec; +impl From> for V { + fn from(val: ComponentPublicInstances) -> Self { + [vec![val.output_commit, val.promise_result_commit], val.other].concat() + } +} + +impl TryFrom> for ComponentPublicInstances { + type Error = anyhow::Error; + fn try_from(val: V) -> anyhow::Result { + if val.len() < 2 { + return Err(anyhow::anyhow!("invalid length")); + } + Ok(Self { output_commit: val[0], promise_result_commit: val[1], other: val[2..].to_vec() }) + } +} + +impl From>> for ComponentPublicInstances { + fn from(f: ComponentPublicInstances>) -> Self { + Self { + output_commit: *f.output_commit.value(), + promise_result_commit: *f.promise_result_commit.value(), + other: f.other.into_iter().map(|v| *v.value()).collect_vec(), + } + } +} + +const FIELD_SIZE_EMPTY: [usize; 0] = []; +/// Type for empty public instance +#[derive(Default, Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct LogicalEmpty(PhantomData); + +impl TryFrom> for LogicalEmpty { + type Error = anyhow::Error; + + fn try_from(value: Flatten) -> std::result::Result { + if value.field_size != FIELD_SIZE_EMPTY { + return Err(anyhow::anyhow!("invalid field size")); + } + if value.field_size.len() != value.fields.len() { + return Err(anyhow::anyhow!("field length doesn't match")); + } + Ok(LogicalEmpty::(PhantomData::)) + } +} +// This should be done by marco. +impl From> for Flatten { + fn from(_val: LogicalEmpty) -> Self { + Flatten:: { fields: vec![], field_size: &FIELD_SIZE_EMPTY } + } +} +// This should be done by marco. +impl FixLenLogical for LogicalEmpty { + fn get_field_size() -> &'static [usize] { + &FIELD_SIZE_EMPTY + } +} + +impl From> for Vec> { + fn from(val: LogicalEmpty) -> Self { + vec![val] + } +} + +impl LogicalInputValue for LogicalEmpty { + fn get_capacity(&self) -> usize { + 1 + } +} + +/// Empty component type. +#[derive(Debug, Clone)] +pub struct EmptyComponentType(PhantomData); +impl ComponentType for EmptyComponentType { + type InputValue = LogicalEmpty; + type InputWitness = LogicalEmpty>; + type OutputValue = LogicalEmpty; + type OutputWitness = LogicalEmpty>; + type LogicalInput = LogicalEmpty; + + fn get_type_id() -> ComponentTypeId { + "axiom-eth:EmptyComponentType".to_string() + } + + fn logical_result_to_virtual_rows_impl( + _ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + unreachable!() + } + + fn logical_input_to_virtual_rows_impl(_li: &Self::LogicalInput) -> Vec { + unreachable!() + } +} + +// ================== Macro for conversion to/from Flatten ================== +#[macro_export] +macro_rules! impl_flatten_conversion { + ($struct_name:ident, $bits_per_fe:ident) => { + impl TryFrom<$crate::utils::component::types::Flatten> for $struct_name { + type Error = anyhow::Error; + + fn try_from( + value: $crate::utils::component::types::Flatten, + ) -> anyhow::Result { + if &value.field_size != &$bits_per_fe { + anyhow::bail!("invalid field size"); + } + if value.field_size.len() != value.fields.len() { + anyhow::bail!("field length doesn't match"); + } + let res = value.fields.try_into()?; + Ok(res) + } + } + + impl From<$struct_name> for $crate::utils::component::types::Flatten { + fn from(value: $struct_name) -> Self { + $crate::utils::component::types::Flatten:: { + fields: value.flatten().to_vec(), + field_size: &$bits_per_fe, + } + } + } + + impl $crate::utils::component::types::FixLenLogical for $struct_name { + fn get_field_size() -> &'static [usize] { + &$bits_per_fe + } + } + }; +} + +#[macro_export] +macro_rules! impl_logical_input { + ($struct_name:ident, $capacity:expr) => { + impl> $crate::utils::component::LogicalInputValue + for $struct_name + { + fn get_capacity(&self) -> usize { + $capacity + } + } + }; +} + +#[macro_export] +macro_rules! impl_fix_len_call_witness { + ($call_name:ident, $fix_len_logical_name:ident, $component_type_name:ident) => { + #[derive(Clone, Copy, Debug)] + pub struct $call_name(pub $fix_len_logical_name>); + impl $crate::utils::component::PromiseCallWitness for $call_name { + fn get_component_type_id(&self) -> $crate::utils::component::ComponentTypeId { + $component_type_name::::get_type_id() + } + fn get_capacity(&self) -> usize { + 1 + } + fn to_rlc( + &self, + (_, rlc_ctx): (&mut $crate::halo2_base::Context, &mut $crate::halo2_base::Context), + _range_chip: &$crate::halo2_base::gates::RangeChip, + rlc_chip: &$crate::rlc::chip::RlcChip, + ) -> AssignedValue { + $crate::utils::component::promise_loader::flatten_witness_to_rlc( + rlc_ctx, + &rlc_chip, + &self.0.clone().into(), + ) + } + fn to_typeless_logical_input( + &self, + ) -> $crate::utils::component::TypelessLogicalInput { + let f_a: $crate::utils::component::types::Flatten> = + self.0.clone().into(); + let f_v: $crate::utils::component::types::Flatten = f_a.into(); + let l_v: <$component_type_name as ComponentType>::LogicalInput = + f_v.try_into().unwrap(); + $crate::utils::component::utils::into_key(l_v) + } + fn get_mock_output(&self) -> $crate::utils::component::types::Flatten { + let output_val: <$component_type_name as $crate::utils::component::ComponentType>::OutputValue = + Default::default(); + output_val.into() + } + fn as_any(&self) -> &dyn std::any::Any { + self + } + } + }; +} diff --git a/axiom-eth/src/utils/component/utils.rs b/axiom-eth/src/utils/component/utils.rs new file mode 100644 index 00000000..69187a47 --- /dev/null +++ b/axiom-eth/src/utils/component/utils.rs @@ -0,0 +1,168 @@ +use super::{param::*, types::*}; +use super::{ComponentType, FlattenVirtualRow}; +use crate::Field; +use halo2_base::gates::GateInstructions; +use halo2_base::gates::RangeChip; +use halo2_base::poseidon::hasher::spec::OptimizedPoseidonSpec; +use halo2_base::AssignedValue; +use halo2_base::Context; +use itertools::Itertools; +use serde::de::DeserializeOwned; +use serde::Serialize; +use snark_verifier::{loader::native::NativeLoader, util::hash::Poseidon}; + +/// Do not recreate this unless you need to: it recomputes the OptimizedPoseidonSpec each time. +/// +/// Unfortunately we can't use lazy_static due to the generic type `F`. +pub fn native_poseidon_hasher() -> Poseidon { + Poseidon::::new::< + POSEIDON_R_F, + POSEIDON_R_P, + POSEIDON_SECURE_MDS, + >(&NativeLoader) +} + +/// Do not recreate this unless you need to: it is computationally expensive. +/// +/// Unfortunately we can't use lazy_static due to the generic type `F`. +pub fn optimized_poseidon_spec() -> OptimizedPoseidonSpec { + OptimizedPoseidonSpec::::new::< + POSEIDON_R_F, + POSEIDON_R_P, + POSEIDON_SECURE_MDS, + >() +} + +pub fn compute_poseidon(payload: &[F]) -> F { + let mut native_poseidon_sponge = native_poseidon_hasher(); + native_poseidon_sponge.update(payload); + native_poseidon_sponge.squeeze() +} + +/// Return values of merkle tree nodes. Top to bottom, left to right. +pub fn compute_poseidon_merkle_tree( + ctx: &mut Context, + gate: &impl GateInstructions, + initialized_hasher: &PoseidonHasher, + leaves: Vec>, +) -> Vec> { + let len = leaves.len(); + // Also implict len > 0 + assert!(len.is_power_of_two()); + if len == 1 { + return leaves; + } + let next_level = + leaves.chunks(2).map(|c| initialized_hasher.hash_fix_len_array(ctx, gate, c)).collect_vec(); + let mut ret: Vec> = + compute_poseidon_merkle_tree(ctx, gate, initialized_hasher, next_level); + ret.extend(leaves); + ret +} + +pub fn compress_flatten_pair( + ctx: &mut Context, + range_chip: &RangeChip, + input: &Flatten>, + output: &Flatten>, +) -> Vec> { + let mut result = vec![]; + let mut used_bits = 0; + let const_zero = ctx.load_zero(); + let mut witness_current = const_zero; + for (a, bits) in input + .fields + .iter() + .chain(output.fields.iter()) + .zip_eq(input.field_size.iter().chain(output.field_size)) + { + let bits = *bits; + // If bits > capacity, this is a hacky way to speicify this field taking a whole witness. + if used_bits + bits <= (F::CAPACITY as usize) { + let const_mul = ctx.load_constant(range_chip.gate.pow_of_two[used_bits]); + witness_current = range_chip.gate.mul_add(ctx, const_mul, *a, witness_current); + if used_bits + bits == (F::CAPACITY as usize) { + result.push(witness_current); + used_bits = 0; + witness_current = const_zero; + } else { + used_bits += bits; + } + } else { + // TODO: maybe decompose a here to fully utilize capacity. + result.push(witness_current); + used_bits = bits; + witness_current = *a; + } + } + if used_bits > 0 { + result.push(witness_current); + } + result +} + +/// Load logical value as witness using Flatten as intermediate. V and W should come from +/// the same struct. +pub fn load_logical_value, W: FixLenLogical>>( + ctx: &mut Context, + v: &V, +) -> W { + let flatten_value: Flatten = v.clone().into(); + let flatten_witness = flatten_value.assign(ctx); + W::try_from(flatten_witness).unwrap() +} + +/// Get logical value from witness using Flatten as intermediate. V and W should come from +/// the same struct. +pub fn get_logical_value>, V: FixLenLogical>( + w: &W, +) -> V { + let flatten_witness: Flatten> = w.clone().into(); + let flatten_value: Flatten = flatten_witness.into(); + V::try_from(flatten_value).unwrap() +} + +pub fn create_hasher() -> PoseidonHasher { + // Construct in-circuit Poseidon hasher. + let spec = OptimizedPoseidonSpec::new::(); + PoseidonHasher::new(spec) +} + +pub fn compute_commitment>( + ctx: &mut Context, + gate: &impl GateInstructions, + initialized_hasher: &PoseidonHasher, + io_pairs: Vec<(T::InputWitness, T::OutputWitness)>, +) -> AssignedValue { + let flatten_io_pairs = io_pairs.into_iter().map(|(i, o)| (i.into(), o.into())).collect_vec(); + let commit = compute_commitment_with_flatten(ctx, gate, initialized_hasher, &flatten_io_pairs); + log::debug!("component_type_id: {} commit: {:?}", T::get_type_id(), commit.value()); + commit +} + +#[allow(clippy::type_complexity)] +pub fn compute_commitment_with_flatten( + ctx: &mut Context, + gate: &impl GateInstructions, + initialized_hasher: &PoseidonHasher, + io_pairs: &[FlattenVirtualRow>], +) -> AssignedValue { + if io_pairs.is_empty() { + return ctx.load_zero(); + } + let to_commit: Vec> = io_pairs + .iter() + .flat_map(|(i, o)| [i.fields.clone(), o.fields.clone()].concat()) + .collect_vec(); + initialized_hasher.hash_fix_len_array(ctx, gate, &to_commit) +} + +/// Convert LogicalInputValue into key which can be used to look up promise results. +pub fn into_key(key: impl Serialize) -> Vec { + bincode::serialize(&key).unwrap() +} + +/// Convert key back into LogicalInputValue. +pub fn try_from_key(key: &[u8]) -> anyhow::Result { + bincode::deserialize(key).map_err(anyhow::Error::from) +} diff --git a/axiom-eth/src/utils/eth_circuit.rs b/axiom-eth/src/utils/eth_circuit.rs new file mode 100644 index 00000000..5ce1c66a --- /dev/null +++ b/axiom-eth/src/utils/eth_circuit.rs @@ -0,0 +1,471 @@ +use std::{ + cell::RefCell, + collections::HashMap, + fs::File, + ops::DerefMut, + path::Path, + sync::{Arc, Mutex}, +}; + +use crate::Field; +use halo2_base::{ + gates::circuit::CircuitBuilderStage, + halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner}, + plonk::{self, Circuit, ConstraintSystem, SecondPhase}, + }, + virtual_region::{lookups::basic::BasicDynLookupConfig, manager::VirtualRegionManager}, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::{ + keccak::{ + types::{ComponentTypeKeccak, KeccakLogicalInput, OutputKeccakShard}, + KeccakChip, + }, + mpt::MPTChip, + rlc::{ + circuit::{builder::RlcCircuitBuilder, RlcCircuitParams, RlcConfig}, + virtual_region::RlcThreadBreakPoints, + }, + rlp::RlpChip, + utils::{ + build_utils::pinning::{CircuitPinningInstructions, RlcCircuitPinning}, + component::{ + circuit::{ComponentBuilder, PromiseBuilder}, + promise_collector::{PromiseCaller, PromiseCollector, SharedPromiseCollector}, + promise_loader::single::{PromiseLoader, PromiseLoaderConfig, PromiseLoaderParams}, + }, + DEFAULT_RLC_CACHE_BITS, + }, +}; + +use super::{ + build_utils::pinning::Halo2CircuitPinning, + component::{ + utils::try_from_key, ComponentPromiseResultsInMerkle, ComponentType, LogicalInputValue, + }, +}; + +/// Default number of lookup bits for range check is set to 8 for range checking bytes. +pub(crate) const ETH_LOOKUP_BITS: usize = 8; + +/// Configuration parameters for [EthConfig] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EthCircuitParams { + pub rlc: RlcCircuitParams, + /// Keccak promise loader + pub keccak: PromiseLoaderParams, +} + +impl Default for EthCircuitParams { + fn default() -> Self { + let mut rlc = RlcCircuitParams::default(); + rlc.base.num_instance_columns = 1; + rlc.base.lookup_bits = Some(ETH_LOOKUP_BITS); + let keccak = Default::default(); + Self { rlc, keccak } + } +} + +impl EthCircuitParams { + pub fn new(rlc: RlcCircuitParams, keccak: PromiseLoaderParams) -> Self { + Self { rlc, keccak } + } + pub fn from_path>(path: P) -> Self { + serde_json::from_reader(File::open(&path).unwrap()).unwrap() + } + pub fn k(&self) -> usize { + self.rlc.base.k + } + pub fn set_k(&mut self, k: usize) { + self.rlc.base.k = k; + } +} + +/// Halo2 Config shared by all circuits that prove data about the Ethereum execution layer (EL). +/// Includes [BaseConfig] and [PureRlcConfig] inside [RlcConfig] that use Base + RLC + Keccak +#[derive(Clone)] +pub struct EthConfig { + pub rlc_config: RlcConfig, + pub keccak: PromiseLoaderConfig, +} + +impl EthConfig { + pub fn configure(meta: &mut ConstraintSystem, params: impl Into) -> Self { + let params: EthCircuitParams = params.into(); + let k = params.k(); + let mut rlc_config = RlcConfig::configure(meta, params.rlc); + // TODO: allow 0 columns here for more flexility + let keccak = PromiseLoaderConfig { + dyn_lookup_config: BasicDynLookupConfig::new(meta, || SecondPhase, 1), + }; + log::info!("Poisoned rows after EthConfig::configure {}", meta.minimum_rows()); + // Warning: this needs to be updated if you create more advice columns after this `EthConfig` is created + let usable_rows = (1usize << k) - meta.minimum_rows(); + rlc_config.set_usable_rows(usable_rows); + Self { rlc_config, keccak } + } +} + +/// Simple trait describing the FirstPhase and SecondPhase witness generation of a circuit +/// that only uses [EthConfig]. +/// +/// * In FirstPhase, [MPTChip] is provided with `None` for RlcChip. +/// * In SecondPhase, [MPTChip] is provided with RlcChip that has challenge value loaded. +pub trait EthCircuitInstructions: Clone { + type FirstPhasePayload; + + fn virtual_assign_phase0( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + ) -> Self::FirstPhasePayload; + + /// SecondPhase is optional + #[allow(unused_variables)] + fn virtual_assign_phase1( + &self, + builder: &mut RlcCircuitBuilder, + mpt: &MPTChip, + payload: Self::FirstPhasePayload, + ) { + } +} + +/// This struct is used for the concrete implementation of [Circuit] trait from [EthCircuitInstructions]. +/// This provides a quick way to create a circuit that only uses [EthConfig]. +// This is basically a simplified version of `ComponentCircuitImpl` with `EthCircuitInstructions` + `PromiseLoader` for Keccak. +pub struct EthCircuitImpl> { + pub logic_inputs: I, + pub keccak_chip: KeccakChip, + pub rlc_builder: RefCell>, + pub promise_collector: SharedPromiseCollector, + pub promise_builder: RefCell>>, + /// The FirstPhasePayload is set after FirstPhase witness generation. + /// This is used both to pass payload between phases and also to detect if `virtual_assign_phase0` + /// was already run outside of `synthesize` (e.g., to determine public instances) + payload: RefCell>, +} + +impl EthCircuitImpl +where + F: Field, + I: EthCircuitInstructions, +{ + pub fn new( + logic_inputs: I, + prompt_rlc_params: RlcCircuitParams, + promise_params: PromiseLoaderParams, + ) -> Self { + // Mock is general, can be used for anything + Self::new_impl(CircuitBuilderStage::Mock, logic_inputs, prompt_rlc_params, promise_params) + } + pub fn new_impl( + stage: CircuitBuilderStage, + logic_inputs: I, + prompt_rlc_params: RlcCircuitParams, + promise_params: PromiseLoaderParams, + ) -> Self { + let rlc_builder = RlcCircuitBuilder::from_stage(stage, DEFAULT_RLC_CACHE_BITS) + .use_params(prompt_rlc_params); + let promise_loader = PromiseLoader::>::new(promise_params); + let promise_collector = Arc::new(Mutex::new(PromiseCollector::new(vec![ + ComponentTypeKeccak::::get_type_id(), + ]))); + let range = rlc_builder.range_chip(); + let keccak = KeccakChip::new_with_promise_collector( + range, + PromiseCaller::new(promise_collector.clone()), + ); + Self { + logic_inputs, + keccak_chip: keccak, + rlc_builder: RefCell::new(rlc_builder), + promise_collector, + promise_builder: RefCell::new(promise_loader), + payload: RefCell::new(None), + } + } + pub fn use_break_points(self, break_points: RlcThreadBreakPoints) -> Self { + self.rlc_builder.borrow_mut().set_break_points(break_points); + self + } + pub fn prover( + logic_inputs: I, + prompt_rlc_pinning: RlcCircuitPinning, + promise_params: PromiseLoaderParams, + ) -> Self { + Self::new_impl( + CircuitBuilderStage::Prover, + logic_inputs, + prompt_rlc_pinning.params, + promise_params, + ) + .use_break_points(prompt_rlc_pinning.break_points) + } + pub fn clear_witnesses(&self) { + self.rlc_builder.borrow_mut().clear(); + self.promise_collector.lock().unwrap().clear_witnesses(); + self.payload.borrow_mut().take(); + self.promise_builder.borrow_mut().clear_witnesses(); + } + + /// FirstPhase witness generation with error handling. + pub fn virtual_assign_phase0(&self) -> Result<(), plonk::Error> { + if self.payload.borrow().is_some() { + return Ok(()); + } + let mut borrowed_rlc_builder = self.rlc_builder.borrow_mut(); + let rlc_builder = borrowed_rlc_builder.deref_mut(); + let mut promise_builder = self.promise_builder.borrow_mut(); + + log::info!("EthCircuit: FirstPhase witness generation start"); + { + let mut borrowed_promise_collector = self.promise_collector.lock().unwrap(); + let promise_collector = borrowed_promise_collector.deref_mut(); + promise_builder.virtual_assign_phase0(rlc_builder, promise_collector); + } + + let rlp = RlpChip::new(self.keccak_chip.range(), None); + let mpt = MPTChip::new(rlp, &self.keccak_chip); + let payload = I::virtual_assign_phase0(&self.logic_inputs, rlc_builder, &mpt); + self.payload.borrow_mut().replace(payload); + // Add keccak promise as the last public instance in column 0: + let promise_commit = self + .promise_collector + .lock() + .unwrap() + .get_commit_by_component_type_id(&ComponentTypeKeccak::::get_type_id()) + .ok_or(plonk::Error::InvalidInstances)?; + if rlc_builder.base.assigned_instances.is_empty() { + return Err(plonk::Error::InvalidInstances); + } + rlc_builder.base.assigned_instances[0].push(promise_commit); + log::info!("EthCircuit: FirstPhase witness generation complete"); + Ok(()) + } + + pub fn virtual_assign_phase1(&self) { + let payload = + self.payload.borrow_mut().take().expect("FirstPhase witness generation was not run"); + log::info!("EthCircuit: SecondPhase witness generation start"); + let mut rlc_builder = self.rlc_builder.borrow_mut(); + let range_chip = self.keccak_chip.range(); + let rlc_chip = rlc_builder.rlc_chip(&range_chip.gate); + let rlp = RlpChip::new(range_chip, Some(&rlc_chip)); + let mpt = MPTChip::new(rlp, &self.keccak_chip); + { + let mut promise_collector = self.promise_collector.lock().unwrap(); + self.promise_builder + .borrow_mut() + .virtual_assign_phase1(&mut rlc_builder, promise_collector.deref_mut()); + } + I::virtual_assign_phase1(&self.logic_inputs, &mut rlc_builder, &mpt, payload); + log::info!("EthCircuit: SecondPhase witness generation complete"); + } + + pub fn fulfill_keccak_promise_results( + &self, + keccak_promise_results: ComponentPromiseResultsInMerkle, + ) -> Result<(), anyhow::Error> { + let mut borrowed_promise_collector = self.promise_collector.lock().unwrap(); + let promise_collector = borrowed_promise_collector.deref_mut(); + promise_collector.fulfill(&HashMap::from_iter([( + ComponentTypeKeccak::::get_type_id(), + keccak_promise_results, + )])); + self.promise_builder.borrow_mut().fulfill_promise_results(promise_collector); + Ok(()) + } + + /// Calculate params. This should be called only after all promise results are fulfilled. + pub fn calculate_params(&mut self) -> EthCircuitParams { + self.virtual_assign_phase0().expect("virtual assign phase0 failed"); + self.virtual_assign_phase1(); + + let rlc_params = self.rlc_builder.borrow_mut().calculate_params(Some(20)); + let promise_params = self.promise_builder.borrow_mut().calculate_params(); + + self.clear_witnesses(); + + EthCircuitParams { rlc: rlc_params, keccak: promise_params } + } + + pub fn break_points(&self) -> RlcThreadBreakPoints { + self.rlc_builder.borrow().break_points() + } + pub fn set_break_points(&self, break_points: RlcThreadBreakPoints) { + self.rlc_builder.borrow_mut().set_break_points(break_points); + } + + /// For testing only. A helper function to fulfill keccak promises for this circuit. + pub fn mock_fulfill_keccak_promises(&self, capacity: Option) { + let rlp = RlpChip::new(self.keccak_chip.range(), None); + let mpt = MPTChip::new(rlp, &self.keccak_chip); + I::virtual_assign_phase0(&self.logic_inputs, &mut self.rlc_builder.borrow_mut(), &mpt); + let calls = self.promise_collector.lock().unwrap().get_deduped_calls(); + let keccak_calls = &calls[&ComponentTypeKeccak::::get_type_id()]; + let mut used_capacity = 0; + let responses = keccak_calls + .iter() + .map(|call| { + let li = try_from_key::(&call.logical_input).unwrap(); + used_capacity += >::get_capacity(&li); + (li.bytes.clone().into(), None) + }) + .collect_vec(); + + let capacity = if let Some(capacity) = capacity { capacity } else { used_capacity }; + let output_shard = OutputKeccakShard { responses, capacity }; + self.fulfill_keccak_promise_results(ComponentPromiseResultsInMerkle::from_single_shard( + output_shard.into_logical_results(), + )) + .unwrap(); + self.clear_witnesses(); + } + + pub fn instances(&self) -> Vec> { + self.virtual_assign_phase0().unwrap(); + let builder = self.rlc_builder.borrow(); + builder + .base + .assigned_instances + .iter() + .map(|instance| instance.iter().map(|x| *x.value()).collect()) + .collect() + } +} + +impl Circuit for EthCircuitImpl +where + F: Field, + I: EthCircuitInstructions, +{ + type FloorPlanner = SimpleFloorPlanner; + type Config = EthConfig; + type Params = EthCircuitParams; + + fn params(&self) -> Self::Params { + let rlc = self.rlc_builder.borrow().params(); + let keccak = self.promise_builder.borrow().get_params(); + EthCircuitParams { rlc, keccak } + } + + fn without_witnesses(&self) -> Self { + unimplemented!() + } + fn configure_with_params(meta: &mut ConstraintSystem, params: Self::Params) -> Self::Config { + EthConfig::configure(meta, params) + } + fn configure(_: &mut ConstraintSystem) -> Self::Config { + unreachable!() + } + + // Mostly copied from ComponentCircuitImpl + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), plonk::Error> { + self.promise_collector.lock().unwrap().set_promise_results_ready(true); + config.rlc_config.base.initialize(&mut layouter); + self.virtual_assign_phase0()?; + { + let mut promise_builder = self.promise_builder.borrow_mut(); + let rlc_builder = self.rlc_builder.borrow(); + + let mut phase0_layouter = layouter.namespace(|| "raw synthesize phase0"); + promise_builder.raw_synthesize_phase0(&config.keccak, &mut phase0_layouter); + rlc_builder.raw_synthesize_phase0(&config.rlc_config, phase0_layouter); + } + #[cfg(feature = "halo2-axiom")] + { + layouter.next_phase(); + } + self.rlc_builder + .borrow_mut() + .load_challenge(&config.rlc_config, layouter.namespace(|| "load challenges")); + + self.virtual_assign_phase1(); + + { + let rlc_builder = self.rlc_builder.borrow(); + let phase1_layouter = layouter.namespace(|| "RlcCircuitBuilder raw synthesize phase1"); + rlc_builder.raw_synthesize_phase1(&config.rlc_config, phase1_layouter, false); + + let mut promise_builder = self.promise_builder.borrow_mut(); + promise_builder.raw_synthesize_phase1(&config.keccak, &mut layouter); + } + + let rlc_builder = self.rlc_builder.borrow(); + if !rlc_builder.witness_gen_only() { + layouter.assign_region( + || "copy constraints", + |mut region| { + let constant_cols = config.rlc_config.base.constants(); + rlc_builder.copy_manager().assign_raw(constant_cols, &mut region); + Ok(()) + }, + )?; + } + drop(rlc_builder); + + // clear in case synthesize is called multiple times + self.clear_witnesses(); + + Ok(()) + } +} + +impl CircuitPinningInstructions for EthCircuitImpl +where + F: Field, + I: EthCircuitInstructions, +{ + type Pinning = RlcCircuitPinning; + fn pinning(&self) -> Self::Pinning { + let break_points = self.break_points(); + let params = self.rlc_builder.borrow().params(); + RlcCircuitPinning::new(params, break_points) + } +} + +#[cfg(feature = "aggregation")] +mod aggregation { + use crate::Field; + use snark_verifier_sdk::CircuitExt; + + use crate::utils::build_utils::aggregation::CircuitMetadata; + + use super::{EthCircuitImpl, EthCircuitInstructions}; + + impl CircuitExt for EthCircuitImpl + where + F: Field, + I: EthCircuitInstructions + CircuitMetadata, + { + fn accumulator_indices() -> Option> { + I::accumulator_indices() + } + + fn instances(&self) -> Vec> { + self.instances() + } + + fn num_instance(&self) -> Vec { + self.logic_inputs.num_instance() + } + } +} + +// ==== convenience functions for testing & benchmarking ==== + +pub fn create_circuit>( + stage: CircuitBuilderStage, + circuit_params: RlcCircuitParams, + logic_inputs: I, +) -> EthCircuitImpl { + EthCircuitImpl::new_impl(stage, logic_inputs, circuit_params, Default::default()) +} diff --git a/axiom-eth/src/utils/hilo.rs b/axiom-eth/src/utils/hilo.rs new file mode 100644 index 00000000..ec5b74ff --- /dev/null +++ b/axiom-eth/src/utils/hilo.rs @@ -0,0 +1,94 @@ +use std::io::{Error, ErrorKind, Result}; + +use ethers_core::types::H256; +use halo2_base::{AssignedValue, Context}; +use serde::{Deserialize, Serialize}; +use zkevm_hashes::util::{eth_types::Field, word::Word}; + +use crate::impl_flatten_conversion; + +use super::encode_h256_to_hilo; + +/// Stored as [lo, hi], just like Word2 +#[derive(Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize, Hash)] +pub struct HiLo([T; 2]); + +impl HiLo { + /// Create a new [HiLo] from a `[lo, hi]` array. + pub fn from_lo_hi([lo, hi]: [T; 2]) -> Self { + Self([lo, hi]) + } + /// Create a new [HiLo] from a `[hi, lo]` array. + pub fn from_hi_lo([hi, lo]: [T; 2]) -> Self { + Self([lo, hi]) + } + pub fn hi(&self) -> T + where + T: Copy, + { + self.0[1] + } + pub fn lo(&self) -> T + where + T: Copy, + { + self.0[0] + } + pub fn hi_lo(&self) -> [T; 2] + where + T: Copy, + { + [self.hi(), self.lo()] + } + pub fn flatten(&self) -> [T; 2] + where + T: Copy, + { + self.hi_lo() + } +} + +impl HiLo { + pub fn assign(&self, ctx: &mut Context) -> HiLo> { + HiLo(self.0.map(|x| ctx.load_witness(x))) + } +} + +impl From> for HiLo { + fn from(word: Word) -> Self { + Self::from_hi_lo([word.hi(), word.lo()]) + } +} + +impl From> for Word { + fn from(lohi: HiLo) -> Self { + Word::new(lohi.0) + } +} + +impl From for HiLo { + fn from(value: H256) -> Self { + encode_h256_to_hilo(&value) + } +} + +impl TryFrom> for HiLo { + type Error = Error; + + fn try_from(value: Vec) -> Result { + let [hi, lo] = value + .try_into() + .map_err(|_| Error::new(ErrorKind::InvalidInput, "invalid array length"))?; + Ok(Self::from_hi_lo([hi, lo])) + } +} + +/// We will only flatten HiLo to uint128 words. +pub const BITS_PER_FE_HILO: [usize; 2] = [128, 128]; +impl_flatten_conversion!(HiLo, BITS_PER_FE_HILO); + +impl std::fmt::Debug for HiLo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("HiLo").field(&self.0[1]).field(&self.0[0]).finish() + } +} diff --git a/axiom-eth/src/utils/keccak/decorator.rs b/axiom-eth/src/utils/keccak/decorator.rs new file mode 100644 index 00000000..7831c16b --- /dev/null +++ b/axiom-eth/src/utils/keccak/decorator.rs @@ -0,0 +1,598 @@ +//! A `Circuit` implementation for `EthCircuitInstructions` that consists of a circuit with both `RlcCircuitBuilder` and `KeccakComponentShardCircuit` as sub-circuits. +//! This is a complete circuit that can be used when keccak computations are necessary. +//! This circuit is **not** part of the Component Framework, so it does not have any additional dependencies or verification assumptions. + +// @dev We still use the `ComponentType` and `ComponentLoader` trait to share as much code as possible with the Keccak promise loader implementation. + +use std::{ + cell::RefCell, + iter::{self, zip}, + mem, + ops::DerefMut, + sync::{Arc, Mutex}, +}; + +use ethers_core::{types::H256, utils::keccak256}; +use halo2_base::{ + gates::{ + circuit::CircuitBuilderStage, + flex_gate::threads::{parallelize_core, SinglePhaseCoreManager}, + GateInstructions, RangeChip, + }, + halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner}, + plonk::{self, Circuit, ConstraintSystem}, + }, + safe_types::SafeTypeChip, + AssignedValue, + QuantumCell::Constant, +}; +use itertools::{zip_eq, Itertools}; +use serde::{Deserialize, Serialize}; +use zkevm_hashes::keccak::{ + component::{ + circuit::shard::{ + pack_inputs_from_keccak_fs, transmute_keccak_assigned_to_virtual, LoadedKeccakF, + }, + encode::format_input, + }, + vanilla::{ + keccak_packed_multi::get_num_keccak_f, witness::multi_keccak, KeccakAssignedRow, + KeccakCircuitConfig, KeccakConfigParams, + }, +}; + +use crate::{ + keccak::{ + promise::{KeccakFixLenCall, KeccakVarLenCall}, + types::{ComponentTypeKeccak, KeccakVirtualInput, KeccakVirtualOutput}, + KeccakChip, + }, + mpt::MPTChip, + rlc::{ + circuit::{builder::RlcCircuitBuilder, RlcCircuitParams, RlcConfig}, + virtual_region::RlcThreadBreakPoints, + }, + rlp::RlpChip, + utils::{ + component::{ + promise_collector::{PromiseCaller, PromiseCallsGetter, PromiseCollector}, + ComponentType, + }, + constrain_vec_equal, encode_h256_to_hilo, enforce_conditional_equality, + eth_circuit::EthCircuitInstructions, + hilo::HiLo, + keccak::get_keccak_unusable_rows_from_capacity, + DEFAULT_RLC_CACHE_BITS, + }, + Field, +}; + +/// Configuration parameters for [RlcKeccakConfig] +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct RlcKeccakCircuitParams { + /// RLC circuit parameters + pub rlc: RlcCircuitParams, + /// The number of rows per round of keccak_f in the keccak circuit. + /// No `capacity` is needed because this circuit will do exactly the number of keccaks + /// needed by the instructions. The `keccak_rows_per_round` must be small enough so that the + /// circuit can performs all necessary keccaks, otherwise you will get a `NotEnoughRows` error. + pub keccak_rows_per_round: usize, +} + +impl RlcKeccakCircuitParams { + pub fn new(rlc: RlcCircuitParams, keccak_rows_per_round: usize) -> Self { + Self { rlc, keccak_rows_per_round } + } + pub fn k(&self) -> usize { + self.rlc.base.k + } + pub fn set_k(&mut self, k: usize) { + self.rlc.base.k = k; + } + pub fn use_k(mut self, k: usize) -> Self { + self.rlc.base.k = k; + self + } +} + +/// Halo2 Config that is a combination of [RlcConfig] and vanilla [KeccakCircuitConfig] +#[derive(Clone, Debug)] +pub struct RlcKeccakConfig { + pub rlc: RlcConfig, + pub keccak: KeccakCircuitConfig, +} + +impl RlcKeccakConfig { + pub fn configure(meta: &mut ConstraintSystem, params: RlcKeccakCircuitParams) -> Self { + let k = params.k(); + let mut rlc_config = RlcConfig::configure(meta, params.rlc); + let keccak_config = KeccakCircuitConfig::new( + meta, + KeccakConfigParams { k: k as u32, rows_per_round: params.keccak_rows_per_round }, + ); + log::info!("Poisoned rows after RlcKeccakConfig::configure {}", meta.minimum_rows()); + // Warning: minimum_rows may have changed between RlcConfig::configure and now: + let usable_rows = (1usize << k) - meta.minimum_rows(); + rlc_config.set_usable_rows(usable_rows); + Self { rlc: rlc_config, keccak: keccak_config } + } +} + +/// This struct is used for the concrete implementation of [Circuit] trait from [EthCircuitInstructions] *which has a vanilla zkEVM keccak sub-circuit embedded* in the full circuit. +/// The difference between this and [EthCircuitImpl] is that this circuit is self-contained: it does not rely on any promise calls. All keccak computations will be fully proved when +/// this circuit is verified. +pub struct RlcKeccakCircuitImpl> { + pub logic_inputs: I, + /// RLC Circuit Builder + pub rlc_builder: RefCell>, + /// This is a passthrough to `SharedPromiseCollector` that contains the + /// keccak functions. It collects the keccaks that needs to be done and handles + /// some formatting. The actual keccaks will be passed on to the vanilla zkevm keccak + /// sub-circuit to be proved. + pub keccak_chip: KeccakChip, + keccak_rows_per_round: usize, + /// The FirstPhasePayload is set after FirstPhase witness generation. + /// This is used both to pass payload between phases and also to detect if `virtual_assign_phase0_start` + /// was already run outside of `synthesize` (e.g., to determine public instances) + payload: RefCell>, + + // we keep these as RefCell to pass between phases just so the user doesn't need to keep track of them: + call_collector: RefCell>, +} + +impl RlcKeccakCircuitImpl +where + F: Field, + I: EthCircuitInstructions, +{ + pub fn new(logic_inputs: I, circuit_params: RlcKeccakCircuitParams) -> Self { + // Mock is general, can be used for anything + Self::new_impl( + CircuitBuilderStage::Mock, + logic_inputs, + circuit_params, + DEFAULT_RLC_CACHE_BITS, + ) + } + /// When `RlcChip` is automatically constructed in `virtual_assign_phase1`, it will + /// compute `gamma^{2^i}` for `i = 0..max_rlc_cache_bits`. This cache is used by + /// RlpChip. Usually one should just set this to [DEFAULT_RLC_CACHE_BITS] to cover + /// all possible use cases, since the cache computation cost is low. + /// + /// However we provide the option to set this to `0` because the Keccak decorator + /// does not actually need RlcChip. Therefore if you only use `BaseCircuitBuilder` and + /// KeccakChip and never use RlcChip, then setting this to `0` will mean that you do not + /// create any unnecessary RLC advice columns. + pub fn new_impl( + stage: CircuitBuilderStage, + logic_inputs: I, + circuit_params: RlcKeccakCircuitParams, + max_rlc_cache_bits: usize, + ) -> Self { + let rlc_builder = + RlcCircuitBuilder::from_stage(stage, max_rlc_cache_bits).use_params(circuit_params.rlc); + // We re-use this to save code and also because `KeccakChip` needs it in its constructor + let promise_collector = Arc::new(Mutex::new(PromiseCollector::new(vec![ + ComponentTypeKeccak::::get_type_id(), + ]))); + let range = rlc_builder.range_chip(); + let keccak = + KeccakChip::new_with_promise_collector(range, PromiseCaller::new(promise_collector)); + Self { + logic_inputs, + keccak_chip: keccak, + keccak_rows_per_round: circuit_params.keccak_rows_per_round, + rlc_builder: RefCell::new(rlc_builder), + payload: RefCell::new(None), + call_collector: Default::default(), + } + } + pub fn use_break_points(self, break_points: RlcThreadBreakPoints) -> Self { + self.rlc_builder.borrow_mut().set_break_points(break_points); + self + } + /// Resets entire circuit state. Does not change original inputs. + pub fn clear(&self) { + self.rlc_builder.borrow_mut().clear(); + self.keccak_chip.promise_caller().0.lock().unwrap().clear_witnesses(); + self.payload.borrow_mut().take(); + self.call_collector.borrow_mut().clear(); + } + + /// FirstPhase witness generation with error handling. + pub fn virtual_assign_phase0_start(&self) { + if self.payload.borrow().is_some() { + // We've already done phase0, perhaps outside of synthesize + return; + } + #[cfg(feature = "display")] + let start = std::time::Instant::now(); + let mut rlc_builder_ref = self.rlc_builder.borrow_mut(); + let rlc_builder = &mut rlc_builder_ref; + + let rlp = RlpChip::new(self.keccak_chip.range(), None); + let mpt = MPTChip::new(rlp, &self.keccak_chip); + // main instructions phase0 + let payload = I::virtual_assign_phase0(&self.logic_inputs, rlc_builder, &mpt); + self.payload.borrow_mut().replace(payload); + + // now the KeccakChip promise_collector has all of the keccak (input, outputs) that need to be proven & constrained. + let collector_guard = self.keccak_chip.promise_caller().0.lock().unwrap(); + let mut calls = self.call_collector.borrow_mut(); + assert!(calls.fix_len_calls.is_empty()); + assert!(calls.var_len_calls.is_empty()); + // these are the keccak inputs, outputs as virtual assigned cells + // need to do some rust downcasting from PromiseCallWitness to either KeccakFixLenCall or KeccakVarLenCall + for (input, output) in collector_guard + .get_calls_by_component_type_id(&ComponentTypeKeccak::::get_type_id()) + .unwrap() + .values() + .flatten() + { + if let Some(fix_len_call) = input.as_any().downcast_ref::>() { + calls + .fix_len_calls + .push((fix_len_call.clone(), output.clone().try_into().unwrap())); + } else if let Some(var_len_call) = input.as_any().downcast_ref::>() + { + calls + .var_len_calls + .push((var_len_call.clone(), output.clone().try_into().unwrap())); + } else { + unreachable!("KeccakChip should only use KeccakFixLenCall or KeccakVarLenCall"); + } + } + #[cfg(feature = "display")] + log::info!("RlcKeccackCircuit virtual_assign_phase0_start time: {:?}", start.elapsed()); + } + + pub fn virtual_assign_phase1(&self) { + let payload = + self.payload.borrow_mut().take().expect("FirstPhase witness generation was not run"); + let mut rlc_builder = self.rlc_builder.borrow_mut(); + let range_chip = self.keccak_chip.range(); + // Note: this uses rlc columns to load RLC cache. Set `max_rlc_cache_bits = 0` in the `new_impl` constructor to disable this. + let rlc_chip = rlc_builder.rlc_chip(&range_chip.gate); + let rlp = RlpChip::new(range_chip, Some(&rlc_chip)); + let mpt = MPTChip::new(rlp, &self.keccak_chip); + I::virtual_assign_phase1(&self.logic_inputs, &mut rlc_builder, &mpt, payload); + } + + /// Calculate params. This should be called only after all promise results are fulfilled. + pub fn calculate_params(&mut self) { + self.virtual_assign_phase0_start(); + let mut capacity = 0; + for (call, _) in self.call_collector.borrow().fix_len_calls.iter() { + capacity += get_num_keccak_f(call.bytes().len()); + } + for (call, _) in self.call_collector.borrow().var_len_calls.iter() { + capacity += get_num_keccak_f(call.bytes().max_len()); + } + // make mock loaded_keccak_fs just to simulate + let copy_manager_ref = self.rlc_builder.borrow().copy_manager().clone(); + let mut copy_manager = copy_manager_ref.lock().unwrap(); + let virtual_keccak_fs = (0..capacity) + .map(|_| { + LoadedKeccakF::new( + copy_manager.mock_external_assigned(F::ZERO), + core::array::from_fn(|_| copy_manager.mock_external_assigned(F::ZERO)), + SafeTypeChip::unsafe_to_bool(copy_manager.mock_external_assigned(F::ZERO)), + copy_manager.mock_external_assigned(F::ZERO), + copy_manager.mock_external_assigned(F::ZERO), + ) + }) + .collect_vec(); + drop(copy_manager); + let keccak_calls = mem::take(self.call_collector.borrow_mut().deref_mut()); + keccak_calls.pack_and_constrain( + virtual_keccak_fs, + self.rlc_builder.borrow_mut().base.pool(0), + self.keccak_chip.range(), + ); + self.virtual_assign_phase1(); + + let k = self.params().k(); + let (unusable_rows, rows_per_round) = get_keccak_unusable_rows_from_capacity(k, capacity); + self.rlc_builder.borrow_mut().calculate_params(Some(unusable_rows)); + log::debug!("RlcKeccakCircuit used capacity: {capacity}"); + log::debug!("RlcKeccakCircuit optimal rows_per_round : {rows_per_round}"); + self.keccak_rows_per_round = rows_per_round; + + self.clear(); + } + + pub fn break_points(&self) -> RlcThreadBreakPoints { + self.rlc_builder.borrow().break_points() + } + pub fn set_break_points(&self, break_points: RlcThreadBreakPoints) { + self.rlc_builder.borrow_mut().set_break_points(break_points); + } + + pub fn instances(&self) -> Vec> { + self.virtual_assign_phase0_start(); + let builder = self.rlc_builder.borrow(); + builder + .base + .assigned_instances + .iter() + .map(|instance| instance.iter().map(|x| *x.value()).collect()) + .collect() + } +} + +impl Circuit for RlcKeccakCircuitImpl +where + F: Field, + I: EthCircuitInstructions, +{ + type FloorPlanner = SimpleFloorPlanner; + type Config = RlcKeccakConfig; + type Params = RlcKeccakCircuitParams; + + fn params(&self) -> Self::Params { + let rlc = self.rlc_builder.borrow().params(); + let keccak_rows_per_round = self.keccak_rows_per_round; + RlcKeccakCircuitParams { rlc, keccak_rows_per_round } + } + fn without_witnesses(&self) -> Self { + unimplemented!() + } + fn configure_with_params(meta: &mut ConstraintSystem, params: Self::Params) -> Self::Config { + RlcKeccakConfig::configure(meta, params) + } + fn configure(_: &mut ConstraintSystem) -> Self::Config { + unreachable!() + } + + /// This is organized to match `EthCircuitImpl::synthesize` as closely as possible, except that PromiseLoader is replaced with an actual vanilla keccak sub-circuit. + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), plonk::Error> { + let RlcKeccakCircuitParams { rlc: rlc_circuit_params, keccak_rows_per_round } = + self.params(); + let k = rlc_circuit_params.base.k; + let keccak_circuit_params = + KeccakConfigParams { k: k as u32, rows_per_round: keccak_rows_per_round }; + + #[cfg(feature = "display")] + let start = std::time::Instant::now(); + log::info!("RlcKeccakCircuit: phase0 start"); + config.rlc.base.initialize(&mut layouter); + config.keccak.load_aux_tables(&mut layouter, k as u32)?; + + self.virtual_assign_phase0_start(); + let keccak_calls = mem::take(self.call_collector.borrow_mut().deref_mut()); + keccak_calls.assign_raw_and_constrain( + keccak_circuit_params, + &config.keccak, + &mut layouter.namespace(|| "keccak sub-circuit"), + self.rlc_builder.borrow_mut().base.pool(0), + self.keccak_chip.range(), + )?; + + // raw assign everything in RlcCircuitBuilder phase0, *including* the keccak virtual table (this is important because we will RLC the table in phase1). + self.rlc_builder.borrow().raw_synthesize_phase0( + &config.rlc, + layouter.namespace(|| "RlcCircuitBuilder raw synthesize phase0"), + ); + log::info!("RlcKeccakCircuit: phase0 end"); + #[cfg(feature = "display")] + log::info!("RlcKeccakCircuit phase0 synthesize time: {:?}", start.elapsed()); + + #[cfg(feature = "halo2-axiom")] + layouter.next_phase(); + + self.rlc_builder + .borrow_mut() + .load_challenge(&config.rlc, layouter.namespace(|| "load challenges")); + + self.virtual_assign_phase1(); + + self.rlc_builder.borrow_mut().raw_synthesize_phase1( + &config.rlc, + layouter.namespace(|| "RlcCircuitBuilder raw synthesize phase1 + copy constraints"), + true, + ); + + // clear in case synthesize is called multiple times + self.clear(); + + Ok(()) + } +} + +#[derive(Clone, Default, Debug)] +pub struct KeccakCallCollector { + pub fix_len_calls: Vec<(KeccakFixLenCall, HiLo>)>, + pub var_len_calls: Vec<(KeccakVarLenCall, HiLo>)>, +} + +impl KeccakCallCollector { + pub fn new( + fix_len_calls: Vec<(KeccakFixLenCall, HiLo>)>, + var_len_calls: Vec<(KeccakVarLenCall, HiLo>)>, + ) -> Self { + Self { fix_len_calls, var_len_calls } + } + + pub fn clear(&mut self) { + self.fix_len_calls.clear(); + self.var_len_calls.clear(); + } + + /// Format the KeccakFixLenCall's and KeccakVarLenCall's into + /// variable length bytes as inputs to zkEVM keccak so that the + /// keccak table from the keccak sub-circuit will exactly match + /// the ordering and packing of the calls. + pub fn format_keccak_inputs(&self) -> Vec> { + let fix_len_calls = &self.fix_len_calls; + let fix_len_calls = fix_len_calls.iter().map(|(call, _)| call.to_logical_input().bytes); + let var_len_calls = &self.var_len_calls; + let var_len_calls = var_len_calls.iter().flat_map(|(call, _)| { + let capacity = get_num_keccak_f(call.bytes().max_len()); + let bytes = call.to_logical_input().bytes; + // we need to pad with empty [] inputs so that we use exactly the same number of keccak_f as if we were computing keccak on a max_len input + let used_capacity = get_num_keccak_f(bytes.len()); + iter::once(bytes).chain(iter::repeat(vec![]).take(capacity - used_capacity)) + }); + iter::empty().chain(fix_len_calls).chain(var_len_calls).collect() + } + + /// Packs the loaded keccak_f rows and constrains that they exactly + /// match the packing from the KeccakFixLenCall's and KeccakVarLenCall's. + /// Needs to be done after raw synthesize of keccak sub-circuit. + /// + /// ## Assumptions + /// - The `virtual_keccak_fs` should exactly correspond to the inputs generated by `format_keccak_inputs`. + /// - `range_chip` should share same reference to `copy_manager` as `pool`. + pub fn pack_and_constrain( + self, + virtual_keccak_fs: Vec>, + pool: &mut SinglePhaseCoreManager, + range_chip: &RangeChip, + ) { + // We now pack the virtual cells into a virtual table using fewer field elements + let gate = &range_chip.gate; + let packed_inputs = pack_inputs_from_keccak_fs(pool.main(), gate, &virtual_keccak_fs); + // Return the virtual table of inputs and outputs: + let virtual_table = zip_eq(packed_inputs, virtual_keccak_fs).map(|(chunk, keccak_f)| { + let v_i = KeccakVirtualInput::new( + chunk.inputs().concat().try_into().unwrap(), + chunk.is_final().into(), + ); + let hash = HiLo::from_hi_lo([keccak_f.hash_hi(), keccak_f.hash_lo()]); + let v_o = KeccakVirtualOutput::new(hash); + (v_i, v_o) + }); + + // We also pack the `calls` in exactly the same way: + let Self { fix_len_calls, var_len_calls } = self; + // flat map fix_len_calls into virtual table, for each call since it's fixed len, we set is_final to true only on the last chunk + let fix_len_vt = parallelize_core(pool, fix_len_calls, |ctx, (call, hash)| { + let len = ctx.load_constant(F::from(call.bytes().len() as u64)); + let packed_input = format_input(ctx, gate, call.bytes().bytes(), len); + let capacity = packed_input.len(); + packed_input + .into_iter() + .enumerate() + .map(|(i, chunk)| { + let is_final = ctx.load_constant(F::from(i == capacity - 1)); + let v_i = KeccakVirtualInput::new(chunk.concat().try_into().unwrap(), is_final); + // we mask this with is_final later + let v_o = KeccakVirtualOutput::new(hash); + (v_i, v_o) + }) + .collect_vec() + }) + .into_iter() + .flatten(); + let empty_hash = encode_h256_to_hilo::(&H256(keccak256([]))); + // flat map fix_len_calls into virtual table + let var_len_vt = parallelize_core(pool, var_len_calls, |ctx, (call, hash)| { + let num_keccak_f_m1 = call.num_keccak_f_m1(ctx, range_chip); + // `ensure_0_padding` so that `packed_input` after variable length `bytes.len()` corresponds to format_input of [] empty bytes + let bytes = call.bytes().ensure_0_padding(ctx, gate); + let packed_input = format_input(ctx, gate, bytes.bytes(), *bytes.len()); + // since a call is variable_length, we need to set is_final to be 0, 0, ..., 0, 1, 1, ... where the first 1 is at `num_keccak_f - 1` + let capacity = get_num_keccak_f(call.bytes().max_len()); + assert_eq!(capacity, packed_input.len()); + // we have constrained that `num_keccak_f - 1 < capacity` + let indicator = gate.idx_to_indicator(ctx, num_keccak_f_m1, capacity); + let is_finals = gate.partial_sums(ctx, indicator.clone()).collect_vec(); + zip(packed_input, zip_eq(is_finals, indicator)) + .map(|(chunk, (is_final, is_out))| { + // We pad with empty hashes keccak([]) between the true capacity and the max capacity for each var len call + let v_i = KeccakVirtualInput::new(chunk.concat().try_into().unwrap(), is_final); + // If we beyond the true capacity, then we need the hash output to be empty hash + // We will later mask hash with is_final as well + let hash_hi = gate.select(ctx, hash.hi(), Constant(empty_hash.hi()), is_out); + let hash_lo = gate.select(ctx, hash.lo(), Constant(empty_hash.lo()), is_out); + let v_o = KeccakVirtualOutput::new(HiLo::from_hi_lo([hash_hi, hash_lo])); + (v_i, v_o) + }) + .collect_vec() + }) + .into_iter() + .flatten(); + + // now we compare the virtual table from the vanilla keccak circuit with the virtual table constructed from the calls. we enforce the inputs are exactly equal. we enforce the outputs are exactly equal when `is_final = true`. + let ctx = pool.main(); + for ((table_i, table_o), (call_i, call_o)) in + virtual_table.zip_eq(fix_len_vt.chain(var_len_vt)) + { + constrain_vec_equal(ctx, &table_i.packed_input, &call_i.packed_input); + ctx.constrain_equal(&table_i.is_final, &call_i.is_final); + let is_final = SafeTypeChip::unsafe_to_bool(table_i.is_final); + enforce_conditional_equality(ctx, gate, table_o.hash.hi(), call_o.hash.hi(), is_final); + enforce_conditional_equality(ctx, gate, table_o.hash.lo(), call_o.hash.lo(), is_final); + } + } + + /// Consumes all collected calls and raw assigns them to the keccak sub-circuit specified by `keccak_config`. Then constrains that the [`LoadedKeccakF`]s must equal the virtually assigned calls. + /// + /// This is the only function you need to call to process all calls. + /// + /// ## Assumptions + /// - This should be the **only** time `layouter` is allowed to assign to `keccak_config`. + /// - `range_chip` should share same reference to `copy_manager` as `pool`. + pub fn assign_raw_and_constrain( + self, + keccak_circuit_params: KeccakConfigParams, + keccak_config: &KeccakCircuitConfig, + layouter: &mut impl Layouter, + pool: &mut SinglePhaseCoreManager, + range_chip: &RangeChip, + ) -> Result<(), plonk::Error> { + // We constrain the collected keccak calls using the vanilla zkEVM keccak circuit: + // convert `calls` to actual keccak inputs as bytes (Vec) + let keccak_inputs = self.format_keccak_inputs(); + // raw synthesize of vanilla keccak circuit: + let keccak_assigned_rows: Vec> = layouter.assign_region( + || "vanilla keccak circuit", + |mut region| { + let (keccak_rows, _) = + multi_keccak::(&keccak_inputs, None, keccak_circuit_params); + Ok(keccak_config.assign(&mut region, &keccak_rows)) + }, + )?; + // Convert raw assigned cells into virtual cells + let virtual_keccak_fs = transmute_keccak_assigned_to_virtual( + &pool.copy_manager, + keccak_assigned_rows, + keccak_circuit_params.rows_per_round, + ); + self.pack_and_constrain(virtual_keccak_fs, pool, range_chip); + Ok(()) + } +} + +#[cfg(feature = "aggregation")] +mod aggregation { + use crate::Field; + use snark_verifier_sdk::CircuitExt; + + use crate::utils::build_utils::aggregation::CircuitMetadata; + + use super::{EthCircuitInstructions, RlcKeccakCircuitImpl}; + + impl CircuitExt for RlcKeccakCircuitImpl + where + F: Field, + I: EthCircuitInstructions + CircuitMetadata, + { + fn accumulator_indices() -> Option> { + I::accumulator_indices() + } + + fn instances(&self) -> Vec> { + self.instances() + } + + fn num_instance(&self) -> Vec { + self.logic_inputs.num_instance() + } + } +} diff --git a/axiom-eth/src/utils/keccak/mod.rs b/axiom-eth/src/utils/keccak/mod.rs new file mode 100644 index 00000000..4ea4269e --- /dev/null +++ b/axiom-eth/src/utils/keccak/mod.rs @@ -0,0 +1,57 @@ +use zkevm_hashes::keccak::vanilla::keccak_packed_multi::get_keccak_capacity; + +pub mod decorator; + +#[cfg(test)] +pub mod tests; + +// num_unusable_rows: this is not used in `configure_with_params`, only for auto-circuit tuning +// numbers from empirical tests and also from Scroll: https://github.com/scroll-tech/zkevm-circuits/blob/7d9bc181953cfc6e7baf82ff0ce651281fd70a8a/zkevm-circuits/src/keccak_circuit/keccak_packed_multi.rs#L59C55-L62C7 +const UNUSABLE_ROWS_BY_ROWS_PER_ROUND: [usize; 25] = [ + 23, 59, 59, 59, 59, 59, 59, 59, 55, 69, 65, 61, 47, 81, 79, 77, 75, 73, 71, 69, 67, 65, 63, 61, + 59, +]; + +/// Returns unusable rows for keccak circuit configured with `rows_per_round` rows per round. +/// This is only used for circuit tuning. +pub fn get_keccak_unusable_rows(rows_per_round: usize) -> usize { + *UNUSABLE_ROWS_BY_ROWS_PER_ROUND.get(rows_per_round - 1).unwrap_or(&109) +} + +/// Returns (unusable_rows, rows_per_round) that is optimal for the given `k` and `capacity`. +pub fn get_keccak_unusable_rows_from_capacity(k: usize, capacity: usize) -> (usize, usize) { + let mut rows_per_round = 50; + // find the largest rows per round that works, capping at 50 + let mut unusable = 109; + while rows_per_round > 0 { + unusable = get_keccak_unusable_rows(rows_per_round); + let num_rows = (1 << k) - unusable; + let avail_capacity = get_keccak_capacity(num_rows, rows_per_round); + if avail_capacity >= capacity { + // this rows_per_round is enough to meet our needs + break; + } + rows_per_round -= 1; + } + assert_ne!( + rows_per_round, 0, + "k={} is insufficient for requested keccak capacity={}", + k, capacity + ); + (unusable, rows_per_round) +} + +#[cfg(test)] +#[test] +fn test_get_keccak_unusable() { + assert_eq!(get_keccak_unusable_rows_from_capacity(20, 500), (109, 50)); + assert_eq!(get_keccak_unusable_rows_from_capacity(18, 500), (69, 20)); + assert_eq!(get_keccak_unusable_rows_from_capacity(18, 2000), (59, 5)); +} + +#[cfg(test)] +#[test] +#[should_panic] +fn test_get_keccak_insufficient_capacity() { + get_keccak_unusable_rows_from_capacity(18, 12_000); +} diff --git a/axiom-eth/src/utils/keccak/tests/merkle.rs b/axiom-eth/src/utils/keccak/tests/merkle.rs new file mode 100644 index 00000000..d95239af --- /dev/null +++ b/axiom-eth/src/utils/keccak/tests/merkle.rs @@ -0,0 +1,117 @@ +use std::{ + fs::{remove_file, File}, + ops::Deref, + path::Path, + sync::Arc, +}; + +use halo2_base::{ + gates::flex_gate::MultiPhaseThreadBreakPoints, + halo2_proofs::{halo2curves::bn256::Fr, plonk::keygen_vk_custom, SerdeFormat}, + utils::halo2::ProvingKeyGenerator, +}; +use itertools::Itertools; +use snark_verifier_sdk::{halo2::utils::AggregationDependencyIntentOwned, read_pk}; +use zkevm_hashes::keccak::component::circuit::shard::{ + KeccakComponentShardCircuit, KeccakComponentShardCircuitParams, +}; + +use crate::{ + halo2_base::{gates::circuit::CircuitBuilderStage, utils::fs::gen_srs}, + snark_verifier_sdk::{halo2::gen_snark_shplonk, LIMBS}, + utils::{ + build_utils::{ + aggregation::get_dummy_aggregation_params, + pinning::{aggregation::AggTreeId, PinnableCircuit}, + }, + component::utils::compute_poseidon, + merkle_aggregation::{keygen::AggIntentMerkle, InputMerkleAggregation}, + }, +}; + +use super::shard::get_test_keccak_shard_snark; + +#[test] +#[ignore = "prover"] +pub fn test_keccak_merkle_aggregation_prover() -> anyhow::Result<()> { + let dummy_snark = get_test_keccak_shard_snark(vec![])?; + let inputs = vec![ + (0u8..200).collect_vec(), + vec![], + (0u8..1).collect_vec(), + (0u8..135).collect_vec(), + (0u8..136).collect_vec(), + (0u8..200).collect_vec(), + ]; + let snark = get_test_keccak_shard_snark(inputs)?; + let commit = snark.inner.instances[0][0]; + let k = 20u32; + let params = gen_srs(k); + let [dummy_snarks, snarks] = + [dummy_snark, snark].map(|s| InputMerkleAggregation::new(vec![s; 2])); + + let circuit_params = get_dummy_aggregation_params(k as usize); + let mut keygen_circuit = + dummy_snarks.build(CircuitBuilderStage::Keygen, circuit_params, ¶ms)?; + keygen_circuit.calculate_params(Some(20)); + let pinning_path = "configs/tests/keccak_shard2_merkle.json"; + let pk_path = "data/tests/keccak_shard2_merkle.pk"; + let (pk, pinning) = keygen_circuit.create_pk(¶ms, pk_path, pinning_path)?; + + let prover_circuit = snarks.prover_circuit(pinning, ¶ms)?; + let snark = gen_snark_shplonk(¶ms, &pk, prover_circuit, None::<&str>); + let root = snark.instances[0][4 * LIMBS]; + assert_eq!(compute_poseidon(&[commit, commit]), root); + + remove_file(pk_path).ok(); + remove_file("data/test/keccak_shard.pk").ok(); + Ok(()) +} + +// CARGO_PROFILE_DEV_DEBUG_ASSERTIONS=false cargo t test_keygen_merkle_aggregation -- --nocapture +#[test] +#[ignore = "keygen; turn off debug assertions"] +fn test_keygen_merkle_aggregation() -> anyhow::Result<()> { + let dummy_snark = get_test_keccak_shard_snark(vec![])?; + let pk_path = Path::new("data/tests/keccak_shard.pk"); + let pinning_path = "configs/tests/keccak_shard.json"; + let (params, _): (KeccakComponentShardCircuitParams, MultiPhaseThreadBreakPoints) = + serde_json::from_reader(File::open(pinning_path)?)?; + let dep_pk = read_pk::>(pk_path, params).unwrap(); + let dep_vk = dep_pk.get_vk().clone(); + + let dep_intent = AggregationDependencyIntentOwned { + vk: dep_vk, + num_instance: vec![1], + accumulator_indices: None, + agg_vk_hash_data: None, + }; + let k = 20; + let kzg_params = gen_srs(k); + let kzg_params = Arc::new(kzg_params); + let child_id = AggTreeId::default(); + let intent = AggIntentMerkle { + kzg_params: kzg_params.clone(), + to_agg: vec![child_id; 2], + deps: vec![dep_intent; 2], + k, + }; + let (pk1, _) = intent.create_pk_and_pinning(&kzg_params); + + let dummy_snarks = InputMerkleAggregation::new(vec![dummy_snark; 2]); + let circuit_params = get_dummy_aggregation_params(k as usize); + let mut keygen_circuit = + dummy_snarks.build(CircuitBuilderStage::Keygen, circuit_params, &kzg_params)?; + keygen_circuit.calculate_params(Some(20)); + let vk2 = keygen_vk_custom(kzg_params.deref(), &keygen_circuit, false)?; + + let mut buf1 = Vec::new(); + pk1.get_vk().write(&mut buf1, SerdeFormat::RawBytesUnchecked)?; + let mut buf2 = Vec::new(); + vk2.write(&mut buf2, SerdeFormat::RawBytesUnchecked)?; + if buf1 != buf2 { + panic!("vkey mismatch"); + } + + Ok(()) +} diff --git a/axiom-eth/src/utils/keccak/tests/mod.rs b/axiom-eth/src/utils/keccak/tests/mod.rs new file mode 100644 index 00000000..52a320bb --- /dev/null +++ b/axiom-eth/src/utils/keccak/tests/mod.rs @@ -0,0 +1,2 @@ +pub mod merkle; +pub mod shard; diff --git a/axiom-eth/src/utils/keccak/tests/shard.rs b/axiom-eth/src/utils/keccak/tests/shard.rs new file mode 100644 index 00000000..e356decf --- /dev/null +++ b/axiom-eth/src/utils/keccak/tests/shard.rs @@ -0,0 +1,68 @@ +use std::{ + fs::{remove_file, File}, + path::Path, +}; + +use halo2_base::{gates::flex_gate::MultiPhaseThreadBreakPoints, halo2_proofs::plonk::Circuit}; +use itertools::Itertools; +use snark_verifier_sdk::{gen_pk, read_pk}; +use zkevm_hashes::keccak::component::circuit::shard::{ + KeccakComponentShardCircuit, KeccakComponentShardCircuitParams, +}; + +use crate::{ + halo2_base::utils::fs::gen_srs, + halo2curves::bn256::Fr, + snark_verifier_sdk::halo2::gen_snark_shplonk, + utils::{keccak::get_keccak_unusable_rows, snark_verifier::EnhancedSnark}, +}; + +pub fn get_test_keccak_shard_snark(mult_inputs: Vec>) -> anyhow::Result { + let k = 18u32; + let capacity = 50; + let rows_per_round = 20; + let num_unusable_rows = get_keccak_unusable_rows(rows_per_round); + let mut keccak_params = + KeccakComponentShardCircuitParams::new(k as usize, num_unusable_rows, capacity, false); + let base_params = + KeccakComponentShardCircuit::::calculate_base_circuit_params(&keccak_params); + keccak_params.base_circuit_params = base_params; + + let params = gen_srs(k); + let pk_path = Path::new("data/tests/keccak_shard.pk"); + let pinning_path = "configs/tests/keccak_shard.json"; + let (pk, pinning) = if let Ok(pk) = + read_pk::>(pk_path, keccak_params.clone()) + { + let pinning: (KeccakComponentShardCircuitParams, MultiPhaseThreadBreakPoints) = + serde_json::from_reader(File::open(pinning_path)?)?; + (pk, pinning) + } else { + let circuit = KeccakComponentShardCircuit::::new(vec![], keccak_params, false); + let pk = gen_pk(¶ms, &circuit, Some(pk_path)); + let break_points = circuit.base_circuit_break_points(); + let pinning = (circuit.params(), break_points); + serde_json::to_writer_pretty(File::create(pinning_path)?, &pinning)?; + (pk, pinning) + }; + + let prover_circuit = KeccakComponentShardCircuit::::new(mult_inputs, pinning.0, true); + prover_circuit.set_base_circuit_break_points(pinning.1); + let snark = gen_snark_shplonk(¶ms, &pk, prover_circuit, None::<&str>); + Ok(EnhancedSnark { inner: snark, agg_vk_hash_idx: None }) +} + +#[test] +#[ignore = "prover"] +pub fn test_keccak_shard_prover() { + let inputs = vec![ + (0u8..200).collect_vec(), + vec![], + (0u8..1).collect_vec(), + (0u8..135).collect_vec(), + (0u8..136).collect_vec(), + (0u8..200).collect_vec(), + ]; + get_test_keccak_shard_snark(inputs).unwrap(); + remove_file("data/test/keccak_shard.pk").ok(); +} diff --git a/axiom-eth/src/utils/merkle_aggregation.rs b/axiom-eth/src/utils/merkle_aggregation.rs new file mode 100644 index 00000000..72814ff2 --- /dev/null +++ b/axiom-eth/src/utils/merkle_aggregation.rs @@ -0,0 +1,291 @@ +use std::iter; + +use anyhow::{bail, Result}; +use itertools::Itertools; + +use crate::{ + halo2_base::{ + gates::{circuit::CircuitBuilderStage, GateChip}, + poseidon::hasher::PoseidonHasher, + }, + halo2_proofs::poly::kzg::commitment::ParamsKZG, + halo2curves::bn256::Bn256, + snark_verifier_sdk::{ + halo2::{ + aggregation::{AggregationCircuit, VerifierUniversality}, + POSEIDON_SPEC, + }, + SHPLONK, + }, + utils::{ + build_utils::pinning::aggregation::AggregationCircuitPinning, + component::utils::compute_poseidon_merkle_tree, + }, +}; + +use super::snark_verifier::{ + get_accumulator_indices, AggregationCircuitParams, EnhancedSnark, NUM_FE_ACCUMULATOR, +}; + +/// The input to the Merkle Aggregation Circuit is a collection of [EnhancedSnark]s. +/// The number of snarks does not need to be a power of two. +/// +/// The `snarks` are not allowed to be from universal aggregation circuits. +/// +/// We allow snarks to have accumulators (so they can be non-universal aggregation circuits). +/// +/// We check that: +/// - All snarks have the same number of instances _excluding accumulators_. +/// - All snarks must have at least one non-accumulator instance. +/// +/// The public instances of the Merkle Aggregation Circuit are: +/// - accumulator +/// - padded merkle root of `output`s of `snarks`, where merkle tree is padded with `0`s to next power of two. The `output` is defined as the first non-accumulator instance in each snark. +/// - `snark[i].instance[j] = snark[k].instance[j]` for all `i,j` pairs and all `j > 0`, where `snark[i].instance` are all the non-accumulator instances in `snark[i]`. +/// +/// `snarks` should be non-empty. +#[derive(Clone, Debug)] +pub struct InputMerkleAggregation { + pub snarks: Vec, +} + +impl InputMerkleAggregation { + /// See [InputMerkleAggregation] for details. + pub fn new(snarks: impl IntoIterator) -> Self { + let snarks = snarks.into_iter().collect_vec(); + assert!(!snarks.is_empty()); + assert!( + snarks.iter().all(|s| s.agg_vk_hash_idx.is_none()), + "[MerkleAggregation] snark cannot be universal aggregation circuit" + ); + Self { snarks } + } +} + +impl TryFrom> for InputMerkleAggregation { + type Error = anyhow::Error; + fn try_from(snarks: Vec) -> Result { + if snarks.is_empty() { + bail!("snarks cannot be empty"); + } + if snarks.iter().all(|s| s.agg_vk_hash_idx.is_none()) { + bail!("snark cannot be universal aggregation circuit"); + } + Ok(Self { snarks }) + } +} + +impl InputMerkleAggregation { + /// Returns [AggregationCircuit] with loaded witnesses for a **non-universal** aggregation + /// of the snarks in `self`. + /// + /// This circuit MUST implement `CircuitExt` with accumulator indices non-empty. + /// + /// We allow snarks to have accumulators (so they can be non-universal aggregation circuits). + /// + /// We will check that either: + /// - All snarks have a single instance besides any accumulators, in which case this is the `output` of that snark + /// - Or all snarks have 2 instances besides any accumulators, in which case this is the `(output, promise)` of that snark + /// + /// The public instances of the Merkle Aggregation Circuit are: + /// - accumulator + /// - padded merkle root of `output`s of `snarks`, where merkle tree is padded with `0`s to next power of two + /// - `promise` from `snark[0]` if all snarks have 2 instances. The circuit will constrain that `promise[i] == promise[j]` for all `i,j` + /// + /// `snarks` should be non-empty. + pub fn build( + self, + stage: CircuitBuilderStage, + circuit_params: AggregationCircuitParams, + kzg_params: &ParamsKZG, + ) -> anyhow::Result { + let snarks = self.snarks; + assert!(!snarks.is_empty()); + let prev_acc_indices = get_accumulator_indices(snarks.iter().map(|s| &s.inner)); + + let mut circuit = AggregationCircuit::new::( + stage, + circuit_params, + kzg_params, + snarks.into_iter().map(|s| s.inner), + VerifierUniversality::None, + ); + + // remove accumulator from previous instances + let mut prev_instances = circuit.previous_instances().clone(); + for (prev_instance, acc_indices) in prev_instances.iter_mut().zip_eq(prev_acc_indices) { + for i in acc_indices.iter().rev() { + prev_instance.remove(*i); + } + } + // number of non-accumulator instances per-snark + let num_instance = prev_instances[0].len(); + if num_instance == 0 { + bail!("snark should have at least 1 instances"); + } + if prev_instances.iter().any(|i| i.len() != num_instance) { + bail!("snarks should have same number of instances"); + } + let builder = &mut circuit.builder; + let ctx = builder.main(0); + let const_zero = ctx.load_zero(); + // Compute Merkle root of instance[0] over all snarks + let num_leaves = prev_instances.len().next_power_of_two(); + let leaves = prev_instances + .iter() + .map(|instance| instance[0]) + .chain(iter::repeat(const_zero)) + .take(num_leaves) + .collect_vec(); + + // Optimization: if there is only one snark, we don't need to compute the merkle tree so no need to create hasher. + let merkle_root = if leaves.len() == 1 { + leaves[0] + } else { + let mut hasher = PoseidonHasher::new(POSEIDON_SPEC.clone()); + let gate = GateChip::default(); + hasher.initialize_consts(ctx, &gate); + let nodes = compute_poseidon_merkle_tree(ctx, &gate, &hasher, leaves); + nodes[0] + }; + + // If instance[1] exists, constrain that instance[1] is equal for all snarks + for j in 1..num_instance { + let instance_0j = &prev_instances[0][j]; + for instance in prev_instances.iter().skip(1) { + ctx.constrain_equal(instance_0j, &instance[j]); + } + } + + if builder.assigned_instances.len() != 1 { + bail!("should only have 1 instance column"); + } + assert_eq!(builder.assigned_instances[0].len(), NUM_FE_ACCUMULATOR); + builder.assigned_instances[0].push(merkle_root); + builder.assigned_instances[0].extend_from_slice(&prev_instances[0][1..]); + + Ok(circuit) + } + + /// Circuit for witness generation only + pub fn prover_circuit( + self, + pinning: AggregationCircuitPinning, + kzg_params: &ParamsKZG, + ) -> Result { + Ok(self + .build(CircuitBuilderStage::Prover, pinning.params, kzg_params)? + .use_break_points(pinning.break_points)) + } +} + +#[cfg(feature = "keygen")] +/// Module only used for keygen helper utilities +pub mod keygen { + use std::sync::Arc; + + use halo2_base::{ + gates::circuit::CircuitBuilderStage, + halo2_proofs::{ + halo2curves::bn256::Bn256, + poly::{commitment::ParamsProver, kzg::commitment::ParamsKZG}, + }, + utils::halo2::KeygenCircuitIntent, + }; + use snark_verifier_sdk::{ + halo2::{ + aggregation::AggregationCircuit, + utils::{ + AggregationDependencyIntent, AggregationDependencyIntentOwned, + KeygenAggregationCircuitIntent, + }, + }, + CircuitExt, Snark, + }; + + use crate::{ + halo2curves::bn256::Fr, + utils::{ + build_utils::{ + aggregation::get_dummy_aggregation_params, + keygen::compile_agg_dep_to_protocol, + pinning::{ + aggregation::{AggTreeId, GenericAggParams, GenericAggPinning}, + CircuitPinningInstructions, + }, + }, + snark_verifier::EnhancedSnark, + }, + }; + + use super::InputMerkleAggregation; + + /// Intent for Merkle Aggregation Circuit. + /// For now we do not record the generator of the trusted setup here since it + /// is assumed to be read from file. + #[derive(Clone, Debug)] + pub struct AggIntentMerkle { + /// This is from bad UX; only svk = kzg_params.get_g()[0] is used + pub kzg_params: Arc>, + /// Must be same length as `deps`. Has the corresponding circuit IDs for each dependency snark + pub to_agg: Vec, + /// Vec of `vk, num_instance, is_aggregation` for each dependency snark + pub deps: Vec, + /// The log_2 domain size of the current aggregation circuit + pub k: u32, + } + + impl KeygenAggregationCircuitIntent for AggIntentMerkle { + fn intent_of_dependencies(&self) -> Vec { + self.deps.iter().map(|d| d.into()).collect() + } + fn build_keygen_circuit_from_snarks(self, snarks: Vec) -> Self::AggregationCircuit { + let input = InputMerkleAggregation::new( + snarks.into_iter().map(|s| EnhancedSnark::new(s, None)), + ); + let agg_params = get_dummy_aggregation_params(self.k as usize); + let mut circuit = + input.build(CircuitBuilderStage::Keygen, agg_params, &self.kzg_params).unwrap(); + circuit.calculate_params(Some(20)); + circuit + } + } + + impl KeygenCircuitIntent for AggIntentMerkle { + type ConcreteCircuit = AggregationCircuit; + /// We omit here tags (e.g., hash of vkeys) of the dependencies, they should be recorded separately. + type Pinning = GenericAggPinning; + fn get_k(&self) -> u32 { + self.k + } + fn build_keygen_circuit(self) -> Self::ConcreteCircuit { + self.build_keygen_circuit_shplonk() + } + fn get_pinning_after_keygen( + self, + kzg_params: &ParamsKZG, + circuit: &Self::ConcreteCircuit, + ) -> Self::Pinning { + let svk = kzg_params.get_g()[0]; + let dk = (svk, kzg_params.g2(), kzg_params.s_g2()); + assert_eq!(self.kzg_params.get_g()[0], svk); + assert_eq!(self.kzg_params.g2(), dk.1); + assert_eq!(self.kzg_params.s_g2(), dk.2); + let pinning = circuit.pinning(); + let to_agg = self + .deps + .iter() + .map(|d| compile_agg_dep_to_protocol(kzg_params, d, false)) + .collect(); + let agg_params = GenericAggParams { to_agg, agg_params: pinning.params }; + GenericAggPinning { + params: agg_params, + num_instance: circuit.num_instance(), + accumulator_indices: AggregationCircuit::accumulator_indices().unwrap(), + agg_vk_hash_data: None, + dk: dk.into(), + break_points: pinning.break_points, + } + } + } +} diff --git a/axiom-eth/src/utils/mod.rs b/axiom-eth/src/utils/mod.rs new file mode 100644 index 00000000..296067c0 --- /dev/null +++ b/axiom-eth/src/utils/mod.rs @@ -0,0 +1,261 @@ +use crate::Field; +use ethers_core::{ + types::{Address, H256}, + utils::keccak256, +}; +use halo2_base::{ + gates::{GateInstructions, RangeChip}, + halo2_proofs::halo2curves::ff::PrimeField, + safe_types::{SafeBool, SafeByte, SafeBytes32, SafeTypeChip}, + utils::{decompose, BigPrimeField, ScalarField}, + AssignedValue, Context, + QuantumCell::{Constant, Witness}, +}; +use itertools::Itertools; + +use self::hilo::HiLo; + +/// Traits and templates for concrete `Circuit` implementation and compatibility with [snark_verifier_sdk] +pub mod build_utils; +/// Utilities for circuit writing using [halo2_base] +pub mod circuit_utils; +/// Component framework. +pub mod component; +/// Contains convenience `EthCircuitInstructions` to help auto-implement a Halo2 circuit that uses `RlcCircuitBuilder` and `KeccakPromiseLoader`. +pub mod eth_circuit; +/// Same as Word2 from zkevm +pub mod hilo; +/// Shim for keccak circuit from [axiom_eth::zkevm_hashes::keccak] +pub mod keccak; +/// Non-universal aggregation circuit that verifies several snarks and computes the merkle root +/// of snark inputs. See [InputMerkleAggregation] for more details. +#[cfg(feature = "aggregation")] +pub mod merkle_aggregation; +/// Snark verifier SDK helpers (eventually move to snark-verifier-sdk) +#[cfg(feature = "aggregation")] +pub mod snark_verifier; + +pub const DEFAULT_RLC_CACHE_BITS: usize = 32; + +/// H256 as hi-lo (u128, u128) +pub type AssignedH256 = [AssignedValue; 2]; + +pub fn get_merkle_mountain_range(leaves: &[H256], max_depth: usize) -> Vec { + let num_leaves = leaves.len(); + let mut merkle_roots = Vec::with_capacity(max_depth + 1); + let mut start_idx = 0; + for depth in (0..max_depth + 1).rev() { + if (num_leaves >> depth) & 1 == 1 { + merkle_roots.push(h256_tree_root(&leaves[start_idx..start_idx + (1 << depth)])); + start_idx += 1 << depth; + } else { + merkle_roots.push(H256::zero()); + } + } + merkle_roots +} + +/// # Assumptions +/// * `leaves` should not be empty +pub fn h256_tree_root(leaves: &[H256]) -> H256 { + assert!(!leaves.is_empty(), "leaves should not be empty"); + let depth = leaves.len().ilog2(); + assert_eq!(leaves.len(), 1 << depth); + if depth == 0 { + return leaves[0]; + } + keccak256_tree_root(leaves.iter().map(|leaf| leaf.as_bytes().to_vec()).collect()) +} + +pub fn keccak256_tree_root(mut leaves: Vec>) -> H256 { + assert!(leaves.len() > 1); + let depth = leaves.len().ilog2(); + assert_eq!(leaves.len(), 1 << depth, "leaves.len() must be a power of 2"); + for d in (0..depth).rev() { + for i in 0..(1 << d) { + leaves[i] = keccak256([&leaves[2 * i][..], &leaves[2 * i + 1][..]].concat()).to_vec(); + } + } + H256::from_slice(&leaves[0]) +} + +// Field has PrimeField +/// Takes `hash` as `bytes32` and returns `(hash[..16], hash[16..])` represented as big endian numbers in the prime field +pub fn encode_h256_to_hilo(hash: &H256) -> HiLo { + let hash_lo = u128::from_be_bytes(hash[16..].try_into().unwrap()); + let hash_hi = u128::from_be_bytes(hash[..16].try_into().unwrap()); + HiLo::from_lo_hi([hash_lo, hash_hi].map(F::from_u128)) +} + +pub fn encode_addr_to_field>(input: &Address) -> F { + let mut bytes = input.as_bytes().to_vec(); + bytes.reverse(); + let mut repr = [0u8; 32]; + repr[..20].copy_from_slice(&bytes); + F::from_bytes_le(&repr) +} + +pub fn bytes_to_fe(bytes: &[u8]) -> Vec { + bytes.iter().map(|b| F::from(*b as u64)).collect() +} + +// circuit utils: + +/// Assigns `bytes` as private witnesses **without** range checking. +pub fn unsafe_bytes_to_assigned( + ctx: &mut Context, + bytes: &[u8], +) -> Vec> { + ctx.assign_witnesses(bytes.iter().map(|b| F::from(*b as u64))) +} + +/// **Unsafe:** Resize `bytes` and assign as private witnesses **without** range checking. +pub fn assign_vec( + ctx: &mut Context, + bytes: Vec, + max_len: usize, +) -> Vec> { + let mut newbytes = bytes; + assert!(newbytes.len() <= max_len); + newbytes.resize(max_len, 0); + newbytes.into_iter().map(|byte| ctx.load_witness(F::from(byte as u64))).collect_vec() +} + +/// Enforces `lhs` equals `rhs` only if `cond` is true. +/// +/// Assumes that `cond` is a bit. +pub fn enforce_conditional_equality( + ctx: &mut Context, + gate: &impl GateInstructions, + lhs: AssignedValue, + rhs: AssignedValue, + cond: SafeBool, +) { + let [lhs, rhs] = [lhs, rhs].map(|x| gate.mul(ctx, x, cond)); + ctx.constrain_equal(&lhs, &rhs); +} + +/// Assumes that `bytes` have witnesses that are bytes. +pub fn bytes_be_to_u128( + ctx: &mut Context, + gate: &impl GateInstructions, + bytes: &[SafeByte], +) -> Vec> { + limbs_be_to_u128(ctx, gate, bytes, 8) +} + +pub(crate) fn limbs_be_to_u128( + ctx: &mut Context, + gate: &impl GateInstructions, + limbs: &[impl AsRef>], + limb_bits: usize, +) -> Vec> { + assert!(!limbs.is_empty(), "limbs must not be empty"); + assert_eq!(128 % limb_bits, 0); + limbs + .chunks(128 / limb_bits) + .map(|chunk| { + gate.inner_product( + ctx, + chunk.iter().rev().map(|a| *a.as_ref()), + (0..chunk.len()).map(|idx| Constant(gate.pow_of_two()[limb_bits * idx])), + ) + }) + .collect_vec() +} + +/// Decomposes `uint` into `num_bytes` bytes, in big-endian, and constrains the decomposition. +/// Here `uint` can be any uint that fits into `F`. +pub fn uint_to_bytes_be( + ctx: &mut Context, + range: &RangeChip, + uint: &AssignedValue, + num_bytes: usize, +) -> Vec> { + let mut bytes_be = uint_to_bytes_le(ctx, range, uint, num_bytes); + bytes_be.reverse(); + bytes_be +} + +/// Decomposes `uint` into `num_bytes` bytes, in little-endian, and constrains the decomposition. +/// Here `uint` can be any uint that fits into `F`. +pub fn uint_to_bytes_le( + ctx: &mut Context, + range: &RangeChip, + uint: &AssignedValue, + num_bytes: usize, +) -> Vec> { + // Same logic as RangeChip::range_check + let pows = range.gate.pow_of_two().iter().step_by(8).take(num_bytes).map(|x| Constant(*x)); + let byte_vals = decompose(uint.value(), num_bytes, 8).into_iter().map(Witness); + let (acc, bytes_le) = range.gate.inner_product_left(ctx, byte_vals, pows); + ctx.constrain_equal(&acc, uint); + + let safe = SafeTypeChip::new(range); + safe.raw_to_fix_len_bytes_vec(ctx, bytes_le, num_bytes).into_bytes() +} + +pub fn bytes_be_to_uint( + ctx: &mut Context, + gate: &impl GateInstructions, + input: &[SafeByte], + num_bytes: usize, +) -> AssignedValue { + gate.inner_product( + ctx, + input[..num_bytes].iter().rev().map(|b| *b.as_ref()), + (0..num_bytes).map(|idx| Constant(gate.pow_of_two()[8 * idx])), + ) +} + +/// Converts a fixed length array of `u128` values into a fixed length array of big endian bytes. +pub fn u128s_to_bytes_be( + ctx: &mut Context, + range: &RangeChip, + u128s: &[AssignedValue], +) -> Vec> { + u128s.iter().map(|u128| uint_to_bytes_be(ctx, range, u128, 16)).concat() +} + +pub fn constrain_vec_equal( + ctx: &mut Context, + a: &[impl AsRef>], + b: &[impl AsRef>], +) { + for (left, right) in a.iter().zip_eq(b.iter()) { + let left = left.as_ref(); + let right = right.as_ref(); + // debug_assert_eq!(left.value(), right.value()); + ctx.constrain_equal(left, right); + } +} + +/// Returns 1 if all entries of `input` are zero, 0 otherwise. +pub fn is_zero_vec( + ctx: &mut Context, + gate: &impl GateInstructions, + input: &[impl AsRef>], +) -> AssignedValue { + let is_zeros = input.iter().map(|x| gate.is_zero(ctx, *x.as_ref())).collect_vec(); + let sum = gate.sum(ctx, is_zeros); + let total_len = F::from(input.len() as u64); + gate.is_equal(ctx, sum, Constant(total_len)) +} + +/// Load [H256] as private witness as [SafeBytes32], where bytes have been range checked. +pub fn load_h256_to_safe_bytes32( + ctx: &mut Context, + safe: &SafeTypeChip, + hash: H256, +) -> SafeBytes32 { + load_bytes32(ctx, safe, hash.0) +} + +pub fn load_bytes32( + ctx: &mut Context, + safe: &SafeTypeChip, + bytes: [u8; 32], +) -> SafeBytes32 { + let raw = ctx.assign_witnesses(bytes.map(|b| F::from(b as u64))); + safe.raw_bytes_to(ctx, raw) +} diff --git a/axiom-eth/src/utils/snark_verifier.rs b/axiom-eth/src/utils/snark_verifier.rs new file mode 100644 index 00000000..85852c3f --- /dev/null +++ b/axiom-eth/src/utils/snark_verifier.rs @@ -0,0 +1,303 @@ +use std::hash::Hash; + +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use serde_with::{base64::Base64, serde_as, DeserializeAs, SerializeAs}; +use snark_verifier::verifier::plonk::PlonkProtocol; + +use crate::{ + halo2_base::{ + gates::{ + circuit::CircuitBuilderStage, flex_gate::threads::SinglePhaseCoreManager, GateChip, + GateInstructions, RangeChip, RangeInstructions, + }, + halo2_proofs::{ + halo2curves::bn256::{Bn256, Fr}, + poly::kzg::commitment::ParamsKZG, + }, + poseidon::hasher::PoseidonSponge, + AssignedValue, Context, + }, + halo2curves::bn256::G1Affine, + snark_verifier_sdk::{ + halo2::{ + aggregation::{ + aggregate_snarks, AggregationCircuit, AggregationConfigParams, + AssignedTranscriptObject, Halo2KzgAccumulationScheme, + PreprocessedAndDomainAsWitness, SnarkAggregationOutput, Svk, VerifierUniversality, + }, + POSEIDON_SPEC, + }, + Snark, LIMBS, + }, +}; +#[cfg(feature = "evm")] +use ethers_core::types::Bytes; +#[cfg(feature = "evm")] +use halo2_base::halo2_proofs::plonk::ProvingKey; +#[cfg(feature = "evm")] +use snark_verifier_sdk::{ + evm::{encode_calldata, gen_evm_proof_shplonk}, + CircuitExt, +}; + +pub type AggregationCircuitParams = AggregationConfigParams; + +pub const NUM_FE_ACCUMULATOR: usize = 4 * LIMBS; + +type F = Fr; + +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EnhancedSnark { + #[serde_as(as = "Base64Bytes")] + pub inner: Snark, + /// If this snark is a **universal** aggregation circuit, then it must expose an + /// `agg_vk_hash` in its public instances. In that case `agg_vk_hash_idx = Some(idx)`, + /// where `flattened_instances[idx]` is the `agg_vk_hash` and `flattened_instances` are the + /// instances **with the old accumulator removed**. + #[serde(skip_serializing_if = "Option::is_none")] + pub agg_vk_hash_idx: Option, +} + +impl EnhancedSnark { + pub fn new(snark: Snark, agg_vk_hash_idx: Option) -> Self { + Self { inner: snark, agg_vk_hash_idx } + } +} + +impl AsRef for EnhancedSnark { + fn as_ref(&self) -> &EnhancedSnark { + self + } +} +impl AsRef for EnhancedSnark { + fn as_ref(&self) -> &Snark { + &self.inner + } +} + +/// **Private** witnesses that form the output of [aggregate_enhanced_snarks]. +#[derive(Clone, Debug)] +pub struct EnhancedSnarkAggregationOutput { + /// We remove the old accumulators from the previous instances using `has_accumulator` from + /// the previous [EnhancedSnark]s + pub previous_instances: Vec>>, + pub accumulator: Vec>, + /// This is the single Poseidon hash of all previous `agg_vk_hash` in previously aggregated + /// universal aggregation snarks, together with the preprocessed digest and transcript initial state + /// (aka partial vkey) from the enhanced snarks that were aggregated. + pub agg_vk_hash: AssignedValue, + /// The proof transcript, as loaded scalars and elliptic curve points, for each SNARK that was aggregated. + pub proof_transcripts: Vec>, +} + +/// Aggregate enhanced snarks as a universal aggregation circuit, taking care to +/// compute the new `agg_vk_hash` by hashing together all previous `agg_vk_hash`s and partial vkeys. +pub fn aggregate_enhanced_snarks( + pool: &mut SinglePhaseCoreManager, + range: &RangeChip, + svk: Svk, // gotten by params.get_g()[0].into() + snarks: &[impl AsRef], +) -> EnhancedSnarkAggregationOutput +where + AS: for<'a> Halo2KzgAccumulationScheme<'a>, +{ + let SnarkAggregationOutput { previous_instances, accumulator, preprocessed, proof_transcripts } = + aggregate_snarks::( + pool, + range, + svk, + snarks.iter().map(|s| s.as_ref().inner.clone()), + VerifierUniversality::Full, + ); + let prev_acc_indices = get_accumulator_indices(snarks.iter().map(|s| &s.as_ref().inner)); + let ctx = pool.main(); + let (previous_instances, agg_vk_hash) = process_prev_instances_and_calc_agg_vk_hash( + ctx, + range.gate(), + previous_instances, + preprocessed, + &prev_acc_indices, + snarks.iter().map(|s| s.as_ref().agg_vk_hash_idx), + ); + EnhancedSnarkAggregationOutput { + previous_instances, + accumulator, + agg_vk_hash, + proof_transcripts, + } +} + +/// Returns `(circuit, prev_instances, agg_vkey_hash)` where `prev_instances` has old accumulators removed. +/// +/// ### Previous `agg_vkey_hash` indices +/// If a snark in `snarks` is a **universal** aggregation circuit, then it **must** expose an +/// `agg_vkey_hash` in its public instances. In that case `agg_vkey_hash_idx = Some(idx)`, +/// where `flattened_instances[idx]` is the `agg_vkey_hash` and `flattened_instances` are the +/// instances of the snark **with the old accumulator removed**. +pub fn create_universal_aggregation_circuit( + stage: CircuitBuilderStage, + circuit_params: AggregationCircuitParams, + kzg_params: &ParamsKZG, + snarks: Vec, + agg_vkey_hash_indices: Vec>, +) -> (AggregationCircuit, Vec>>, AssignedValue) +where + AS: for<'a> Halo2KzgAccumulationScheme<'a>, +{ + assert_eq!(snarks.len(), agg_vkey_hash_indices.len()); + let prev_acc_indices = get_accumulator_indices(&snarks); + let mut circuit = AggregationCircuit::new::( + stage, + circuit_params, + kzg_params, + snarks, + VerifierUniversality::Full, + ); + + let prev_instances = circuit.previous_instances().clone(); + let preprocessed = circuit.preprocessed().clone(); + let builder = &mut circuit.builder; + let ctx = builder.main(0); + let gate = GateChip::default(); + let (previous_instances, agg_vkey_hash) = process_prev_instances_and_calc_agg_vk_hash( + ctx, + &gate, + prev_instances, + preprocessed, + &prev_acc_indices, + agg_vkey_hash_indices, + ); + (circuit, previous_instances, agg_vkey_hash) +} + +/// Calculate agg_vk_hash and simultaneously remove old accumulators from previous_instances +/// +/// Returns `previous_instances` with old accumulators at `prev_accumulator_indices` removed, and returns the new `agg_vk_hash` +pub fn process_prev_instances_and_calc_agg_vk_hash( + ctx: &mut Context, + gate: &impl GateInstructions, + mut prev_instances: Vec>>, + preprocessed: Vec, + prev_accumulator_indices: &[Vec], + prev_agg_vk_hash_indices: impl IntoIterator>, +) -> (Vec>>, AssignedValue) { + let mut sponge = PoseidonSponge::from_spec(ctx, POSEIDON_SPEC.clone()); + for (((prev_instance, partial_vk), acc_indices), agg_vk_hash_idx) in prev_instances + .iter_mut() + .zip_eq(preprocessed) + .zip_eq(prev_accumulator_indices) + .zip_eq(prev_agg_vk_hash_indices) + { + sponge.update(&[partial_vk.k]); + sponge.update(&partial_vk.preprocessed); + for i in acc_indices.iter().sorted().rev() { + prev_instance.remove(*i); + } + if let Some(agg_vk_hash_idx) = agg_vk_hash_idx { + assert!(!acc_indices.is_empty()); + sponge.update(&[prev_instance[agg_vk_hash_idx]]); + } + } + let agg_vk_hash = sponge.squeeze(ctx, gate); + (prev_instances, agg_vk_hash) +} + +/// Returns the indices of the accumulator, if any, for each snark **in sorted (increasing) order**. +/// +/// ## Panics +/// Panics if any snark has accumulator indices not in instance column 0. +pub fn get_accumulator_indices<'a>(snarks: impl IntoIterator) -> Vec> { + snarks + .into_iter() + .map(|snark| { + let accumulator_indices = &snark.protocol.accumulator_indices; + assert!(accumulator_indices.len() <= 1, "num_proof per snark should be 1"); + if let Some(acc_indices) = accumulator_indices.last() { + acc_indices + .iter() + .map(|&(i, j)| { + assert_eq!(i, 0, "accumulator should be in instance column 0"); + j + }) + .sorted() + .dedup() + .collect() + } else { + vec![] + } + }) + .collect() +} + +/// Returns calldata as bytes to be sent to SNARK verifier smart contract. +/// Calldata is public instances (field elements) concatenated with proof (bytes). +/// Returned as [Bytes] for easy serialization to hex string. +#[cfg(feature = "evm")] +pub fn gen_evm_calldata_shplonk>( + params: &ParamsKZG, + pk: &ProvingKey, + circuit: C, +) -> Bytes { + let instances = circuit.instances(); + let proof = gen_evm_proof_shplonk(params, pk, circuit, instances.clone()); + encode_calldata(&instances, &proof).into() +} + +// Newtype wrapper around Base64 encoded/decoded bytes +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Base64Bytes(#[serde_as(as = "Base64")] Vec); + +impl SerializeAs for Base64Bytes { + fn serialize_as(snark: &Snark, serializer: S) -> Result + where + S: serde::Serializer, + { + match bincode::serialize(snark) { + Ok(bytes) => Base64Bytes(bytes).serialize(serializer), + Err(e) => Err(serde::ser::Error::custom(e)), + } + } +} + +impl<'de> DeserializeAs<'de, Snark> for Base64Bytes { + fn deserialize_as(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let bytes = Base64Bytes::deserialize(deserializer)?; + bincode::deserialize(&bytes.0).map_err(serde::de::Error::custom) + } +} + +impl SerializeAs> for Base64Bytes { + fn serialize_as(protocol: &PlonkProtocol, serializer: S) -> Result + where + S: serde::Serializer, + { + match bincode::serialize(protocol) { + Ok(bytes) => Base64Bytes(bytes).serialize(serializer), + Err(e) => Err(serde::ser::Error::custom(e)), + } + } +} + +impl<'de> DeserializeAs<'de, PlonkProtocol> for Base64Bytes { + fn deserialize_as(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let bytes = Base64Bytes::deserialize(deserializer)?; + bincode::deserialize(&bytes.0).map_err(serde::de::Error::custom) + } +} + +impl Hash for EnhancedSnark { + fn hash(&self, state: &mut H) { + bincode::serialize(&self.inner).unwrap().hash(state); + self.agg_vk_hash_idx.hash(state); + } +} diff --git a/axiom-query/Cargo.toml b/axiom-query/Cargo.toml new file mode 100644 index 00000000..3725864b --- /dev/null +++ b/axiom-query/Cargo.toml @@ -0,0 +1,76 @@ +[package] +name = "axiom-query" +version = "2.0.14" +authors = ["Intrinsic Technologies"] +license = "MIT" +edition = "2021" +repository = "https://github.com/axiom-crypto/axiom-eth" +readme = "README.md" +description = "This contains the ZK circuits that generate proofs for the `AxiomV2Query` smart contract." +rust-version = "1.73.0" + +[[bin]] +name = "axiom-query-keygen" +path = "src/bin/keygen.rs" +required-features = ["keygen"] + +[dependencies] +itertools = "0.11" +lazy_static = "1.4.0" +# serialization +serde = { version = "1.0", default-features = false, features = ["derive"] } +serde_json = { version = "1.0", default-features = false } +serde_with = { version = "3.3", features = ["base64"] } +bincode = { version = "1.3.3" } +# misc +log = "0.4" +env_logger = "0.10" +getset = "0.1.2" +ark-std = { version = "0.3.0", features = ["print-trace"], optional = true } +anyhow = "1.0" +downcast-rs = "1.2.0" +hex = "0.4.3" +byteorder = { version = "1.4.3" } +rand = "0.8" +rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } + +# halo2, features turned on by axiom-eth +axiom-eth = { version = "=0.4.0", path = "../axiom-eth", default-features = false, features = ["providers", "aggregation", "evm"] } +axiom-codec = { version = "0.2.0", path = "../axiom-codec", default-features = false } + +# crypto +rlp = "0.5.2" +ethers-core = { version = "=2.0.10" } +# mpt implementation +hasher = { version = "=0.1", features = ["hash-keccak"] } +cita_trie = "=5.0.0" + +# keygen +clap = { version = "=4.4.7", features = ["derive"], optional = true } +enum_dispatch = { version = "0.3.12", optional = true } +serde_yaml = { version = "0.9.16", optional = true } + +[dev-dependencies] +hex = "0.4.3" +ark-std = { version = "0.3.0", features = ["print-trace"] } +log = "0.4" +test-log = "0.2.11" +test-case = "3.1.0" +proptest = "1.1.0" +rand_chacha = "0.3.1" +tokio = { version = "1.33", features = ["macros"] } +reqwest = { version = "0.11", features = ["json"] } +# generating circuit inputs from blockchain +ethers-providers = { version = "2.0.10" } +futures = { version = "0.3" } +blake3 = { version = "=1.5" } + +[features] +default = ["halo2-axiom", "jemallocator", "display", "keygen"] +display = ["axiom-eth/display", "dep:ark-std"] +asm = ["axiom-eth/asm"] +revm = ["axiom-eth/revm"] +# halo2-pse = ["axiom-eth/halo2-pse"] +halo2-axiom = ["axiom-eth/halo2-axiom"] +jemallocator = ["axiom-eth/jemallocator"] +keygen = ["axiom-eth/keygen", "dep:enum_dispatch", "dep:clap", "dep:serde_yaml"] diff --git a/axiom-query/KEYGEN.md b/axiom-query/KEYGEN.md new file mode 100644 index 00000000..634621d6 --- /dev/null +++ b/axiom-query/KEYGEN.md @@ -0,0 +1,57 @@ +# AxiomV2Query ZK Circuits + +# Proving and Verifying Key Generation + +To generate the exact proving and verifying keys we use in production on Ethereum Mainnet, you can do the following: + +1. Download the KZG trusted setup that we use with [this script](../trusted_setup_s3.sh). + +``` +bash ../trusted_setup_s3.sh +``` + +You can read more about the trusted setup we use and how it was generated [here](https://docs.axiom.xyz/docs/transparency-and-security/kzg-trusted-setup). + +The trusted setup will be downloaded to a directory called `params/` by default. You can move the directory elsewhere. We'll refer to the directory as `$SRS_DIR` below. + +2. Install `axiom-query-keygen` binary to your path via: + +```bash +cargo install --path axiom-query --force +``` + +This builds the `axiom-query-keygen` binary in release mode and installs it to your path. +Additional details about the binary can be found [here](./src/bin/README.md). + +3. Generate the proving and verifying keys for one of our production configurations. + +We have multiple aggregation configurations that we use in production. These are specified by intent YAML files in the [`configs/production`](./configs/production/) directory. The configurations, ordered by the sum of generated proving key sizes, are: + +- `all_small.yml` +- `all_32_each_default.yml` +- `all_128_each_default.yml` +- `all_large.yml` +- `all_max.yml` + +We will refer to one of these files as `$INTENT_NAME.yml` below. To generate all proving keys and verifying keys for the configuration corresponding to `$INTENT_NAME.yml`, run: + +```bash + +axiom-query-keygen --srs-dir $SRS_DIR --data-dir $CIRCUIT_DATA_DIR --intent configs/production/$INTENT_NAME.yml --tag $INTENT_NAME +``` + +where `$CIRCUIT_DATA_DIR` is the directory you want to store the output files. After the process is complete, a summary JSON containing the different circuit IDs created and the full aggregation tree of these circuit IDs will be output to `$CIRCUIT_DATA_DIR/$INTENT_NAME.tree`. At the top level of the JSON is an `"aggregate_vk_hash"` field, which commits to the aggregation configuration used in this particular `$INTENT_NAME`. + +Check that the top level `"circuit_id"` in `$INTENT_NAME.tree` equals `e94efbee3e07ae4224ed1ae0a6389f5128d210ff7a2a743e459cff501e4379ab`, _regardless of which `$INTENT_NAME` you used_. This is the circuit ID of the final Axiom Aggregation 2 circuit, which is the same for all configurations because Axiom Aggregation 2 is a universal aggregation circuit. The `aggregate_vk_hash` commits to the aggregation configuration and is used to distinguish between them. + +⚠️ **Special Note:** The `all_max.yml` configuration is very large. The largest proving key generated is 200 GB. To run `axiom-query-keygen` on `all_max.yml`, you need a machine with at least 500 GB of RAM, or enough [swap](https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-22-04) to make up the difference. + +4. Rename and forge format the Solidity SNARK verifier file for the `AxiomV2QueryVerifier` smart contract: + +Check that the top level `"circuit_id"` in `$INTENT_NAME.tree` equals `e94efbee3e07ae4224ed1ae0a6389f5128d210ff7a2a743e459cff501e4379ab`, _regardless of which `$INTENT_NAME` you used_. Then run + +```bash +bash src/bin/rename_snark_verifier.sh $CIRCUIT_DATA_DIR/e94efbee3e07ae4224ed1ae0a6389f5128d210ff7a2a743e459cff501e4379ab.sol +``` + +The final Solidity file will be output to `AxiomV2QueryVerifier.sol`. diff --git a/axiom-query/README.md b/axiom-query/README.md new file mode 100644 index 00000000..d57eccdb --- /dev/null +++ b/axiom-query/README.md @@ -0,0 +1,284 @@ +# AxiomV2Query ZK Circuits + +# Proving and Verifying Key Generation + +For instructions on how to generate the exact proving and verifying keys we use in production on Ethereum Mainnet, see [here](./KEYGEN.md). + +# Overview + +## [Subquery Components](./src/components/subqueries/) + +These are the component circuits that prove all subqueries of a single type: + +- [header](./src/components/subqueries/block_header/) +- [account](./src/components/subqueries/account/) +- [storage](./src/components/subqueries/storage/) +- [transaction](./src/components/subqueries/transaction/) +- [receipt](./src/components/subqueries/receipt/) +- [solidity mappings](./src/components/subqueries/solidity_mappings/) + +These circuits all use the [Component Circuit Framework](../axiom-eth/src/utils/README.md). +They each consist of a `ComponentCircuitImpl` with `RlcCircuitBuilder`, a single `PromiseLoader` for keccak, and a single `PromiseLoader` for a dependent subquery type (or none in the case of header). +The `CoreBuilder` in each case specifies the business logic of the circuit in `virtual_assign_phase{0,1}` but there is no special raw assignment: the raw assignments are all done by `RlcCircuitBuilder` and the `PromiseLoader`s. The virtual table output by each circuit is the table of `(subquery, value)` pairs for that subquery type. The `subquery` type is different for each circuit, but we specify a `flatten` function for each type which uniformizes the way to compute the Poseidon-based commitment to the virtual output table. + +For each circuit, one needs to specify: + +- the types of the virtual table output +- the component type to make promise calls to: + - header: none (but it depends on an external blockhash MMR) + - account: header + - storage: account + - tx: header + - receipt: header + - solidity: storage + +What is hidden from the `CoreBuilder` implementation are the other parts of `ComponentCircuitImpl`: + +- the `PromiseLoader`s are correctly loading the promise table and adding dynamic lookups +- the promise table commitment is being computed correctly + +The unchecked assumption of each circuit are: + +- the promise table commitments for keccak and the called component circuit will be matched with the actual output commitments of those circuits + +**Disclaimer:** there is still a fair amount of copy-paste code between subquery circuit implementations. We are working to reduce this further. + +## [Results Root Component Circuit](./src/components/results/) + +This circuit also uses the [Component Circuit Framework](../axiom-eth/src/utils/README.md) but is a bit special compared to the subquery circuits. It is a component circuit with **no** component output. However it does make promise calls to every other subquery component circuit. +These promise tables are _a priori_ grouped by subquery types. Moreover they may contain intermediate subqueries that were only needed to check correctness of the user subquery (e.g., a tx subquery needs a corresponding header subquery to check the transaction root). In order to compute the `queryHash` and `resultsRoot`, we need the _ordered_ list of user subqueries and results. We do this by "joining" the promise tables of different types into one big lookup table and then doing dynamic lookups to check. + +The join is currently done in the `table` module; it will be moved to `MultiPromiseLoader` in a coming PR. + +The reason this component circuit has no component output is that the true outputs of the circuit are: `resultsRoot`, `resultsRootPoseidon`, and `subqueryHash`s. These are outward/user facing outputs, and for future compatibilty their format depends on the number of subqueries (e.g., `resultsRoot` is a padded Merkle root up to next power of 2 of `numSubqueries`). As such there is no automatic way to compute these commitments and we have custom implementations for them. + +## [Keccak Component Shard Circuit](https://github.com/axiom-crypto/halo2-lib/blob/main/hashes/zkevm/src/keccak/component/circuit/shard.rs) + +The base `KeccakComponentShardCircuit` in `halo2-lib/zkevm-hashes` is a component circuit by our [definition](../axiom-eth/src/utils/README.md#definition-of-component-circuit). +We have added adapters to [`axiom-eth`](../axiom-eth/src/keccak/README.md) so that it can be promise called as a component circuit in our framework. + +## [Merkle Aggregation Circuits](../axiom-eth/src/utils/merkle_aggregation.rs) + +Above we have described the component **shard** circuits. For any component type, multiple shard circuits will be aggregated together (exact configurations will be determined after benchmarking) using `InputMerkleAggregation`. As such we will have component aggregation circuits for each component type where the output commitment is a Merkle root of shard output commitments. +We note that this is fully supported by the Subquery Aggregation Circuit and Axiom Aggregation 1 Circuit because the public instance format of the shard and aggregation circuit of a given component type will be exactly the same, excluding accumulators. The Subquery Aggregation Circuit and Axiom Aggregation 1 Circuit know when to remove old accumulators from previous instances of aggregates snarks, so the treatment is uniform. + +## [Verify Compute Circuit](./src/verify_compute/) + +This circuit aggregates a user submitted compute snark (in the form of `AxiomV2ComputeQuery`). +It then decommits a claimed `resultsRootPoseidon` and the claimed Poseidon commitment to `subqueryHashes`. It then requires calling keccaks to combine subquery results and `computeQuery` +to compute the full `queryHash`. + +This is not a component circuit, but it is implemented using `EthCircuitImpl`, which uses `PromiseLoader` to call the keccak component. + +## [Subquery Aggregation Circuit](./src/subquery_aggregation/) + +This is a universal aggregation circuit that aggregates all subquery circuits and the results root circuit. Since the results root circuit calls each subquery circuit, this aggregation circuit will check the public instance equalities between all promise commitments and output commitments of subquery component circuits. + +It will also check that all keccak promise commitments are equal, but this promise commitment is still not checked. + +The public outputs of the subquery aggregation circuit are: + +- those of the results root circuit, +- the blockhash MMR from the header circuit +- keccak promise commitment +- accumulator and aggregate vkey hash from the universal aggregation + +After this aggregation one can forget about the subquery and results root circuits. + +## [Axiom Aggregation 1 Circuit](./src/axiom_aggregation1/) + +This circuit will aggregate: + +- Verify Compute Circuit +- Subquery Aggregation Circuit +- Keccak Component Final Aggregation Circuit + +In other words, it aggregates all remaining circuits. It will check that the `resultsRootPoseidon` and `subqueryHashes` commits in Verify Compute and Subquery Aggregation circuits match. It will also check that the keccak promise and output commitments match. + +## [Axiom Aggregation 2 Circuit](./src/axiom_aggregation2/) + +This aggregates [Axiom Aggregation 1 Circuit](#axiom-aggregation-1-circuit). It is essentially a passthrough circuit, but we also add a `payee` public instance (this is to prevent transaction frontrunning in the mempool). + +# Circuit Public IO Formats + +We start from circuits that touch the smart contract and work back towards dependencies. + +## Axiom Aggregation 2 (final, for EVM) + +This is the snark that will be verified by a fixed verifier on EVM. + +### Public IO + +The public IO is given by: + +- `accumulator` (384 bytes) +- `sourceChainId` (uint64, in F) +- `computeResultsHash` (bytes32, in hi-lo) +- `queryHash` (bytes32, in hi-lo) +- `querySchema` (bytes32, in hi-lo) +- `blockhashMMRKeccak` (bytes32, in hi-lo) +- `aggVkeyHash` (bytes32, in F) +- `payee` (address, in F) + (It doesn't save any EVM keccaks to hash these all together in-circuit, so we can keep multiple public instances.) + +This will be a fixed `AggregationCircuit` with `Universality::Full` that can verify any circuit with a fixed config. The fixed config will be that of another `AggregationCircuit` (aka single phase `BaseCircuitParams`). We call the previous circuit the final EVM verifies the `AxiomAggregation1Circuit`. + +- The `AxiomAggregation2Circuit` will just pass through public instances of the `AxiomAggregation1Circuit` it is verifying **and add a payee instance**. +- It also computes a new `aggVkeyHash` using `AxiomAggregation1Circuit.aggVkeyHash` and the `k, preprocessed_digest` of `AxiomAggregation1Circuit` itself. +- The `k` and selectors of this `AxiomAggregation2Circuit` must be fixed - our on-chain verifier does not allow universality. +- `AxiomAggregation2Circuit` will be configured to use few columns for cheapest on-chain verification. + +## Axiom Aggregation 1 Circuit + +### Public IO + +This is the same as the [Public IO of Axiom Aggregation 2](#axiom-aggregation-2-final-for-evm) except there is **no payee field**. + +- `accumulator` (384 bytes) +- `sourceChainId` (uint64, in F) +- `computeResultsHash` (bytes32, in hi-lo) +- `queryHash` (bytes32, in hi-lo) +- `querySchema` (bytes32, in hi-lo) +- `blockhashMMRKeccak` (bytes32, in hi-lo) +- `aggVkeyHash` (bytes32, in F) + +This is an `AggregationCircuit` with `Universality::Full` + +- `k` and selectors can be variable: this means we can have multiple `ControllerAggregationCircuit`s (I guess this makes this a trait) + - Any such circuit can do anything that only requires `BaseCircuitBuilder`, in particular it can verify an arbitrary fixed number of snarks + - But it cannot do dynamic lookups (requires new columns), or RLC (unless we decide to add a fixed number of RLC columns to be supported) +- Ideally proof generation takes <10s on g5.48xl + +This circuit will aggregate: + +- `VerifyComputeCircuit` +- `SubqueryAggregationCircuit` +- `KeccakFinalAggregationCircuit` + +The `AxiomAggregation2` and `AxiomAggregation1` circuits could be combined if we had a universal verifier in EVM (such as [here](https://github.com/han0110/halo2-solidity-verifier/tree/feature/solidity-generator)), but previous experience says that the `AxiomAggregation1` circuit is large enough that two layers of aggregation is faster than one big layer anyways. + +## Verify Compute Circuit + +### Public IO + +- [not used] Component managed `output_commit` (F) which should be ignored since this circuit has no virtual output +- `promiseCommitment` (F) - in this case this is `poseidon(promiseKeccakComponent)` +- `accumulator` (384 bytes) +- `sourceChainId` (uint64, in F) +- `computeResultsHash` (bytes32, in hi-lo) +- `queryHash` (bytes32, in hi-lo) +- `querySchema` (bytes32, in hi-lo) +- `resultsRootPoseidon` (F) +- `promiseSubqueryHashes` (F) + +Does the following: + +- Verify the compute snark. + - The `k` and vkey of the snark is committed to in `queryHash`. Therefore we do not have any other (Poseidon) `aggVkeyHash` since this is the only snark we're aggregating. +- Compute `dataQueryHash` from `subqueryHashes` +- Compute `queryHash` from `dataQueryHash` and `computeQuery` +- Compute `querySchema` +- Compute `computeResultsHash` + - Requires looking up compute subqueries in data results with **dynamic lookup** + +Depends on external commitment to computations done in the `ResultsRoot` circuit, which computes the actual subquery results and subquery hashes. + +We separate the calculation of query hash into this circuit and not into the circuits that are aggregated by `SubqueryAggregationCircuit` below because the final `queryHash` calculation cannot be parallelized, whereas we could in the future parallelize everything in `SubqueryAggregationCircuit` into multiple data shards. + +## Subquery Aggregation Circuit + +We can have multiple implementations of these, and each can be literally any circuit - no limitations on number of columns, gates, lookups, etc. This provides us a lot of flexibility, and allows us to add new variants later on without changing `FinalVerifier` and `ControllerAggregationCircuit`. + +### Public IO + +- `accumulator` (384 bytes) +- `promiseKeccakComponent` (F) is the re-exposed keccak component promise commit from previous aggregated component snarks +- `aggVkeyHash` (F) +- `resultsRootPoseidon` (F) +- `commitSubqueryHashes` (F) +- `blockhashMMRKeccak` (bytes32, in hi-lo) + +The `SubqueryAggregationCircuit` will: + +- Aggregate the following circuits (for each type it may either be the shard circuit or the [Merkle Aggregation Circuit](#merkle-aggregation-circuit) of circuits of that type): + - ResultsRoot + - Header + - Account + - Storage + - Tx + - Receipt + - Solidity +- Constrain equalities of data component commits between component "calls". +- Constrain the keccak component promise in each aggregated component is equal, and then re-expose this promise as a public instance + +## Results Root Component Circuit + +### Public IO + +- [not used] Component managed `output_commit`. This component has no virtual table as output, and should not be called directly. +- `promiseComponentsCommit` (F) - poseidon hash of all promises, including keccak +- `resultsRootPoseidon` (F) +- `commitSubqueryHashes` (F) + +`commitSubqueryHashes` is the Poseidon commitment to a _variable number_ `numSubqueries` of keccak subquery hashes. We choose to do a variable length Poseidon for more flexibility so the total subquery capacity in this circuit does not have to match the `userMaxSubqueries` in the `VerifyCompute` circuit. As a consequence, this `commitSubqueryHashes` also commits to `numSubqueries`. + +## Header Component Circuit + +### Public IO + +- `commitHeaderComponent` (F) +- `promiseCommitment` (F) +- `blockhashMMRKeccak` (bytes32, in hi-lo) + +## Account Component Circuit + +### Public IO + +- `commitAccountComponent` (F) +- `promiseCommitment` (F) + +## Storage Component Circuit + +### Public IO + +- `commitStorageComponent` (F) +- `promiseCommitment` (F) + +## Transaction Component Circuit + +### Public IO + +- `commitTxComponent` (F) +- `promiseCommitment` (F) + +## Receipt Component Circuit + +### Public IO + +- `commitReceiptComponent` (F) +- `promiseCommitment` (F) + +## Solidity Component Circuit + +### Public IO + +- `commitSolidityComponent` (F) +- `promiseCommitment` (F) + +## Keccak Component Shard Circuit + +### Public IO + +- `keccakComponentCommit` (F) + +## Merkle Aggregation Circuit + +Above we have specified the public IO of component **shard** circuits. We will have multiple configurations where we use the MerkleAggregationCircuit to aggregate multiple shard circuits of the same component type into a new MerkleAggregationCircuit. + +The public IO of the new MerkleAggregationCircuit will consist of the `accumulator` (384 bytes), followed by the exact same instance format as the shard circuit. The output commit is now a Merkle root of shard output commits. The promise commitments of all shards are constrained to be equal. + +# Reference Diagram + +The following diagram is for reference only. The exact configuration and number of circuits will depend on the aggregation configuration used. + +![diagram](https://lucid.app/publicSegments/view/bcfc1a84-c274-4f1d-a747-898ca1ec07f5/image.png) diff --git a/axiom-query/configs/production/all_128_each_default.yml b/axiom-query/configs/production/all_128_each_default.yml new file mode 100644 index 00000000..1539bcb7 --- /dev/null +++ b/axiom-query/configs/production/all_128_each_default.yml @@ -0,0 +1,326 @@ +AxiomAgg2: + k: 23 + force_params: + num_advice: 1 + num_lookup_advice: 1 + num_fixed: 1 + axiom_agg1_intent: + k: 22 + force_params: + num_advice: 13 + num_lookup_advice: 2 + num_fixed: 1 + intent_verify_compute: + k: 22 + core_params: + subquery_results_capacity: 128 + svk: "0100000000000000000000000000000000000000000000000000000000000000" + client_metadata: + version: 0 + numAdvicePerPhase: + - 4 + numLookupAdvicePerPhase: + - 1 + numRlcColumns: 0 + numFixed: 1 + numInstance: + - 2304 + numChallenge: + - 0 + maxOutputs: 128 + isAggregation: false + preprocessed_len: 13 + loader_params: + comp_loader_params: + max_height: 5 + shard_caps: + - 4000 + intent_keccak: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 20 + core_params: + capacity: 4000 + intent_subquery_agg: + k: 22 + deps: + - Header: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + max_extra_data_bytes: 32 + capacity: 132 + loader_params: + comp_loader_params: + note: intent_keccak + max_height: 5 + shard_caps: + - 4000 + - Account: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + capacity: 33 + max_trie_depth: 14 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 5 + shard_caps: + - 4000 + - comp_loader_params: + note: Header + max_height: 1 + shard_caps: + - 132 + - Storage: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + capacity: 33 + max_trie_depth: 13 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 5 + shard_caps: + - 4000 + - comp_loader_params: + note: Account + max_height: 2 + shard_caps: + - 33 + - Tx: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + chip_params: + max_data_byte_len: 8192 + max_access_list_len: 4096 + enable_types: + - true + - true + - true + capacity: 16 + max_trie_depth: 6 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 5 + shard_caps: + - 4000 + - comp_loader_params: + note: Header + max_height: 1 + shard_caps: + - 132 + - Receipt: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + chip_params: + max_data_byte_len: 1024 + max_log_num: 80 + topic_num_bounds: + - 0 + - 4 + capacity: 4 + max_trie_depth: 6 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 5 + shard_caps: + - 4000 + - comp_loader_params: + note: Header + max_height: 1 + shard_caps: + - 132 + - SolidityMapping: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + capacity: 32 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 5 + shard_caps: + - 4000 + - comp_loader_params: + note: Storage + max_height: 2 + shard_caps: + - 33 + - ResultsRoot: + Leaf: + k: 22 + core_params: + enabled_types: + - false + - true + - true + - true + - true + - true + - true + capacity: 128 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 5 + shard_caps: + - 4000 + - params_per_component: + axiom-query:ComponentTypeHeaderSubquery: + max_height: 1 + shard_caps: + - 132 + - 132 + axiom-query:ComponentTypeAccountSubquery: + max_height: 2 + shard_caps: + - 33 + - 33 + - 33 + - 33 + axiom-query:ComponentTypeStorageSubquery: + max_height: 2 + shard_caps: + - 33 + - 33 + - 33 + - 33 + axiom-query:ComponentTypeTxSubquery: + max_height: 3 + shard_caps: + - 16 + - 16 + - 16 + - 16 + - 16 + - 16 + - 16 + - 16 + axiom-query:ComponentTypeReceiptSubquery: + max_height: 5 + shard_caps: + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + - 4 + axiom-query:ComponentTypeSolidityNestedMappingSubquery: + max_height: 2 + shard_caps: + - 32 + - 32 + - 32 + - 32 diff --git a/axiom-query/configs/production/all_32_each_default.yml b/axiom-query/configs/production/all_32_each_default.yml new file mode 100644 index 00000000..958702e2 --- /dev/null +++ b/axiom-query/configs/production/all_32_each_default.yml @@ -0,0 +1,243 @@ +AxiomAgg2: + axiom_agg1_intent: + force_params: + num_advice: 13 + num_fixed: 1 + num_lookup_advice: 2 + intent_keccak: + Node: + child_intent: + Node: + child_intent: + Node: + child_intent: + Leaf: + core_params: + capacity: 4000 + k: 20 + k: 21 + num_children: 2 + k: 21 + num_children: 2 + k: 21 + num_children: 2 + intent_subquery_agg: + deps: + - Header: + Leaf: + core_params: + capacity: 130 + max_extra_data_bytes: 32 + k: 22 + loader_params: + comp_loader_params: + max_height: 3 + note: intent_keccak + shard_caps: + - 4000 + - Account: + Node: + child_intent: + Node: + child_intent: + Leaf: + core_params: + capacity: 24 + max_trie_depth: 14 + k: 21 + loader_params: + - comp_loader_params: + max_height: 3 + note: intent_keccak + shard_caps: + - 4000 + - comp_loader_params: + max_height: 1 + note: Header + shard_caps: + - 130 + k: 21 + num_children: 2 + k: 21 + num_children: 2 + - Storage: + Node: + child_intent: + Node: + child_intent: + Leaf: + core_params: + capacity: 16 + max_trie_depth: 13 + k: 21 + loader_params: + - comp_loader_params: + max_height: 3 + note: intent_keccak + shard_caps: + - 4000 + - comp_loader_params: + max_height: 2 + note: Account + shard_caps: + - 24 + k: 21 + num_children: 2 + k: 21 + num_children: 2 + - Tx: + Node: + child_intent: + Leaf: + core_params: + capacity: 16 + chip_params: + enable_types: + - true + - true + - true + max_access_list_len: 4096 + max_data_byte_len: 8192 + max_trie_depth: 6 + k: 21 + loader_params: + - comp_loader_params: + max_height: 3 + note: intent_keccak + shard_caps: + - 4000 + - comp_loader_params: + max_height: 1 + note: Header + shard_caps: + - 130 + k: 21 + num_children: 2 + - Receipt: + Node: + child_intent: + Leaf: + core_params: + capacity: 16 + chip_params: + max_data_byte_len: 800 + max_log_num: 20 + topic_num_bounds: + - 0 + - 4 + max_trie_depth: 6 + k: 21 + loader_params: + - comp_loader_params: + max_height: 3 + note: intent_keccak + shard_caps: + - 4000 + - comp_loader_params: + max_height: 1 + note: Header + shard_caps: + - 130 + k: 21 + num_children: 2 + - SolidityMapping: + Node: + child_intent: + Leaf: + core_params: + capacity: 16 + k: 21 + loader_params: + - comp_loader_params: + max_height: 3 + note: intent_keccak + shard_caps: + - 4000 + - comp_loader_params: + max_height: 2 + note: Storage + shard_caps: + - 16 + k: 21 + num_children: 2 + - ResultsRoot: + Leaf: + core_params: + capacity: 128 + enabled_types: + - false + - true + - true + - true + - true + - true + - true + k: 22 + loader_params: + - comp_loader_params: + max_height: 3 + note: intent_keccak + shard_caps: + - 4000 + - params_per_component: + axiom-query:ComponentTypeAccountSubquery: + max_height: 2 + shard_caps: + - 24 + - 24 + axiom-query:ComponentTypeHeaderSubquery: + max_height: 1 + shard_caps: + - 130 + axiom-query:ComponentTypeReceiptSubquery: + max_height: 1 + shard_caps: + - 16 + - 16 + axiom-query:ComponentTypeSolidityNestedMappingSubquery: + max_height: 1 + shard_caps: + - 16 + - 16 + axiom-query:ComponentTypeStorageSubquery: + max_height: 2 + shard_caps: + - 16 + - 16 + axiom-query:ComponentTypeTxSubquery: + max_height: 1 + shard_caps: + - 16 + - 16 + k: 22 + intent_verify_compute: + core_params: + client_metadata: + isAggregation: false + maxOutputs: 128 + numAdvicePerPhase: + - 4 + numChallenge: + - 0 + numFixed: 1 + numInstance: + - 2304 + numLookupAdvicePerPhase: + - 1 + numRlcColumns: 0 + version: 0 + preprocessed_len: 13 + subquery_results_capacity: 128 + svk: "0100000000000000000000000000000000000000000000000000000000000000" + k: 22 + loader_params: + comp_loader_params: + max_height: 3 + shard_caps: + - 4000 + k: 22 + force_params: + num_advice: 1 + num_fixed: 1 + num_lookup_advice: 1 + k: 23 diff --git a/axiom-query/configs/production/all_large.yml b/axiom-query/configs/production/all_large.yml new file mode 100644 index 00000000..a5122dba --- /dev/null +++ b/axiom-query/configs/production/all_large.yml @@ -0,0 +1,282 @@ +AxiomAgg2: + k: 23 + force_params: + num_advice: 1 + num_lookup_advice: 1 + num_fixed: 1 + axiom_agg1_intent: + k: 22 + force_params: + num_advice: 13 + num_lookup_advice: 2 + num_fixed: 1 + intent_verify_compute: + k: 22 + core_params: + subquery_results_capacity: 128 + svk: "0100000000000000000000000000000000000000000000000000000000000000" + client_metadata: + version: 0 + numAdvicePerPhase: + - 4 + numLookupAdvicePerPhase: + - 1 + numRlcColumns: 0 + numFixed: 1 + numInstance: + - 2304 + numChallenge: + - 0 + maxOutputs: 128 + isAggregation: false + preprocessed_len: 13 + loader_params: + comp_loader_params: + max_height: 4 + shard_caps: + - 5000 + intent_keccak: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 20 + core_params: + capacity: 5000 + intent_subquery_agg: + k: 22 + deps: + - Header: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + max_extra_data_bytes: 32 + capacity: 132 + loader_params: + comp_loader_params: + note: intent_keccak + max_height: 4 + shard_caps: + - 5000 + - Account: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + capacity: 33 + max_trie_depth: 14 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 4 + shard_caps: + - 5000 + - comp_loader_params: + note: Header + max_height: 1 + shard_caps: + - 132 + - Storage: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + capacity: 33 + max_trie_depth: 13 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 4 + shard_caps: + - 5000 + - comp_loader_params: + note: Account + max_height: 2 + shard_caps: + - 33 + - Tx: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + chip_params: + max_data_byte_len: 32768 + max_access_list_len: 16384 + enable_types: + - true + - true + - true + capacity: 4 + max_trie_depth: 6 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 4 + shard_caps: + - 5000 + - comp_loader_params: + note: Header + max_height: 1 + shard_caps: + - 132 + - Receipt: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + chip_params: + max_data_byte_len: 2048 + max_log_num: 80 + topic_num_bounds: + - 0 + - 4 + capacity: 2 + max_trie_depth: 6 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 4 + shard_caps: + - 5000 + - comp_loader_params: + note: Header + max_height: 1 + shard_caps: + - 132 + - SolidityMapping: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + capacity: 32 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 4 + shard_caps: + - 5000 + - comp_loader_params: + note: Storage + max_height: 2 + shard_caps: + - 33 + - ResultsRoot: + Leaf: + k: 22 + core_params: + enabled_types: + - false + - true + - true + - true + - true + - true + - true + capacity: 128 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 4 + shard_caps: + - 5000 + - params_per_component: + axiom-query:ComponentTypeHeaderSubquery: + max_height: 1 + shard_caps: + - 132 + - 132 + axiom-query:ComponentTypeAccountSubquery: + max_height: 2 + shard_caps: + - 33 + - 33 + - 33 + - 33 + axiom-query:ComponentTypeStorageSubquery: + max_height: 2 + shard_caps: + - 33 + - 33 + - 33 + - 33 + axiom-query:ComponentTypeTxSubquery: + max_height: 2 + shard_caps: + - 4 + - 4 + - 4 + - 4 + axiom-query:ComponentTypeReceiptSubquery: + max_height: 3 + shard_caps: + - 2 + - 2 + - 2 + - 2 + - 2 + - 2 + - 2 + - 2 + axiom-query:ComponentTypeSolidityNestedMappingSubquery: + max_height: 2 + shard_caps: + - 32 + - 32 + - 32 + - 32 diff --git a/axiom-query/configs/production/all_max.yml b/axiom-query/configs/production/all_max.yml new file mode 100644 index 00000000..8631218b --- /dev/null +++ b/axiom-query/configs/production/all_max.yml @@ -0,0 +1,271 @@ +AxiomAgg2: + k: 23 + force_params: + num_advice: 1 + num_lookup_advice: 1 + num_fixed: 1 + axiom_agg1_intent: + k: 22 + force_params: + num_advice: 13 + num_lookup_advice: 2 + num_fixed: 1 + intent_verify_compute: + k: 22 + core_params: + subquery_results_capacity: 128 + svk: "0100000000000000000000000000000000000000000000000000000000000000" + client_metadata: + version: 0 + numAdvicePerPhase: + - 4 + numLookupAdvicePerPhase: + - 1 + numRlcColumns: 0 + numFixed: 1 + numInstance: + - 2304 + numChallenge: + - 0 + maxOutputs: 128 + isAggregation: false + preprocessed_len: 13 + loader_params: + comp_loader_params: + max_height: 4 + shard_caps: + - 4000 + intent_keccak: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 20 + core_params: + capacity: 4000 + intent_subquery_agg: + k: 22 + deps: + - Header: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + max_extra_data_bytes: 32 + capacity: 132 + loader_params: + comp_loader_params: + note: intent_keccak + max_height: 4 + shard_caps: + - 4000 + - Account: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + capacity: 33 + max_trie_depth: 14 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 4 + shard_caps: + - 4000 + - comp_loader_params: + note: Header + max_height: 1 + shard_caps: + - 132 + - Storage: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + capacity: 33 + max_trie_depth: 13 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 4 + shard_caps: + - 4000 + - comp_loader_params: + note: Account + max_height: 2 + shard_caps: + - 33 + - Tx: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 22 + core_params: + chip_params: + max_data_byte_len: 330000 + max_access_list_len: 131072 + enable_types: + - true + - true + - true + capacity: 1 + max_trie_depth: 6 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 4 + shard_caps: + - 4000 + - comp_loader_params: + note: Header + max_height: 1 + shard_caps: + - 132 + - Receipt: + Node: + k: 21 + num_children: 1 + child_intent: + Node: + k: 22 + num_children: 1 + child_intent: + Leaf: + k: 22 + core_params: + chip_params: + max_data_byte_len: 1024 + max_log_num: 400 + topic_num_bounds: + - 0 + - 4 + capacity: 1 + max_trie_depth: 6 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 4 + shard_caps: + - 4000 + - comp_loader_params: + note: Header + max_height: 1 + shard_caps: + - 132 + - SolidityMapping: + Node: + k: 21 + num_children: 2 + child_intent: + Node: + k: 21 + num_children: 2 + child_intent: + Leaf: + k: 21 + core_params: + capacity: 32 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 4 + shard_caps: + - 4000 + - comp_loader_params: + note: Storage + max_height: 2 + shard_caps: + - 33 + - ResultsRoot: + Leaf: + k: 22 + core_params: + enabled_types: + - false + - true + - true + - true + - true + - true + - true + capacity: 128 + loader_params: + - comp_loader_params: + note: intent_keccak + max_height: 4 + shard_caps: + - 4000 + - params_per_component: + axiom-query:ComponentTypeHeaderSubquery: + max_height: 1 + shard_caps: + - 132 + - 132 + axiom-query:ComponentTypeAccountSubquery: + max_height: 2 + shard_caps: + - 33 + - 33 + - 33 + - 33 + axiom-query:ComponentTypeStorageSubquery: + max_height: 2 + shard_caps: + - 33 + - 33 + - 33 + - 33 + axiom-query:ComponentTypeTxSubquery: + max_height: 2 + shard_caps: + - 1 + - 1 + - 1 + - 1 + axiom-query:ComponentTypeReceiptSubquery: + max_height: 0 + shard_caps: + - 1 + axiom-query:ComponentTypeSolidityNestedMappingSubquery: + max_height: 2 + shard_caps: + - 32 + - 32 + - 32 + - 32 diff --git a/axiom-query/configs/production/all_small.yml b/axiom-query/configs/production/all_small.yml new file mode 100644 index 00000000..c36fb75d --- /dev/null +++ b/axiom-query/configs/production/all_small.yml @@ -0,0 +1,192 @@ +# The total subquery capacity is 32 +AxiomAgg2: + k: 23 + force_params: + num_advice: 1 + num_lookup_advice: 1 + num_fixed: 1 + axiom_agg1_intent: + k: 22 + force_params: + num_advice: 13 + num_lookup_advice: 2 + num_fixed: 1 + intent_verify_compute: + k: 22 + core_params: + subquery_results_capacity: 32 + svk: "0100000000000000000000000000000000000000000000000000000000000000" + client_metadata: + version: 0 + numAdvicePerPhase: [4] + numLookupAdvicePerPhase: [1] + numRlcColumns: 0 + numFixed: 1 + numInstance: [2304] + numChallenge: [0] + maxOutputs: 128 + isAggregation: false + preprocessed_len: 13 + loader_params: + comp_loader_params: # keccak + max_height: 1 + shard_caps: + - 4000 + intent_keccak: + Node: + k: 21 + num_children: 1 + child_intent: + Leaf: + k: 20 + core_params: + capacity: 4000 + intent_subquery_agg: + k: 21 + deps: + - Header: + Leaf: + k: 21 + core_params: + max_extra_data_bytes: 32 + capacity: 33 + loader_params: + comp_loader_params: # keccak + max_height: 1 + shard_caps: + - 4000 + + - Account: + Leaf: + k: 21 + core_params: + capacity: 9 + max_trie_depth: 14 + loader_params: + - comp_loader_params: # keccak + max_height: 1 + shard_caps: + - 4000 + - comp_loader_params: # header + max_height: 1 + shard_caps: + - 33 + + - Storage: + Leaf: + k: 21 + core_params: + capacity: 9 + max_trie_depth: 13 + loader_params: + - comp_loader_params: # keccak + max_height: 1 + shard_caps: + - 4000 + - comp_loader_params: # account + max_height: 1 + shard_caps: + - 9 + - Tx: + Leaf: + k: 21 + core_params: + chip_params: + max_data_byte_len: 8192 + max_access_list_len: 4096 + enable_types: + - true + - true + - true + capacity: 8 + max_trie_depth: 6 + loader_params: + - comp_loader_params: # keccak + max_height: 1 + shard_caps: + - 4000 + - comp_loader_params: # header + max_height: 1 + shard_caps: + - 33 + + - Receipt: + Leaf: + k: 21 + core_params: + chip_params: + max_data_byte_len: 800 + max_log_num: 20 + topic_num_bounds: + - 0 + - 4 + capacity: 8 + max_trie_depth: 6 + loader_params: + - comp_loader_params: # keccak + max_height: 1 + shard_caps: + - 4000 + - comp_loader_params: # header + max_height: 1 + shard_caps: + - 33 + + - SolidityMapping: + Leaf: + k: 21 + core_params: + capacity: 8 + loader_params: + - comp_loader_params: # keccak + max_height: 1 + shard_caps: + - 4000 + - comp_loader_params: # storage + max_height: 1 + shard_caps: + - 9 + + - ResultsRoot: + Leaf: + k: 21 + core_params: + enabled_types: + - false # Null + - true # Header + - true # Account + - true # Storage + - true # Transaction + - true # Receipt + - true # SolidityNestedMapping + capacity: 32 + loader_params: + - comp_loader_params: # keccak + max_height: 1 + shard_caps: + - 4000 + - params_per_component: + "axiom-query:ComponentTypeHeaderSubquery": + max_height: 1 + shard_caps: + - 33 + "axiom-query:ComponentTypeAccountSubquery": + max_height: 1 + shard_caps: + - 9 + "axiom-query:ComponentTypeStorageSubquery": + max_height: 1 + shard_caps: + - 9 + "axiom-query:ComponentTypeTxSubquery": + max_height: 1 + shard_caps: + - 8 + "axiom-query:ComponentTypeReceiptSubquery": + max_height: 1 + shard_caps: + - 8 + "axiom-query:ComponentTypeSolidityNestedMappingSubquery": + max_height: 1 + shard_caps: + - 8 diff --git a/axiom-query/configs/templates/account.leaf.yml b/axiom-query/configs/templates/account.leaf.yml new file mode 100644 index 00000000..4c0e82a6 --- /dev/null +++ b/axiom-query/configs/templates/account.leaf.yml @@ -0,0 +1,15 @@ +Subquery: + Account: + Leaf: + k: 16 + core_params: + capacity: 33 + loader_params: + - comp_loader_params: # keccak + max_height: 0 + shard_caps: + - 200 + - comp_loader_params: # header + max_height: 0 + shard_caps: + - 33 diff --git a/axiom-query/configs/templates/axiom_agg_1.yml b/axiom-query/configs/templates/axiom_agg_1.yml new file mode 100644 index 00000000..a929458d --- /dev/null +++ b/axiom-query/configs/templates/axiom_agg_1.yml @@ -0,0 +1,104 @@ +AxiomAgg1: + k: 21 + force_params: + - num_advice: 13 + - num_lookup_advice: 2 + - num_fixed: 1 + intent_verify_compute: + k: 20 + core_params: + subquery_results_capacity: 1 + svk: "0100000000000000000000000000000000000000000000000000000000000000" + client_metadata: + version: 0 + numAdvicePerPhase: [4] + numLookupAdvicePerPhase: [1] + numRlcColumns: 0 + numFixed: 1 + numInstance: [2304] + numChallenge: [0] + maxOutputs: 128 + isAggregation: false + preprocessed_len: 13 + loader_params: + comp_loader_params: # keccak + max_height: 2 + shard_caps: + - 50 + intent_keccak: + Node: + k: 20 + num_children: 2 + child_intent: + Leaf: + k: 18 + core_params: + capacity: 50 + intent_subquery_agg: + k: 21 + deps: + - Header: + Node: + k: 20 + num_children: 2 + child_intent: + Leaf: + k: 20 + core_params: + max_extra_data_bytes: 32 + capacity: 1 + loader_params: + - comp_loader_params: # keccak + max_height: 2 + shard_caps: + - 50 + + - Receipt: + Leaf: + k: 20 + core_params: + chip_params: + max_data_byte_len: 512 + max_log_num: 20 + topic_num_bounds: + - 0 + - 4 + network: null + capacity: 1 + loader_params: + - comp_loader_params: # keccak + max_height: 2 + shard_caps: + - 50 + - comp_loader_params: # header + max_height: 1 + shard_caps: + - 1 + + - ResultsRoot: + Leaf: + k: 20 + core_params: + enabled_types: + - false # Null + - true # Header + - false # Account + - false # Storage + - false # Transaction + - true # Receipt + - false # SolidityNestedMapping + capacity: 1 + loader_params: + - comp_loader_params: # keccak + max_height: 2 + shard_caps: + - 50 + - params_per_component: + "axiom-query:ComponentTypeHeaderSubquery": + max_height: 1 + shard_caps: + - 1 + "axiom-query:ComponentTypeReceiptSubquery": + max_height: 1 + shard_caps: + - 1 diff --git a/axiom-query/configs/templates/axiom_agg_2.yml b/axiom-query/configs/templates/axiom_agg_2.yml new file mode 100644 index 00000000..25ec6996 --- /dev/null +++ b/axiom-query/configs/templates/axiom_agg_2.yml @@ -0,0 +1,106 @@ +AxiomAgg2: + k: 21 + force_params: + - num_advice: 1 + - num_lookup_advice: 1 + - num_fixed: 1 + axiom_agg1_intent: + k: 21 + intent_verify_compute: + k: 20 + core_params: + subquery_results_capacity: 1 + svk: "0100000000000000000000000000000000000000000000000000000000000000" + client_metadata: + version: 0 + numAdvicePerPhase: [4] + numLookupAdvicePerPhase: [1] + numRlcColumns: 0 + numFixed: 1 + numInstance: [2304] + numChallenge: [0] + maxOutputs: 128 + isAggregation: false + preprocessed_len: 13 + loader_params: + comp_loader_params: # keccak + max_height: 2 + shard_caps: + - 50 + intent_keccak: + Node: + k: 20 + num_children: 2 + child_intent: + Leaf: + k: 18 + core_params: + capacity: 50 + intent_subquery_agg: + k: 21 + deps: + - Header: + Node: + k: 20 + num_children: 2 + child_intent: + Leaf: + k: 20 + core_params: + max_extra_data_bytes: 32 + capacity: 1 + loader_params: + - comp_loader_params: # keccak + max_height: 2 + shard_caps: + - 50 + + - Receipt: + Leaf: + k: 20 + core_params: + chip_params: + max_data_byte_len: 512 + max_log_num: 20 + topic_num_bounds: + - 0 + - 4 + network: null + capacity: 1 + loader_params: + - comp_loader_params: # keccak + max_height: 2 + shard_caps: + - 50 + - comp_loader_params: # header + max_height: 1 + shard_caps: + - 1 + + - ResultsRoot: + Leaf: + k: 20 + core_params: + enabled_types: + - false # Null + - true # Header + - false # Account + - false # Storage + - false # Transaction + - true # Receipt + - false # SolidityNestedMapping + capacity: 1 + loader_params: + - comp_loader_params: # keccak + max_height: 2 + shard_caps: + - 50 + - params_per_component: + "axiom-query:ComponentTypeHeaderSubquery": + max_height: 1 + shard_caps: + - 1 + "axiom-query:ComponentTypeReceiptSubquery": + max_height: 1 + shard_caps: + - 1 diff --git a/axiom-query/configs/templates/header.leaf.yml b/axiom-query/configs/templates/header.leaf.yml new file mode 100644 index 00000000..e9302304 --- /dev/null +++ b/axiom-query/configs/templates/header.leaf.yml @@ -0,0 +1,12 @@ +Subquery: + Header: + Leaf: + k: 16 + core_params: + max_extra_data_bytes: 32 + capacity: 33 + loader_params: + comp_loader_params: # keccak + max_height: 0 + shard_caps: + - 1000 diff --git a/axiom-query/configs/templates/header.tree.yml b/axiom-query/configs/templates/header.tree.yml new file mode 100644 index 00000000..aea16810 --- /dev/null +++ b/axiom-query/configs/templates/header.tree.yml @@ -0,0 +1,20 @@ +# tree of depth 1 with 2 leaves +Subquery: + Header: + Node: + k: 20 + num_children: 1 + child_intent: + Node: + k: 20 + num_children: 1 + child_intent: + Leaf: + k: 18 + core_params: + max_extra_data_bytes: 32 + capacity: 16 + loader_params: + comp_loader_params: # keccak + max_height: 1 + shard_caps: [100, 100] diff --git a/axiom-query/configs/templates/keccak.leaf.yml b/axiom-query/configs/templates/keccak.leaf.yml new file mode 100644 index 00000000..b449fc15 --- /dev/null +++ b/axiom-query/configs/templates/keccak.leaf.yml @@ -0,0 +1,6 @@ +Keccak: + Leaf: + k: 18 + core_params: + capacity: 50 + capacity: 50 \ No newline at end of file diff --git a/axiom-query/configs/templates/keccak.tree.yml b/axiom-query/configs/templates/keccak.tree.yml new file mode 100644 index 00000000..d72158c1 --- /dev/null +++ b/axiom-query/configs/templates/keccak.tree.yml @@ -0,0 +1,9 @@ +Keccak: + Node: + k: 20 + num_children: 2 + child_intent: + Leaf: + k: 18 + core_params: + capacity: 50 \ No newline at end of file diff --git a/axiom-query/configs/templates/receipt.leaf.yml b/axiom-query/configs/templates/receipt.leaf.yml new file mode 100644 index 00000000..be46acda --- /dev/null +++ b/axiom-query/configs/templates/receipt.leaf.yml @@ -0,0 +1,22 @@ +Subquery: + Receipt: + Leaf: + k: 16 + core_params: + chip_params: + max_data_byte_len: 512 + max_log_num: 20 + topic_num_bounds: + - 0 + - 4 + network: null + capacity: 8 + loader_params: + - comp_loader_params: # keccak + max_height: 0 + shard_caps: + - 200 + - comp_loader_params: # header + max_height: 0 + shard_caps: + - 33 diff --git a/axiom-query/configs/templates/results_root.leaf.yml b/axiom-query/configs/templates/results_root.leaf.yml new file mode 100644 index 00000000..37aa8dcc --- /dev/null +++ b/axiom-query/configs/templates/results_root.leaf.yml @@ -0,0 +1,28 @@ +Subquery: + ResultsRoot: + Leaf: + k: 20 + core_params: + enabled_types: + - false # Null + - true # Header + - false # Account + - false # Storage + - false # Transaction + - true # Receipt + - false # SolidityNestedMapping + capacity: 1 + loader_params: + - comp_loader_params: # keccak + max_height: 2 + shard_caps: + - 50 + - params_per_component: + "axiom-query:ComponentTypeHeaderSubquery": + max_height: 1 + shard_caps: + - 1 + "axiom-query:ComponentTypeReceiptSubquery": + max_height: 1 + shard_caps: + - 1 diff --git a/axiom-query/configs/templates/soliditymapping.leaf.yml b/axiom-query/configs/templates/soliditymapping.leaf.yml new file mode 100644 index 00000000..e4bbe8a4 --- /dev/null +++ b/axiom-query/configs/templates/soliditymapping.leaf.yml @@ -0,0 +1,15 @@ +Subquery: + SolidityMapping: + Leaf: + k: 16 + core_params: + capacity: 33 + loader_params: + - comp_loader_params: # keccak + max_height: 0 + shard_caps: + - 1000 + - comp_loader_params: # storage + max_height: 1 + shard_caps: + - 1 diff --git a/axiom-query/configs/templates/storage.leaf.yml b/axiom-query/configs/templates/storage.leaf.yml new file mode 100644 index 00000000..3b0b5d1f --- /dev/null +++ b/axiom-query/configs/templates/storage.leaf.yml @@ -0,0 +1,15 @@ +Subquery: + Storage: + Leaf: + k: 16 + core_params: + capacity: 33 + loader_params: + - comp_loader_params: # keccak + max_height: 0 + shard_caps: + - 1000 + - comp_loader_params: # account + max_height: 1 + shard_caps: + - 1 diff --git a/axiom-query/configs/templates/subquery_agg.yml b/axiom-query/configs/templates/subquery_agg.yml new file mode 100644 index 00000000..4711320d --- /dev/null +++ b/axiom-query/configs/templates/subquery_agg.yml @@ -0,0 +1,36 @@ +SubqueryAgg: + k: 21 + deps: + - Header: + Leaf: + k: 20 + core_params: + max_extra_data_bytes: 32 + capacity: 33 + loader_params: + - comp_loader_params: # keccak + max_height: 0 + shard_caps: + - 1000 + + - Receipt: + Leaf: + k: 20 + core_params: + chip_params: + max_data_byte_len: 512 + max_log_num: 20 + topic_num_bounds: + - 0 + - 4 + network: null + capacity: 8 + loader_params: + - comp_loader_params: # keccak + max_height: 0 + shard_caps: + - 1000 + - comp_loader_params: # header + max_height: 0 + shard_caps: + - 33 diff --git a/axiom-query/configs/templates/verifycompute.yml b/axiom-query/configs/templates/verifycompute.yml new file mode 100644 index 00000000..8de548d0 --- /dev/null +++ b/axiom-query/configs/templates/verifycompute.yml @@ -0,0 +1,21 @@ +VerifyCompute: + k: 16 + core_params: + subquery_results_capacity: 1 + svk: "0100000000000000000000000000000000000000000000000000000000000000" + client_metadata: + version: 0 + numAdvicePerPhase: [4] + numLookupAdvicePerPhase: [1] + numRlcColumns: 0 + numFixed: 1 + numInstance: [2304] + numChallenge: [0] + maxOutputs: 128 + isAggregation: false + preprocessed_len: 13 + loader_params: + comp_loader_params: # keccak + max_height: 0 + shard_caps: + - 100 diff --git a/axiom-eth/configs/tests/batch_query/account_3_6_0.json b/axiom-query/configs/test/axiom_aggregation1_for_agg.json similarity index 79% rename from axiom-eth/configs/tests/batch_query/account_3_6_0.json rename to axiom-query/configs/test/axiom_aggregation1_for_agg.json index 5250cfe2..f77c87e4 100644 --- a/axiom-eth/configs/tests/batch_query/account_3_6_0.json +++ b/axiom-query/configs/test/axiom_aggregation1_for_agg.json @@ -1,22 +1,21 @@ { "params": { "degree": 22, - "num_advice": 8, + "num_advice": 9, "num_lookup_advice": 1, "num_fixed": 1, "lookup_bits": 21 }, "break_points": [ [ - 4194292, + 4194294, 4194293, 4194294, 4194294, 4194294, 4194293, + 4194294, 4194294 - ], - [], - [] + ] ] } \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_17_7_for_evm_1.json b/axiom-query/configs/test/axiom_aggregation2.json similarity index 91% rename from axiom-eth/configs/headers/mainnet_17_7_for_evm_1.json rename to axiom-query/configs/test/axiom_aggregation2.json index b3e69eea..62868f16 100644 --- a/axiom-eth/configs/headers/mainnet_17_7_for_evm_1.json +++ b/axiom-query/configs/test/axiom_aggregation2.json @@ -7,8 +7,6 @@ "lookup_bits": 22 }, "break_points": [ - [], - [], [] ] } \ No newline at end of file diff --git a/axiom-query/configs/test/header_subquery.json b/axiom-query/configs/test/header_subquery.json new file mode 100644 index 00000000..ce0bfdb0 --- /dev/null +++ b/axiom-query/configs/test/header_subquery.json @@ -0,0 +1,39 @@ +{ + "params": { + "base": { + "k": 18, + "num_advice_per_phase": [ + 12, + 1 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 1, + 0 + ], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 1 + }, + "break_points": { + "base": [ + [ + 262134, + 262134, + 262133, + 262133, + 262133, + 262134, + 262132, + 262134, + 262133, + 262134, + 262133 + ], + [] + ], + "rlc": [] + } +} \ No newline at end of file diff --git a/axiom-query/configs/test/header_subquery_core_params.json b/axiom-query/configs/test/header_subquery_core_params.json new file mode 100644 index 00000000..33b4acb2 --- /dev/null +++ b/axiom-query/configs/test/header_subquery_core_params.json @@ -0,0 +1,4 @@ +{ + "max_extra_data_bytes": 32, + "capacity": 3 +} \ No newline at end of file diff --git a/axiom-query/configs/test/header_subquery_for_agg.json b/axiom-query/configs/test/header_subquery_for_agg.json new file mode 100644 index 00000000..ce0bfdb0 --- /dev/null +++ b/axiom-query/configs/test/header_subquery_for_agg.json @@ -0,0 +1,39 @@ +{ + "params": { + "base": { + "k": 18, + "num_advice_per_phase": [ + 12, + 1 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 1, + 0 + ], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 1 + }, + "break_points": { + "base": [ + [ + 262134, + 262134, + 262133, + 262133, + 262133, + 262134, + 262132, + 262134, + 262133, + 262134, + 262133 + ], + [] + ], + "rlc": [] + } +} \ No newline at end of file diff --git a/axiom-query/configs/test/header_subquery_loader_params.json b/axiom-query/configs/test/header_subquery_loader_params.json new file mode 100644 index 00000000..e9fff93e --- /dev/null +++ b/axiom-query/configs/test/header_subquery_loader_params.json @@ -0,0 +1,8 @@ +{ + "comp_loader_params": { + "max_height": 3, + "shard_caps": [ + 200 + ] + } +} \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_13_7.json b/axiom-query/configs/test/keccak_for_agg.json similarity index 77% rename from axiom-eth/configs/headers/mainnet_13_7.json rename to axiom-query/configs/test/keccak_for_agg.json index 213a02e9..d379ae41 100644 --- a/axiom-eth/configs/headers/mainnet_13_7.json +++ b/axiom-query/configs/test/keccak_for_agg.json @@ -8,24 +8,22 @@ }, "break_points": [ [ + 1048565, 1048566, + 1048565, 1048566, 1048565, 1048565, + 1048565, 1048564, 1048566, - 1048564, 1048565, 1048566, - 1048565, - 1048564, - 1048564, - 1048564, + 1048566, 1048565, 1048566, - 1048566 - ], - [], - [] + 1048565, + 1048564 + ] ] } \ No newline at end of file diff --git a/axiom-query/configs/test/results_root.json b/axiom-query/configs/test/results_root.json new file mode 100644 index 00000000..97ed11d1 --- /dev/null +++ b/axiom-query/configs/test/results_root.json @@ -0,0 +1,58 @@ +{ + "params": { + "base": { + "k": 18, + "num_advice_per_phase": [ + 31, + 1 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 1, + 0 + ], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 1 + }, + "break_points": { + "base": [ + [ + 262134, + 262134, + 262133, + 262132, + 262134, + 262134, + 262132, + 262134, + 262132, + 262134, + 262132, + 262132, + 262134, + 262132, + 262134, + 262132, + 262134, + 262132, + 262134, + 262132, + 262134, + 262134, + 262134, + 262132, + 262134, + 262133, + 262132, + 262134, + 262134, + 262134 + ], + [] + ], + "rlc": [] + } +} \ No newline at end of file diff --git a/axiom-query/configs/test/results_root_for_agg.json b/axiom-query/configs/test/results_root_for_agg.json new file mode 100644 index 00000000..e1cb6209 --- /dev/null +++ b/axiom-query/configs/test/results_root_for_agg.json @@ -0,0 +1,38 @@ +{ + "params": { + "base": { + "k": 18, + "num_advice_per_phase": [ + 11, + 1 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 1, + 0 + ], + "lookup_bits": 8, + "num_instance_columns": 1 + }, + "num_rlc_columns": 1 + }, + "break_points": { + "base": [ + [ + 262134, + 262134, + 262133, + 262133, + 262133, + 262134, + 262132, + 262134, + 262133, + 262134 + ], + [] + ], + "rlc": [] + } +} \ No newline at end of file diff --git a/axiom-eth/configs/headers/mainnet_14_7.json b/axiom-query/configs/test/subquery_aggregation_for_agg.json similarity index 84% rename from axiom-eth/configs/headers/mainnet_14_7.json rename to axiom-query/configs/test/subquery_aggregation_for_agg.json index 981bb2f5..78d0d73b 100644 --- a/axiom-eth/configs/headers/mainnet_14_7.json +++ b/axiom-query/configs/test/subquery_aggregation_for_agg.json @@ -1,35 +1,31 @@ { "params": { "degree": 20, - "num_advice": 21, + "num_advice": 19, "num_lookup_advice": 3, "num_fixed": 1, "lookup_bits": 19 }, "break_points": [ [ + 1048565, 1048566, 1048566, 1048566, - 1048566, + 1048564, 1048565, 1048566, - 1048564, 1048565, 1048566, 1048565, 1048564, - 1048565, - 1048565, - 1048565, 1048566, 1048564, 1048566, 1048565, 1048564, + 1048566, 1048566 - ], - [], - [] + ] ] } \ No newline at end of file diff --git a/axiom-query/configs/test/verify_compute_for_agg.json b/axiom-query/configs/test/verify_compute_for_agg.json new file mode 100644 index 00000000..14ea0c43 --- /dev/null +++ b/axiom-query/configs/test/verify_compute_for_agg.json @@ -0,0 +1,46 @@ +{ + "params": { + "base": { + "k": 19, + "num_advice_per_phase": [ + 19, + 1 + ], + "num_fixed": 1, + "num_lookup_advice_per_phase": [ + 1, + 1, + 0 + ], + "lookup_bits": 18, + "num_instance_columns": 1 + }, + "num_rlc_columns": 1 + }, + "break_points": { + "base": [ + [ + 524278, + 524278, + 524278, + 524277, + 524277, + 524278, + 524277, + 524276, + 524277, + 524278, + 524277, + 524278, + 524276, + 524278, + 524277, + 524277, + 524276, + 524277 + ], + [] + ], + "rlc": [] + } +} \ No newline at end of file diff --git a/axiom-query/data/test/axiom_aggregation1_for_agg.snark.json b/axiom-query/data/test/axiom_aggregation1_for_agg.snark.json new file mode 100644 index 00000000..d0afd5c9 --- /dev/null +++ b/axiom-query/data/test/axiom_aggregation1_for_agg.snark.json @@ -0,0 +1 @@ +{"inner":"FgAAAAAAAAAAAEAAAAAAAEGwKWiEsB9eqk4Yqqdz0iJXfmpA1Z7P8aTU90+xTWQw3r4mgxZU1k0b6RZ/NMXO/8VHOZLXD7OhEU5R5hpfyRhkORpNIFvWfDeTdBVwvZnhIEgyW4deaLHHuE7jH1dPEwAXAAAAAAAAAD2PumVP6pqldS0PKWW8Q4IvnE/rvtm5/2fXvG196sYhB0h1WkJ/cKWfqkn59UkIeWkIHInqDJ5elCw88GiL2SQ8xgadkPBgKs9Ez6A5pLigxRoUWDKXaDgr7VskH5t1LR3bKcHa6ybj6a0ry7TdlFEF/WBqI+mu9GvSQpgOZKkFVWZyRrG9Na+r0elaHOSMk3+C4OvcTPdhYKVHgUxv0G/rr43b14EV57L+QhvTOVF/92WR305UX42vm6GjM7B3Bf0pp3KCOBUydlyDWepNtKHWF8pHAO6wMaDZxq9w7zYdYRtr9jIk6Z/ck9Fnmgi30vCcgdnLAIGbDgpE9xXhGE8rZBbm4ilIw8+nSXFBPhOH2/AakzOnAPWZWi4Qkd8IE6zi66Y/cSEiQsvATSb9YCprihQDQCI6synWa0UyV1xYIdYnzkBTgEcFXj4PbmqU/BJIQYJK4QYxe8z6gMh26EA5gGOi8pxeQRjm0LHZ9ZF7msFsISfgIepudgkVCVtvbBEgjU9mHFbeeWaZh6LNJ9HKyAwxH2FyymcQBG4CTHJmkyXc7kbUzz6QdZondkz/mxb3yuR8JPFqlyMvtCCqwSFfEhZPz59hZkwjiSO3IiNJb3RvCxhu45WGp32sgP8dIhd6ZK41KpLj3dCcbeY6HpOwLh64qq/h3wbKGFsyHD9Wtxngk4Niv6djmanaUt+0ZXg9IjMk5we7Tmb6QPpe30dj0CITj5xKMsv5H3Ie1AObXjul9EKGBp56JX+EBdofQMgyywKAzCxB0J4xkl6PZ+D1j8KU2Htcs9c7LPlPM/5c639kULTWXnlyOveMq2ch5umfMrQNPU9VCnU1taAP0FjU70x02Nyz9nkPkBt098JGiIo+VGVHeGFCesv4Ho/4GxufF4WP6aB0GXZ0s53JP9FvixmsEh6DsgKGIjfDU2tSjikxImkI9UYyV5vDtRAeCnCU75Q6g1Ip3QFsM4B4XEkBAAAAAAAAABYAAAAAAAAAAwAAAAAAAAAKAAAAAAAAAAIAAAAAAAAACAAAAAAAAAADAAAAAAAAAAEAAAAAAAAAAgAAAAAAAAABAAAAAAAAAFMAAAAAAAAAGAAAAAAAAAAAAAAAGAAAAAAAAAABAAAAGAAAAAAAAAACAAAAGAAAAAAAAAADAAAAGQAAAAAAAAAAAAAAGQAAAAAAAAABAAAAGQAAAAAAAAACAAAAGQAAAAAAAAADAAAAGgAAAAAAAAAAAAAAGgAAAAAAAAABAAAAGgAAAAAAAAACAAAAGgAAAAAAAAADAAAAGwAAAAAAAAAAAAAAGwAAAAAAAAABAAAAGwAAAAAAAAACAAAAGwAAAAAAAAADAAAAHAAAAAAAAAAAAAAAHAAAAAAAAAABAAAAHAAAAAAAAAACAAAAHAAAAAAAAAADAAAAHQAAAAAAAAAAAAAAHQAAAAAAAAABAAAAHQAAAAAAAAACAAAAHQAAAAAAAAADAAAAHgAAAAAAAAAAAAAAHgAAAAAAAAABAAAAHgAAAAAAAAACAAAAHgAAAAAAAAADAAAAHwAAAAAAAAAAAAAAHwAAAAAAAAABAAAAHwAAAAAAAAACAAAAHwAAAAAAAAADAAAAIAAAAAAAAAAAAAAAIAAAAAAAAAABAAAAIAAAAAAAAAACAAAAIAAAAAAAAAADAAAAIQAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAwAAAAAAAAAAAAAABAAAAAAAAAAAAAAABQAAAAAAAAAAAAAABgAAAAAAAAAAAAAABwAAAAAAAAAAAAAACAAAAAAAAAAAAAAACQAAAAAAAAAAAAAACgAAAAAAAAAAAAAAKwAAAAAAAAAAAAAACwAAAAAAAAAAAAAADAAAAAAAAAAAAAAADQAAAAAAAAAAAAAADgAAAAAAAAAAAAAADwAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEQAAAAAAAAAAAAAAEgAAAAAAAAAAAAAAEwAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAFQAAAAAAAAAAAAAAFgAAAAAAAAAAAAAAJAAAAAAAAAAAAAAAJAAAAAAAAAABAAAAJAAAAAAAAAD5////JQAAAAAAAAAAAAAAJQAAAAAAAAABAAAAJQAAAAAAAAD5////JgAAAAAAAAAAAAAAJgAAAAAAAAABAAAAJgAAAAAAAAD5////JwAAAAAAAAAAAAAAJwAAAAAAAAABAAAAJwAAAAAAAAD5////KAAAAAAAAAAAAAAAKAAAAAAAAAABAAAAKAAAAAAAAAD5////KQAAAAAAAAAAAAAAKQAAAAAAAAABAAAAKgAAAAAAAAAAAAAAKgAAAAAAAAABAAAAIgAAAAAAAAAAAAAAIgAAAAAAAAD/////IwAAAAAAAAAAAAAAVAAAAAAAAAAYAAAAAAAAAAAAAAAYAAAAAAAAAAEAAAAYAAAAAAAAAAIAAAAYAAAAAAAAAAMAAAAZAAAAAAAAAAAAAAAZAAAAAAAAAAEAAAAZAAAAAAAAAAIAAAAZAAAAAAAAAAMAAAAaAAAAAAAAAAAAAAAaAAAAAAAAAAEAAAAaAAAAAAAAAAIAAAAaAAAAAAAAAAMAAAAbAAAAAAAAAAAAAAAbAAAAAAAAAAEAAAAbAAAAAAAAAAIAAAAbAAAAAAAAAAMAAAAcAAAAAAAAAAAAAAAcAAAAAAAAAAEAAAAcAAAAAAAAAAIAAAAcAAAAAAAAAAMAAAAdAAAAAAAAAAAAAAAdAAAAAAAAAAEAAAAdAAAAAAAAAAIAAAAdAAAAAAAAAAMAAAAeAAAAAAAAAAAAAAAeAAAAAAAAAAEAAAAeAAAAAAAAAAIAAAAeAAAAAAAAAAMAAAAfAAAAAAAAAAAAAAAfAAAAAAAAAAEAAAAfAAAAAAAAAAIAAAAfAAAAAAAAAAMAAAAgAAAAAAAAAAAAAAAgAAAAAAAAAAEAAAAgAAAAAAAAAAIAAAAgAAAAAAAAAAMAAAAhAAAAAAAAAAAAAAAkAAAAAAAAAAAAAAAkAAAAAAAAAAEAAAAlAAAAAAAAAAAAAAAlAAAAAAAAAAEAAAAmAAAAAAAAAAAAAAAmAAAAAAAAAAEAAAAnAAAAAAAAAAAAAAAnAAAAAAAAAAEAAAAoAAAAAAAAAAAAAAAoAAAAAAAAAAEAAAApAAAAAAAAAAAAAAApAAAAAAAAAAEAAAAoAAAAAAAAAPn///8nAAAAAAAAAPn///8mAAAAAAAAAPn///8lAAAAAAAAAPn///8kAAAAAAAAAPn///8qAAAAAAAAAAAAAAAiAAAAAAAAAAAAAAAjAAAAAAAAAAAAAAAiAAAAAAAAAP////8qAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAJAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAANAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAARAAAAAAAAAAAAAAASAAAAAAAAAAAAAAATAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAVAAAAAAAAAAAAAAAWAAAAAAAAAAAAAAAsAAAAAAAAAAAAAAArAAAAAAAAAAAAAAABAAAAAAAAAAgAAAAbAAAAAAAAAAYAAAACAAAAAgAAAAAAAAAAAAAABQAAAAUAAAACAAAAGAAAAAAAAAAAAAAABgAAAAIAAAAYAAAAAAAAAAEAAAACAAAAGAAAAAAAAAACAAAABAAAAAIAAAAYAAAAAAAAAAMAAAAGAAAAAgAAAAMAAAAAAAAAAAAAAAUAAAAFAAAAAgAAABkAAAAAAAAAAAAAAAYAAAACAAAAGQAAAAAAAAABAAAAAgAAABkAAAAAAAAAAgAAAAQAAAACAAAAGQAAAAAAAAADAAAABgAAAAIAAAAEAAAAAAAAAAAAAAAFAAAABQAAAAIAAAAaAAAAAAAAAAAAAAAGAAAAAgAAABoAAAAAAAAAAQAAAAIAAAAaAAAAAAAAAAIAAAAEAAAAAgAAABoAAAAAAAAAAwAAAAYAAAACAAAABQAAAAAAAAAAAAAABQAAAAUAAAACAAAAGwAAAAAAAAAAAAAABgAAAAIAAAAbAAAAAAAAAAEAAAACAAAAGwAAAAAAAAACAAAABAAAAAIAAAAbAAAAAAAAAAMAAAAGAAAAAgAAAAYAAAAAAAAAAAAAAAUAAAAFAAAAAgAAABwAAAAAAAAAAAAAAAYAAAACAAAAHAAAAAAAAAABAAAAAgAAABwAAAAAAAAAAgAAAAQAAAACAAAAHAAAAAAAAAADAAAABgAAAAIAAAAHAAAAAAAAAAAAAAAFAAAABQAAAAIAAAAdAAAAAAAAAAAAAAAGAAAAAgAAAB0AAAAAAAAAAQAAAAIAAAAdAAAAAAAAAAIAAAAEAAAAAgAAAB0AAAAAAAAAAwAAAAYAAAACAAAACAAAAAAAAAAAAAAABQAAAAUAAAACAAAAHgAAAAAAAAAAAAAABgAAAAIAAAAeAAAAAAAAAAEAAAACAAAAHgAAAAAAAAACAAAABAAAAAIAAAAeAAAAAAAAAAMAAAAGAAAAAgAAAAkAAAAAAAAAAAAAAAUAAAAFAAAAAgAAAB8AAAAAAAAAAAAAAAYAAAACAAAAHwAAAAAAAAABAAAAAgAAAB8AAAAAAAAAAgAAAAQAAAACAAAAHwAAAAAAAAADAAAABgAAAAIAAAAKAAAAAAAAAAAAAAAFAAAABQAAAAIAAAAgAAAAAAAAAAAAAAAGAAAAAgAAACAAAAAAAAAAAQAAAAIAAAAgAAAAAAAAAAIAAAAEAAAAAgAAACAAAAAAAAAAAwAAAAYAAAABAAAAAQAAAAAAAAAFAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAIAAAAkAAAAAAAAAAAAAAAGAAAAAQAAAAEAAAD5////BQAAAAYAAAACAAAAKQAAAAAAAAAAAAAAAgAAACkAAAAAAAAAAAAAAAQAAAACAAAAKQAAAAAAAAAAAAAABgAAAAEAAAABAAAAAAAAAAUAAAACAAAAJQAAAAAAAAAAAAAABAAAAAIAAAAkAAAAAAAAAPn///8GAAAAAQAAAAEAAAAAAAAABQAAAAIAAAAmAAAAAAAAAAAAAAAEAAAAAgAAACUAAAAAAAAA+f///wYAAAABAAAAAQAAAAAAAAAFAAAAAgAAACcAAAAAAAAAAAAAAAQAAAACAAAAJgAAAAAAAAD5////BgAAAAEAAAABAAAAAAAAAAUAAAACAAAAKAAAAAAAAAAAAAAABAAAAAIAAAAnAAAAAAAAAPn///8GAAAAAQAAAAEAAAAAAAAABQAAAAIAAAApAAAAAAAAAAAAAAAEAAAAAgAAACgAAAAAAAAA+f///wYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAAAkAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAAAQAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAALAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAAYAAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAAAwAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAACQAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAABAAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAAGAAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAAoukz5btWDoclP5ZejolfW3FuyNSqJuxkyvDGIm5rIgkBAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAAAlAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAAGQAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAANAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAAaAAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAAA4AAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAACUAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAAZAAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAAArNxDfun0nPHO9gaKHe0IRwpiPA4Fg0f4V6S/o1GCzEwEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAAGgAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAAUj4xq5sTidFOhU28jtFb1tSSpHbgwT9nApO96SPfrxgBAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAAAmAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAAGwAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAAPAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAAcAAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAABAAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAACYAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAAbAAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAABC+4rasD/Dz8HxuI/kVUHnKOJwsy+4tz7Ipq8TujahAAEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAAHAAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAAnWkIkL756kQ9WGR4B9nzHyWhAdKNBZLUF19Uzg11uS4BAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAAAnAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAAHQAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAARAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAAeAAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAABIAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAACcAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAAdAAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAAAbQnFsr2M4U3LluNu4dr5N1VF8eCK3ev/6jElvkw5wDAEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAAHgAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAAS0hg2BrMj6FjByaMt1naWeNn9soexvLrRi7mtwaVgiABAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAAAoAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAAHwAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAATAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAAgAAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAABQAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAACgAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAAfAAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAAAn5TBIsH5NStIzBSS+EDLLioLaBlnTMOOp6oSDQSByEgEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAAIAAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAAFOAujNwn9ZgMFqDjXi6h/seM1vkURQrMkV7D9vgJRS4BAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAAApAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAAIQAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAAVAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAAXAAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAABYAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAACkAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAAhAAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAAAHZhgcDfiby3j6OKvK320SbAqjNEgcjfGiQBBxOFIIIwEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAAFwAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAA4MAjESpKAW+jY3rCE49V6RziSZIa0RzJe5UqUyRkyREBAAAAAAAAAAMAAAACAAAAAAAAAAYAAAABAAAAAQAAAAAAAAAFAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAIAAAAqAAAAAAAAAAAAAAAGAAAAAQAAAAEAAAD5////BQAAAAYAAAACAAAAKgAAAAAAAAAAAAAAAgAAACoAAAAAAAAAAAAAAAQAAAACAAAAKgAAAAAAAAAAAAAABgAAAAUAAAAFAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAEAAAABAAAA+f///wQAAAAFAAAABQAAAAUAAAAFAAAABQAAAAEAAAABAAAA+v///wEAAAABAAAA+////wEAAAABAAAA/P///wEAAAABAAAA/f///wEAAAABAAAA/v///wEAAAABAAAA/////wUAAAAGAAAABgAAAAIAAAAqAAAAAAAAAAEAAAAFAAAAAgAAACIAAAAAAAAAAAAAAAMAAAABAAAAAAAAAAUAAAACAAAAIwAAAAAAAAAAAAAAAwAAAAIAAAAAAAAABAAAAAYAAAAGAAAAAgAAACoAAAAAAAAAAAAAAAUAAAAIAAAAAQAAAAAAAAACAAAAIQAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAwAAAAEAAAAAAAAABQAAAAgAAAABAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAGAAAAAQAAAAEAAAAAAAAABQAAAAIAAAAiAAAAAAAAAAAAAAAEAAAAAgAAACMAAAAAAAAAAAAAAAYAAAAGAAAABQAAAAUAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAQAAAAEAAAD5////BAAAAAUAAAAFAAAABQAAAAUAAAAFAAAAAQAAAAEAAAD6////AQAAAAEAAAD7////AQAAAAEAAAD8////AQAAAAEAAAD9////AQAAAAEAAAD+////AQAAAAEAAAD/////BQAAAAIAAAAiAAAAAAAAAAAAAAAEAAAAAgAAACMAAAAAAAAAAAAAAAUAAAACAAAAIgAAAAAAAAAAAAAABAAAAAIAAAAiAAAAAAAAAP////8DAAAAAwAAAAAAAAABP3v50FmOsef2XsidQhNZ7iTM4wiEFkwyck0RXaZ7MSgAAAEAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAkAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAACwAAAAAAAAABAAAAAAAAABYAAAAAAAAAWCXHDGzEOL5rWJEAAAAAAAAAAAAAAAAAAAAAAAAAAAC6Xif+4+q3JUvyyAAAAAAAAAAAAAAAAAAAAAAAAAAAAOblNHIw3ID0sykAAAAAAAAAAAAAAAAAAAAAAAAAAAAA29ufJGljxEZSgl4AAAAAAAAAAAAAAAAAAAAAAAAAAADxTgpFFtL/Pm1lgwAAAAAAAAAAAAAAAAAAAAAAAAAAAOu0pzhyiI4CgSQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmU8xCoDcHdRQ+n0AAAAAAAAAAAAAAAAAAAAAAAAAAAANIqpgRDzQ26+Z2wAAAAAAAAAAAAAAAAAAAAAAAAAAACOEM+0OVxvDoQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAduJ52IOKL05Wc5QAAAAAAAAAAAAAAAAAAAAAAAAAAAAF0gEoofwJeQli4gAAAAAAAAAAAAAAAAAAAAAAAAAAAG08jGHpqusCwBMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmwQcs/eysPnDCxaZpefRjAAAAAAAAAAAAAAAAAAAAABIo34zgNedbINrC7t8QLEAAAAAAAAAAAAAAAAAAAAAAD0Q2BWn6hxdw34nOvkGjswAAAAAAAAAAAAAAAAAAAAAkWO+s2HBNzISCbQXteWQyAAAAAAAAAAAAAAAAAAAAAAlfyxQG2OUovVnsgh88Vi0AAAAAAAAAAAAAAAAAAAAAOUKUIeiV/ReeAKsJMa2CUQAAAAAAAAAAAAAAAAAAAACsYWP9ABWbsxZgwJImHBu5AAAAAAAAAAAAAAAAAAAAAPHDKvwe6JUUpuMI3HjwTc4AAAAAAAAAAAAAAAAAAAAAdghdtWkHYT1DuqLEGFXqZPJVy2htttlx2BzpQ1uFGCeADQAAAAAAAJSbmaIE+vL89Oegq602L3rlUy4n1xEjDHV73pq9I9oZbTJwEQkaTSsTnIcwSGroDTw9xoMO25vtJUUmVh0JPHDWDKhBDHFDnz5Z0l0zxQpcFDvCtHifc12auwreOZ+IWfl057BuiNbbNPkeEiSonE5NUMQMz8UoXlqkHw+AWNIVBbxdmMhNELyo02gKNrkw/HXG5XN9SdhDhhusmqRN/w3YQwEWkLBhIBWY67Hu7RBZyYR5ZiJuCEDIWQKDe5QvSS5ZmLXcoIZZrVFcy7ObOtsT2aygKD+IZPok//vm4Iko0YeZsmZYMo9zEQvv3cmMTBdPHIM1MCKkFsxzMRI+2AZTEan5UtAb7POsU6AV66NyWrVSdKeQYG+PSgf4sg7tGqhuo+90YoCda/q4qJW8Xc400T03V985oPUdlUVRRuEQ0e+m0PgnM0YKe6TfBZ9hzw/1ddz0uXHTW5lPYSmr4mXrAw6+5JAvtCoZnNrF0BosBsxtRpEfinpIsVi0VXfqLLFlRz/Sqzxvp2bpNoUertIM3yNQm1evjzzBvhUO13pHTe6d25fW0AE1uvWsGt1KQbo2GCWufGtj75HoHX6oYAW+kNHbhGhJMm672SW7d8QqcWhjdHcXiCzdvle2p+GBKg3qx5P/9L9dTXkRLFTTvjucUe2qFt+TBX8QdCZz8wRnN58g6Kr2ASxsUmNvI0AefmwH006PZi8wvCZyplePGgNYkydiyIssLnZP/BOwuZTlsNcHJqB4w5XiiNsMVws9BE41UjfCnH1X5YLbKNAW0olQ7HWvahgpwC2UCf0FNyRgKUTWK2O91SgoOStgl3hDRifSf80BebYYx2GBFRCtWEQ1cg0zWJxkVcSUObQU7MiZ5QgauxOGFAMYwUuoRyXOLzCZ2GGVLkig26HeUuB++0CukOzUBqCikd1YU9zUhqpeViLoEBwFYvkUzScvl4AfUUyWIDOApz1hsrsEzDpsmgncoyoWr/ddjFQhdN/bjgWWziZ4sSOk4LXDfxW0NKE9CCWUXuKcwPGeuW/y/VVPoDQ2uY3W4hKwVjiwXRwx50QuLjhkY0/XORCC2Ldi8ygDuoBbWLL27VtHxFYchTYoZB6++pzzhgNirtRcMbz2u7rX2YPi9+2mrgK8gGFUCsJZAVOMd5UBMAUt5nbM2qETFY7nLoEpyAH2ffwp/Nooey0SIySXojfslam9kNGdcYKARVa4Qz4J6xcHDBnSjZ6tfCNXTfOsd+JMuAH6uo1a4QyUoULYaiOcLFZzVZFeH8zUC9wh9aMzsouebBXV15YSfQmysRSkXoC4LTMHwRKsMLEY3fr5OcdEAnGwepD/i/id8Ww9UjZwDXEiNJaAPTB7FRzJkitMShsF4c+1UEEToleFiPGQTZiXY9inyagESI3FJj8+Xxl/yQb022RjW7GJdoshdc5BQSb7fbkQnaml73seZCuaIKmG3rjWMaR7bPzK/9LZkJNqkD7v1c/mGYkqqASsUX4O8mPayC3vppsVmIDoBfscWYzihQqnjdAlmOEwGqcXlIGrHOsBSfE8aA5IzBI2Gful4DuyUPgYqw1zWgIsYR5djXsI1dy9FMN87awdz7lsbrq/P36x951/ueDvJiwwP1BhWpkA8PUe8pOVyzwivs/WRE74Np+EHaOwEuf7KD9kGWV4FWkspFpK6kxYHEYizxfplEfhA3dJY3smj84ZZ/pcls9TIA5ZfZEpSieoUuWUtnC8g3ozjoEAFOTeyRCxi0kGU5O3cGaBgdA0rZUBmuZxBgmrmylDUbxJAfDQIqsXY9RGfIJh07It/bwmFgCogb4h0zClr/SPNFDNROIJKg92UsTY4+nYaMGM0EWp6sp989YhnpOKcjm62zGs5Ctxo69LD01n8wkH+ZZp995btbv1ym8C8/Hpxi6FkC+eLJBGPfmkyWk1cT3s3XGa7IjvVolXUFsEDxvJffhwqKYseyd6Vawewg5BHklmETWwXnXF/lRMQlKk1tpY4FXTDCFV0CENWd3gTFalyBCBdsWMPUS6puiLSXJyPrrT5FeSLnXzPSwXc8U0TGQCJDcyaIQ0gSzRa6t3qU9oZ9PgukwqwMpF1E2UL2wIST4k7pyPMbvlNHnUxz81pRLhbpAepyB4pS+O6sHi+DPEQV+j3kAYW+kxy2/11oqpqWXZaU+qEGgaRRNpoAefq7L48gajwW6EToiD4p1v+1Ll1yDXTQcLoP4jtBnDuMPZf3DR3Km4OmrAZjeq7NWTtXigvyeBMBVOVsuGwcSokg8OnyqjgwOHUKpZExIsn/6Gxbw1SjdMGkcVrAtiQvz5YRrBADg/Ngvmxyqa8f1IIVfNzdrJ0wQNNA4N9FuXfl2uPB3d++U8z6+SVdlKfCNR1Vk4oO+M2inuhtzEt/g4VNU0Zp+yCKnh+fXQHv5aC3Mjex2SgKULKUChstZz/Gg46Iv/5dgNqA0rfZgpUDe5RMLNLe2gPN4Zo5lSL35jG/C3kauzoAmhWQYRF0KRfaevrwRIyP5v0AEP1uZH6AX3o2lOGQ57NS3HIEPPLNgCa0sZWPDufaXKBoTi0sBar0eTDYD4Qvsp+r5HtG8XQANIz7Tdrxo/EUsbHHzNzzsr0nTfDt6qhcXr0/06l2DcMeFMP2Q6WlEFyhQ98lhKQrrrjr1+shIBKLO4wyPgSZfu+mOVayR/xO8bCaXrh5Poh+jEYICSFXr81S8gwdqJTNGBvei3EO5fQQQOTUu3bAv1sYWRxze3HEKfKKNhLfmrnJQvRGZDn9KsAiKYocR0/KEPFiwhdxdCDTa2uVgPjx9Z9IeYKsi44pxyFxcgz1/q7qXL4S/QEwfbAg2XkcjU9eL9oqeCO93wq1kdIw2ZmJe5xZ+y/s6FY+yuGKnN4hCtdj8/ufg5UBy2mBNFudWR7DyPvCFDt3YvT0Zrb4M7wJgClbe3U/H/7JCvEcUO9zUMKw546GJ+Zcg79uC5tWdBJCKnR61bIn0j+G8iewly3wEoq6qWS+J8YPqhkAefAXMn1iEeU9bdWa0QTRc5NrYfu/ZfXZFiqUi3P92eQZkLmrYy+2iVesuOEmCoGmZNLHg7czkQ1Pd/ppufWDZsakVjt0PK6haTpwwmjK0e0JCOEIGndOUGVE3fUhiSpqpr926SlbN9dsA+EpxbbxKv/oP4gLGpcoUQOqdavwzUmaHkCT8Hwfge1MsE2EdnK8NOk2cvQT48MD/t0U7OE70ewsk/lpHeSf98Sa8LO/cITQpmBiWVMxWQO5L9d0d++a/0siva8rj5xW3ga2A0WB6BzJy7k7reF8q7Q1vC/AsKiVJehqsMBmJjqoSJZUy+AZIRF/WJ1FBQWvS+X/fyYtVMvU/Ueo+mleQKm3X5XqgD+kq2Jm6ZrAm4zD7l3+CNtKv3Nh80yNc6dV68nom0whCVvBd79g8PjyTlP2HbMCUHqA8O32fUeI+j7yC0wDcmDklJcO59Y+9PmGVmBwaBThxi1RrlwM77jdmuy93Wo34WaV8/dMpySLYmvdrvew2p58z3Oww6+5PLoaMz6owQPxcZurgPnTHBKCKJqDhpul4r5OXhx7Xr8HvQb2mEDmYMAbtvLcmCp8PLowGo3opuRLWhQgv4lL5AN7M1LVtnmTgN18giQedrL5bqPwxQVOS9QpWlLSxIIvOba2whYTZ4KAiodMFBD/1QsrLtCdnA3u5snUcwsNJ/IZdRFsEHrqgyJmh91RVs7pYazDxcXTOrtUjOtoqQssucjdnMMNA8RKInCwi21FwH0lGZGWjSTjOIEoqLoHWodc+UsgsnbB/+cQ7WYWySuaaGG6Ll8fgruesEB3XP3uDanCXiBJT5GvvOBC/Au8e1VM6CoYDB5kWT3Hv80YaohBaFTQMTZxx0Cb8TC9RHqYtgIiTRmMEO7noQL6tZr8MqcSThTMzbzjkGtgbagPVKTgjTjWtkw8iPNh/Dmmx0pNAeBXUu3Hu2chOBD2L5Ter7Neq5lRypT8s8OQIOgjWDbKdwlb6hFkXI5VwccFKDhmZzsD/PEhXpyJR/O06e45DzLyxvUsrMWbYpIyFkGn+KpwtHGwAqPAiXqrSFF7gF5KfQ7nvMt4TGl7BaLFRQgemp9qO9VvB8QGSan9oX1WeolICqkDXvBxg/kYQnZhSV0os23htHD2JSuMcYK55ItNvpcRuOli85u4U69hwGd+C3Q+t8ywhhv5yveXJslcwRkBokGil8ivz380/vLM1+VxAF47L38Drp3SdfGyS057+oM5+0YrSmDih96Ukab8Qjix2HgR8tKkVcpEEU3XjWo3xIVhejrzSXmq2qeBTri3Adj01LbkoSbImfd6BJ2mKtGFOIsZLi49szgUyLJln1UjdSExdDLUuXmaNY+Ky9VPLSU2SgTzEzP5jwOXAfUdEWsXxrh0Aa3nM7KvihNEI0lqImT5oHxVhetCKgNxrM82vszzG236mOxSsUugFgCH8w9A2CfMFdF9GTLI1pJqDlI1lYyz4ly/IdCYQBiap4BwetmfL0gnLz8YoMTAkrQi9wezDiiomuWeLx/8A86b/7zmlJTyJZydOEcog/8gMNxNIu2qbErDKopeFv88EidHLaaewn9UFPWxmXlwIkBA==","aggVkHashIdx":9} \ No newline at end of file diff --git a/axiom-query/data/test/input_header_for_agg.json b/axiom-query/data/test/input_header_for_agg.json new file mode 100644 index 00000000..65886b56 --- /dev/null +++ b/axiom-query/data/test/input_header_for_agg.json @@ -0,0 +1,2157 @@ +{ + "mmr": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x348bab8f4d2acdd4022f4b7becb8ba36a260bd9f699aa4df0a0660dc772ddcfc", + "0xfff5804d8cd309c79ff26f19ecc6a9cef13556c9d5ddb18afae277d71ad7d411", + "0x4f1a2ab29b70f47ff5c88dc7048bb23fcc4a59fbcc3b9f0e9529297afb28cb8a", + "0x995310788441a017c79a1fc0edab3074e6d5495712b35e120c446301b8b60d8c", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xc4933b4a3d6a41bf6c8009e24436a19e3e91a6f4198a0f498ade1179d2978500", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xdff0f5a47ed23e4dd682fa5b20c861c8d616f0124bf3366def9f620e1569ade6", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x4755084f1c80a73395852c7ede586dddb3b8db7828e4ad2cf3259f693ac4f875", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + "requests": [ + { + "header_rlp": [ + 249, + 2, + 24, + 160, + 216, + 95, + 50, + 4, + 112, + 10, + 121, + 104, + 27, + 83, + 253, + 131, + 4, + 164, + 65, + 65, + 189, + 42, + 242, + 254, + 68, + 178, + 38, + 90, + 237, + 99, + 108, + 22, + 237, + 111, + 226, + 35, + 160, + 29, + 204, + 77, + 232, + 222, + 199, + 93, + 122, + 171, + 133, + 181, + 103, + 182, + 204, + 212, + 26, + 211, + 18, + 69, + 27, + 148, + 138, + 116, + 19, + 240, + 161, + 66, + 253, + 64, + 212, + 147, + 71, + 148, + 36, + 155, + 219, + 68, + 153, + 189, + 124, + 104, + 54, + 100, + 193, + 73, + 39, + 108, + 29, + 134, + 16, + 142, + 33, + 55, + 160, + 156, + 228, + 86, + 29, + 250, + 219, + 79, + 176, + 34, + 222, + 188, + 124, + 1, + 49, + 65, + 163, + 106, + 35, + 212, + 103, + 244, + 38, + 140, + 14, + 98, + 10, + 123, + 186, + 5, + 177, + 116, + 248, + 160, + 236, + 76, + 142, + 192, + 34, + 129, + 193, + 150, + 227, + 248, + 130, + 180, + 6, + 28, + 5, + 244, + 240, + 132, + 58, + 14, + 175, + 114, + 163, + 238, + 71, + 21, + 7, + 114, + 32, + 147, + 75, + 188, + 160, + 13, + 254, + 202, + 181, + 217, + 122, + 77, + 163, + 97, + 185, + 44, + 41, + 21, + 124, + 214, + 165, + 8, + 155, + 85, + 250, + 206, + 224, + 7, + 238, + 161, + 96, + 5, + 191, + 168, + 216, + 38, + 148, + 185, + 1, + 0, + 25, + 130, + 124, + 128, + 145, + 88, + 164, + 154, + 65, + 140, + 34, + 195, + 42, + 197, + 16, + 176, + 97, + 232, + 128, + 112, + 116, + 65, + 32, + 20, + 161, + 10, + 0, + 0, + 38, + 12, + 36, + 231, + 42, + 74, + 3, + 69, + 176, + 142, + 12, + 80, + 128, + 81, + 88, + 3, + 2, + 19, + 3, + 142, + 146, + 5, + 36, + 76, + 81, + 0, + 183, + 34, + 48, + 104, + 1, + 142, + 241, + 100, + 128, + 9, + 54, + 116, + 0, + 100, + 214, + 66, + 130, + 35, + 212, + 21, + 16, + 78, + 188, + 193, + 161, + 101, + 102, + 32, + 100, + 36, + 167, + 7, + 208, + 81, + 68, + 129, + 166, + 0, + 128, + 9, + 10, + 52, + 94, + 0, + 7, + 36, + 70, + 208, + 0, + 33, + 1, + 41, + 197, + 140, + 19, + 138, + 88, + 34, + 172, + 104, + 98, + 6, + 24, + 231, + 172, + 170, + 205, + 52, + 175, + 22, + 85, + 52, + 58, + 202, + 51, + 64, + 17, + 208, + 64, + 99, + 162, + 146, + 3, + 165, + 162, + 34, + 156, + 68, + 64, + 122, + 150, + 129, + 237, + 58, + 96, + 150, + 3, + 48, + 112, + 98, + 1, + 118, + 74, + 56, + 42, + 23, + 14, + 140, + 32, + 129, + 0, + 5, + 236, + 0, + 0, + 120, + 193, + 149, + 137, + 207, + 208, + 128, + 51, + 80, + 128, + 21, + 225, + 74, + 5, + 48, + 34, + 137, + 11, + 78, + 96, + 178, + 10, + 81, + 52, + 144, + 36, + 134, + 107, + 12, + 0, + 167, + 0, + 91, + 49, + 65, + 6, + 115, + 198, + 197, + 20, + 72, + 114, + 242, + 132, + 185, + 230, + 18, + 188, + 24, + 0, + 24, + 209, + 82, + 241, + 66, + 128, + 27, + 165, + 39, + 128, + 163, + 210, + 8, + 13, + 144, + 236, + 150, + 5, + 198, + 251, + 207, + 73, + 52, + 1, + 145, + 146, + 68, + 48, + 20, + 68, + 8, + 150, + 182, + 79, + 80, + 77, + 248, + 135, + 7, + 242, + 109, + 127, + 128, + 201, + 162, + 131, + 145, + 101, + 237, + 131, + 152, + 36, + 48, + 131, + 152, + 17, + 146, + 132, + 94, + 80, + 64, + 41, + 151, + 67, + 114, + 117, + 120, + 112, + 111, + 111, + 108, + 32, + 80, + 80, + 83, + 47, + 110, + 105, + 99, + 101, + 104, + 97, + 115, + 104, + 45, + 49, + 160, + 23, + 76, + 110, + 69, + 220, + 100, + 249, + 186, + 143, + 248, + 63, + 99, + 234, + 201, + 165, + 31, + 214, + 2, + 218, + 81, + 24, + 0, + 54, + 137, + 174, + 148, + 73, + 134, + 163, + 9, + 214, + 130, + 136, + 142, + 224, + 18, + 50, + 29, + 215, + 108, + 211, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "mmr_proof": [ + "0xd85f3204700a79681b53fd8304a44141bd2af2fe44b2265aed636c16ed6fe223", + "0x250018a435d17b5473e05f884ea1af54be447730a201fd72a707da51a820345c", + "0x19e99f81e6a4933a1dd41191733bd7b11ee9dbdf74f3a508e04514eb5053a57e", + "0x5a58cfc592d45892857d908e3c8c2da8c40b1cab2d4c941546ed9232114d1b37", + "0x752448ae955fe7ab9d28a97fb104fccca709d7ef6c73fd814ae3fefb5d211d7b", + "0xbb33fff29f3094bd9de4d3668fd20f2a45f8028c0c532f4e10b2368ab5c783aa", + "0xf99a2e4fdc253c869f26e9e13e8794c41a3bebb7498fe90513ff1996378db3d2", + "0x1eeaaf21efadf32e39e4db63eedb3b791e5586e373bcdc783c42ea6a8028b489", + "0x82dd59d4c6cb74831ca6eebc0b9beb938a7c0e33e000c160264520780daecd87", + "0x791c7933517944067f8dcf8cdbb4bf9c634d07c0f1f91ad3648920bc29a6884d", + "0xf3d9b0595399d791bb7b2e91630175518483c8c517d1097e4b67b0ae78c1bb21", + "0x82c76ea3624a6f4e2d62c5375b3dc0a86bf69810715bc7fddebc92937cd4b0e8", + "0x1b7b0f134de7422015a5421b9d4c0fbd370493f06144b53b5ca3ebf419756f00", + "0xa3e48aa8f6637e4a196bf2e5c51d63b766cee11645a1e2cb20e38fd6c2f67616", + "0xcea8ad8ff6608360c6c43b229e482664a5ce0f23090fab77acd12803c6094c03", + "0x223b4e206927956a87b2bd3be0b39f6176172c13d773b99e11b4d0ac8c1307d4", + "0x5079605c269d43ce5a544d19e7832911a862f74b0df14d7b604232869b39ed78", + "0xfb07e7779638fe0e542e6a55e3e4068d1a0a71e178637553e2abe5177c3a3d7d", + "0x93071e157ae565f7dbfea93c3da2e03caf192a8d97e1e4f752cad4b03dad40a8", + "0xb2838c6d8d70917ab69e952493e18b67e0e1f22745f3b30528bcb4346e47455b", + "0x68d509b78ca15e200209038efa53ab5fc30eef61b87bd1c3417fd4eab256c255", + "0xc3c2b03b5831d3cf7bfc8c96e38b1f5e1c3a19745b220cdc5f9eec15f6a1f9c1", + "0x16fed94ec0cf0aa4b075e19cb2b6ff780a6c11e9b1c60195b036c1b8622c0147", + "0xf47e6584af17667881494720b50dd063d0900b288438df7a37e2e91440aedd23", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + "field_idx": 1 + }, + { + "header_rlp": [ + 249, + 2, + 24, + 160, + 216, + 95, + 50, + 4, + 112, + 10, + 121, + 104, + 27, + 83, + 253, + 131, + 4, + 164, + 65, + 65, + 189, + 42, + 242, + 254, + 68, + 178, + 38, + 90, + 237, + 99, + 108, + 22, + 237, + 111, + 226, + 35, + 160, + 29, + 204, + 77, + 232, + 222, + 199, + 93, + 122, + 171, + 133, + 181, + 103, + 182, + 204, + 212, + 26, + 211, + 18, + 69, + 27, + 148, + 138, + 116, + 19, + 240, + 161, + 66, + 253, + 64, + 212, + 147, + 71, + 148, + 36, + 155, + 219, + 68, + 153, + 189, + 124, + 104, + 54, + 100, + 193, + 73, + 39, + 108, + 29, + 134, + 16, + 142, + 33, + 55, + 160, + 156, + 228, + 86, + 29, + 250, + 219, + 79, + 176, + 34, + 222, + 188, + 124, + 1, + 49, + 65, + 163, + 106, + 35, + 212, + 103, + 244, + 38, + 140, + 14, + 98, + 10, + 123, + 186, + 5, + 177, + 116, + 248, + 160, + 236, + 76, + 142, + 192, + 34, + 129, + 193, + 150, + 227, + 248, + 130, + 180, + 6, + 28, + 5, + 244, + 240, + 132, + 58, + 14, + 175, + 114, + 163, + 238, + 71, + 21, + 7, + 114, + 32, + 147, + 75, + 188, + 160, + 13, + 254, + 202, + 181, + 217, + 122, + 77, + 163, + 97, + 185, + 44, + 41, + 21, + 124, + 214, + 165, + 8, + 155, + 85, + 250, + 206, + 224, + 7, + 238, + 161, + 96, + 5, + 191, + 168, + 216, + 38, + 148, + 185, + 1, + 0, + 25, + 130, + 124, + 128, + 145, + 88, + 164, + 154, + 65, + 140, + 34, + 195, + 42, + 197, + 16, + 176, + 97, + 232, + 128, + 112, + 116, + 65, + 32, + 20, + 161, + 10, + 0, + 0, + 38, + 12, + 36, + 231, + 42, + 74, + 3, + 69, + 176, + 142, + 12, + 80, + 128, + 81, + 88, + 3, + 2, + 19, + 3, + 142, + 146, + 5, + 36, + 76, + 81, + 0, + 183, + 34, + 48, + 104, + 1, + 142, + 241, + 100, + 128, + 9, + 54, + 116, + 0, + 100, + 214, + 66, + 130, + 35, + 212, + 21, + 16, + 78, + 188, + 193, + 161, + 101, + 102, + 32, + 100, + 36, + 167, + 7, + 208, + 81, + 68, + 129, + 166, + 0, + 128, + 9, + 10, + 52, + 94, + 0, + 7, + 36, + 70, + 208, + 0, + 33, + 1, + 41, + 197, + 140, + 19, + 138, + 88, + 34, + 172, + 104, + 98, + 6, + 24, + 231, + 172, + 170, + 205, + 52, + 175, + 22, + 85, + 52, + 58, + 202, + 51, + 64, + 17, + 208, + 64, + 99, + 162, + 146, + 3, + 165, + 162, + 34, + 156, + 68, + 64, + 122, + 150, + 129, + 237, + 58, + 96, + 150, + 3, + 48, + 112, + 98, + 1, + 118, + 74, + 56, + 42, + 23, + 14, + 140, + 32, + 129, + 0, + 5, + 236, + 0, + 0, + 120, + 193, + 149, + 137, + 207, + 208, + 128, + 51, + 80, + 128, + 21, + 225, + 74, + 5, + 48, + 34, + 137, + 11, + 78, + 96, + 178, + 10, + 81, + 52, + 144, + 36, + 134, + 107, + 12, + 0, + 167, + 0, + 91, + 49, + 65, + 6, + 115, + 198, + 197, + 20, + 72, + 114, + 242, + 132, + 185, + 230, + 18, + 188, + 24, + 0, + 24, + 209, + 82, + 241, + 66, + 128, + 27, + 165, + 39, + 128, + 163, + 210, + 8, + 13, + 144, + 236, + 150, + 5, + 198, + 251, + 207, + 73, + 52, + 1, + 145, + 146, + 68, + 48, + 20, + 68, + 8, + 150, + 182, + 79, + 80, + 77, + 248, + 135, + 7, + 242, + 109, + 127, + 128, + 201, + 162, + 131, + 145, + 101, + 237, + 131, + 152, + 36, + 48, + 131, + 152, + 17, + 146, + 132, + 94, + 80, + 64, + 41, + 151, + 67, + 114, + 117, + 120, + 112, + 111, + 111, + 108, + 32, + 80, + 80, + 83, + 47, + 110, + 105, + 99, + 101, + 104, + 97, + 115, + 104, + 45, + 49, + 160, + 23, + 76, + 110, + 69, + 220, + 100, + 249, + 186, + 143, + 248, + 63, + 99, + 234, + 201, + 165, + 31, + 214, + 2, + 218, + 81, + 24, + 0, + 54, + 137, + 174, + 148, + 73, + 134, + 163, + 9, + 214, + 130, + 136, + 142, + 224, + 18, + 50, + 29, + 215, + 108, + 211, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "mmr_proof": [ + "0xd85f3204700a79681b53fd8304a44141bd2af2fe44b2265aed636c16ed6fe223", + "0x250018a435d17b5473e05f884ea1af54be447730a201fd72a707da51a820345c", + "0x19e99f81e6a4933a1dd41191733bd7b11ee9dbdf74f3a508e04514eb5053a57e", + "0x5a58cfc592d45892857d908e3c8c2da8c40b1cab2d4c941546ed9232114d1b37", + "0x752448ae955fe7ab9d28a97fb104fccca709d7ef6c73fd814ae3fefb5d211d7b", + "0xbb33fff29f3094bd9de4d3668fd20f2a45f8028c0c532f4e10b2368ab5c783aa", + "0xf99a2e4fdc253c869f26e9e13e8794c41a3bebb7498fe90513ff1996378db3d2", + "0x1eeaaf21efadf32e39e4db63eedb3b791e5586e373bcdc783c42ea6a8028b489", + "0x82dd59d4c6cb74831ca6eebc0b9beb938a7c0e33e000c160264520780daecd87", + "0x791c7933517944067f8dcf8cdbb4bf9c634d07c0f1f91ad3648920bc29a6884d", + "0xf3d9b0595399d791bb7b2e91630175518483c8c517d1097e4b67b0ae78c1bb21", + "0x82c76ea3624a6f4e2d62c5375b3dc0a86bf69810715bc7fddebc92937cd4b0e8", + "0x1b7b0f134de7422015a5421b9d4c0fbd370493f06144b53b5ca3ebf419756f00", + "0xa3e48aa8f6637e4a196bf2e5c51d63b766cee11645a1e2cb20e38fd6c2f67616", + "0xcea8ad8ff6608360c6c43b229e482664a5ce0f23090fab77acd12803c6094c03", + "0x223b4e206927956a87b2bd3be0b39f6176172c13d773b99e11b4d0ac8c1307d4", + "0x5079605c269d43ce5a544d19e7832911a862f74b0df14d7b604232869b39ed78", + "0xfb07e7779638fe0e542e6a55e3e4068d1a0a71e178637553e2abe5177c3a3d7d", + "0x93071e157ae565f7dbfea93c3da2e03caf192a8d97e1e4f752cad4b03dad40a8", + "0xb2838c6d8d70917ab69e952493e18b67e0e1f22745f3b30528bcb4346e47455b", + "0x68d509b78ca15e200209038efa53ab5fc30eef61b87bd1c3417fd4eab256c255", + "0xc3c2b03b5831d3cf7bfc8c96e38b1f5e1c3a19745b220cdc5f9eec15f6a1f9c1", + "0x16fed94ec0cf0aa4b075e19cb2b6ff780a6c11e9b1c60195b036c1b8622c0147", + "0xf47e6584af17667881494720b50dd063d0900b288438df7a37e2e91440aedd23", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + "field_idx": 71 + }, + { + "header_rlp": [ + 249, + 2, + 24, + 160, + 216, + 95, + 50, + 4, + 112, + 10, + 121, + 104, + 27, + 83, + 253, + 131, + 4, + 164, + 65, + 65, + 189, + 42, + 242, + 254, + 68, + 178, + 38, + 90, + 237, + 99, + 108, + 22, + 237, + 111, + 226, + 35, + 160, + 29, + 204, + 77, + 232, + 222, + 199, + 93, + 122, + 171, + 133, + 181, + 103, + 182, + 204, + 212, + 26, + 211, + 18, + 69, + 27, + 148, + 138, + 116, + 19, + 240, + 161, + 66, + 253, + 64, + 212, + 147, + 71, + 148, + 36, + 155, + 219, + 68, + 153, + 189, + 124, + 104, + 54, + 100, + 193, + 73, + 39, + 108, + 29, + 134, + 16, + 142, + 33, + 55, + 160, + 156, + 228, + 86, + 29, + 250, + 219, + 79, + 176, + 34, + 222, + 188, + 124, + 1, + 49, + 65, + 163, + 106, + 35, + 212, + 103, + 244, + 38, + 140, + 14, + 98, + 10, + 123, + 186, + 5, + 177, + 116, + 248, + 160, + 236, + 76, + 142, + 192, + 34, + 129, + 193, + 150, + 227, + 248, + 130, + 180, + 6, + 28, + 5, + 244, + 240, + 132, + 58, + 14, + 175, + 114, + 163, + 238, + 71, + 21, + 7, + 114, + 32, + 147, + 75, + 188, + 160, + 13, + 254, + 202, + 181, + 217, + 122, + 77, + 163, + 97, + 185, + 44, + 41, + 21, + 124, + 214, + 165, + 8, + 155, + 85, + 250, + 206, + 224, + 7, + 238, + 161, + 96, + 5, + 191, + 168, + 216, + 38, + 148, + 185, + 1, + 0, + 25, + 130, + 124, + 128, + 145, + 88, + 164, + 154, + 65, + 140, + 34, + 195, + 42, + 197, + 16, + 176, + 97, + 232, + 128, + 112, + 116, + 65, + 32, + 20, + 161, + 10, + 0, + 0, + 38, + 12, + 36, + 231, + 42, + 74, + 3, + 69, + 176, + 142, + 12, + 80, + 128, + 81, + 88, + 3, + 2, + 19, + 3, + 142, + 146, + 5, + 36, + 76, + 81, + 0, + 183, + 34, + 48, + 104, + 1, + 142, + 241, + 100, + 128, + 9, + 54, + 116, + 0, + 100, + 214, + 66, + 130, + 35, + 212, + 21, + 16, + 78, + 188, + 193, + 161, + 101, + 102, + 32, + 100, + 36, + 167, + 7, + 208, + 81, + 68, + 129, + 166, + 0, + 128, + 9, + 10, + 52, + 94, + 0, + 7, + 36, + 70, + 208, + 0, + 33, + 1, + 41, + 197, + 140, + 19, + 138, + 88, + 34, + 172, + 104, + 98, + 6, + 24, + 231, + 172, + 170, + 205, + 52, + 175, + 22, + 85, + 52, + 58, + 202, + 51, + 64, + 17, + 208, + 64, + 99, + 162, + 146, + 3, + 165, + 162, + 34, + 156, + 68, + 64, + 122, + 150, + 129, + 237, + 58, + 96, + 150, + 3, + 48, + 112, + 98, + 1, + 118, + 74, + 56, + 42, + 23, + 14, + 140, + 32, + 129, + 0, + 5, + 236, + 0, + 0, + 120, + 193, + 149, + 137, + 207, + 208, + 128, + 51, + 80, + 128, + 21, + 225, + 74, + 5, + 48, + 34, + 137, + 11, + 78, + 96, + 178, + 10, + 81, + 52, + 144, + 36, + 134, + 107, + 12, + 0, + 167, + 0, + 91, + 49, + 65, + 6, + 115, + 198, + 197, + 20, + 72, + 114, + 242, + 132, + 185, + 230, + 18, + 188, + 24, + 0, + 24, + 209, + 82, + 241, + 66, + 128, + 27, + 165, + 39, + 128, + 163, + 210, + 8, + 13, + 144, + 236, + 150, + 5, + 198, + 251, + 207, + 73, + 52, + 1, + 145, + 146, + 68, + 48, + 20, + 68, + 8, + 150, + 182, + 79, + 80, + 77, + 248, + 135, + 7, + 242, + 109, + 127, + 128, + 201, + 162, + 131, + 145, + 101, + 237, + 131, + 152, + 36, + 48, + 131, + 152, + 17, + 146, + 132, + 94, + 80, + 64, + 41, + 151, + 67, + 114, + 117, + 120, + 112, + 111, + 111, + 108, + 32, + 80, + 80, + 83, + 47, + 110, + 105, + 99, + 101, + 104, + 97, + 115, + 104, + 45, + 49, + 160, + 23, + 76, + 110, + 69, + 220, + 100, + 249, + 186, + 143, + 248, + 63, + 99, + 234, + 201, + 165, + 31, + 214, + 2, + 218, + 81, + 24, + 0, + 54, + 137, + 174, + 148, + 73, + 134, + 163, + 9, + 214, + 130, + 136, + 142, + 224, + 18, + 50, + 29, + 215, + 108, + 211, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "mmr_proof": [ + "0xd85f3204700a79681b53fd8304a44141bd2af2fe44b2265aed636c16ed6fe223", + "0x250018a435d17b5473e05f884ea1af54be447730a201fd72a707da51a820345c", + "0x19e99f81e6a4933a1dd41191733bd7b11ee9dbdf74f3a508e04514eb5053a57e", + "0x5a58cfc592d45892857d908e3c8c2da8c40b1cab2d4c941546ed9232114d1b37", + "0x752448ae955fe7ab9d28a97fb104fccca709d7ef6c73fd814ae3fefb5d211d7b", + "0xbb33fff29f3094bd9de4d3668fd20f2a45f8028c0c532f4e10b2368ab5c783aa", + "0xf99a2e4fdc253c869f26e9e13e8794c41a3bebb7498fe90513ff1996378db3d2", + "0x1eeaaf21efadf32e39e4db63eedb3b791e5586e373bcdc783c42ea6a8028b489", + "0x82dd59d4c6cb74831ca6eebc0b9beb938a7c0e33e000c160264520780daecd87", + "0x791c7933517944067f8dcf8cdbb4bf9c634d07c0f1f91ad3648920bc29a6884d", + "0xf3d9b0595399d791bb7b2e91630175518483c8c517d1097e4b67b0ae78c1bb21", + "0x82c76ea3624a6f4e2d62c5375b3dc0a86bf69810715bc7fddebc92937cd4b0e8", + "0x1b7b0f134de7422015a5421b9d4c0fbd370493f06144b53b5ca3ebf419756f00", + "0xa3e48aa8f6637e4a196bf2e5c51d63b766cee11645a1e2cb20e38fd6c2f67616", + "0xcea8ad8ff6608360c6c43b229e482664a5ce0f23090fab77acd12803c6094c03", + "0x223b4e206927956a87b2bd3be0b39f6176172c13d773b99e11b4d0ac8c1307d4", + "0x5079605c269d43ce5a544d19e7832911a862f74b0df14d7b604232869b39ed78", + "0xfb07e7779638fe0e542e6a55e3e4068d1a0a71e178637553e2abe5177c3a3d7d", + "0x93071e157ae565f7dbfea93c3da2e03caf192a8d97e1e4f752cad4b03dad40a8", + "0xb2838c6d8d70917ab69e952493e18b67e0e1f22745f3b30528bcb4346e47455b", + "0x68d509b78ca15e200209038efa53ab5fc30eef61b87bd1c3417fd4eab256c255", + "0xc3c2b03b5831d3cf7bfc8c96e38b1f5e1c3a19745b220cdc5f9eec15f6a1f9c1", + "0x16fed94ec0cf0aa4b075e19cb2b6ff780a6c11e9b1c60195b036c1b8622c0147", + "0xf47e6584af17667881494720b50dd063d0900b288438df7a37e2e91440aedd23", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + "field_idx": 12 + } + ], + "_phantom": null +} \ No newline at end of file diff --git a/axiom-query/data/test/input_keccak_shard_for_header.json b/axiom-query/data/test/input_keccak_shard_for_header.json new file mode 100644 index 00000000..b0f2eb39 --- /dev/null +++ b/axiom-query/data/test/input_keccak_shard_for_header.json @@ -0,0 +1,137 @@ +{ + "capacity": 41, + "responses": [ + [ + "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000348bab8f4d2acdd4022f4b7becb8ba36a260bd9f699aa4df0a0660dc772ddcfcfff5804d8cd309c79ff26f19ecc6a9cef13556c9d5ddb18afae277d71ad7d4114f1a2ab29b70f47ff5c88dc7048bb23fcc4a59fbcc3b9f0e9529297afb28cb8a995310788441a017c79a1fc0edab3074e6d5495712b35e120c446301b8b60d8c0000000000000000000000000000000000000000000000000000000000000000c4933b4a3d6a41bf6c8009e24436a19e3e91a6f4198a0f498ade1179d29785000000000000000000000000000000000000000000000000000000000000000000dff0f5a47ed23e4dd682fa5b20c861c8d616f0124bf3366def9f620e1569ade60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004755084f1c80a73395852c7ede586dddb3b8db7828e4ad2cf3259f693ac4f875", + null + ], + [ + "0x0e12a985789e947d5c94b2f0dfee712cdff776434e0a8c13379a81ec9cabbd90fb07e7779638fe0e542e6a55e3e4068d1a0a71e178637553e2abe5177c3a3d7d", + null + ], + [ + "0x19e99f81e6a4933a1dd41191733bd7b11ee9dbdf74f3a508e04514eb5053a57ea1aba88a1e5835420920b9de5a5c9489f57d8bee868c142f47f3716b9e1352c6", + null + ], + [ + "0x1eeaaf21efadf32e39e4db63eedb3b791e5586e373bcdc783c42ea6a8028b48907449018b55a87abbc033d85102853995a41aeaa8544452dc2240aebda5e9cb3", + null + ], + [ + "0x3020fe1044a376596e399087fc0e5cb8783a853e2c85c7206514dc2cf71e42f31b7b0f134de7422015a5421b9d4c0fbd370493f06144b53b5ca3ebf419756f00", + null + ], + [ + "0x4755084f1c80a73395852c7ede586dddb3b8db7828e4ad2cf3259f693ac4f8750000000000000000000000000000000000000000000000000000000000000000", + null + ], + [ + "0x477c055e69de14e3bbfe2af0389e6c3ac28ffb5b0cc8fa26b543ac47857fd64616fed94ec0cf0aa4b075e19cb2b6ff780a6c11e9b1c60195b036c1b8622c0147", + null + ], + [ + "0x5079605c269d43ce5a544d19e7832911a862f74b0df14d7b604232869b39ed78a0d8feace83653ed55151dbf0959b0524c9e3187f928e41aab067daed472d269", + null + ], + [ + "0x5459bcc28c212f4f7ba9b7075fad7c3f2292e83997007998dd27d5369d469616752448ae955fe7ab9d28a97fb104fccca709d7ef6c73fd814ae3fefb5d211d7b", + null + ], + [ + "0x594f03e3c07dfc61489a41b9645c2715fa0cbeebd2c13d25b30add4bbfd315950000000000000000000000000000000000000000000000000000000000000000", + null + ], + [ + "0x5a58cfc592d45892857d908e3c8c2da8c40b1cab2d4c941546ed9232114d1b373409945b63c2e1f8a04cc3236cf0d4ac7a340695c8f5641a526dc6c783ca91e0", + null + ], + [ + "0x615ab700eca77f00034ff0b208a7c9e876cde7cd947aaebb2cb004ce461a1e43b2838c6d8d70917ab69e952493e18b67e0e1f22745f3b30528bcb4346e47455b", + null + ], + [ + "0x655090a6192edeb76e76b01476071607739746e8197158c58ed6d3a001021e43c3c2b03b5831d3cf7bfc8c96e38b1f5e1c3a19745b220cdc5f9eec15f6a1f9c1", + null + ], + [ + "0x68d509b78ca15e200209038efa53ab5fc30eef61b87bd1c3417fd4eab256c255c317e0c0c4ac8c49716549e55a7ab0252ba4116ea7f7a518fb1441d02ef1e248", + null + ], + [ + "0x6d495f5672d5d7482e1063eb1e1381edb72cabfdfa2c218cc38461136cc513970000000000000000000000000000000000000000000000000000000000000000", + null + ], + [ + "0x79a9b620dd7c6e8a40e973ae2193a378de5c45d9ff7917e3f31d01b019ac0527791c7933517944067f8dcf8cdbb4bf9c634d07c0f1f91ad3648920bc29a6884d", + null + ], + [ + "0x79e3279ab366ab5dc5c1f8ad4aad3ce7689086e4f8e25572c835f43d4a826432250018a435d17b5473e05f884ea1af54be447730a201fd72a707da51a820345c", + null + ], + [ + "0x82dd59d4c6cb74831ca6eebc0b9beb938a7c0e33e000c160264520780daecd8737d210b023995e28e43315bb1a98eeddde85a35b7321d7162f51c9425fd5f905", + null + ], + [ + "0x833b246c8bdd751a108869f958b69cfaee4a03c036905b90c39bfc986bda80ee0000000000000000000000000000000000000000000000000000000000000000", + null + ], + [ + "0x9071e895206e9ac84c8ae51e7180e9479827ca3288175f4615fdc724e28008d793071e157ae565f7dbfea93c3da2e03caf192a8d97e1e4f752cad4b03dad40a8", + null + ], + [ + "0xa3e48aa8f6637e4a196bf2e5c51d63b766cee11645a1e2cb20e38fd6c2f676163eb43d4625448a26adb10215f7f092a6fc8622a21b35048c2b2ef1d991162d84", + null + ], + [ + "0xa60d89ad021ef63b49250141377cc3e835945e5d4136ffc1909047c4ecca3af70000000000000000000000000000000000000000000000000000000000000000", + null + ], + [ + "0xbb33fff29f3094bd9de4d3668fd20f2a45f8028c0c532f4e10b2368ab5c783aa22b2e40d32a936ed08307b41db986d35160fe5917c8d2609d27e7bd02f4b767e", + null + ], + [ + "0xbe8c5ff2ada208b4577bd49d7660fdaf3e17983530f5d1d574126c5ee743de900000000000000000000000000000000000000000000000000000000000000000", + null + ], + [ + "0xc364b3480c792d14112d17f167581d8fcf74b6d7805bc1ef1526f5fa7415be400000000000000000000000000000000000000000000000000000000000000000", + null + ], + [ + "0xcea8ad8ff6608360c6c43b229e482664a5ce0f23090fab77acd12803c6094c03f7533e213fc68258934880ec7189125fc6071e01f20aa465e56f7c7ecfccb800", + null + ], + [ + "0xd85f3204700a79681b53fd8304a44141bd2af2fe44b2265aed636c16ed6fe223c68b30cb54482f31743afe91abb6841704d275a6a378f7faa389d387c384d568", + null + ], + [ + "0xdad303cf18a459358a346f84e5e3a3065256a3584769c3cfae5b1729e168ed52223b4e206927956a87b2bd3be0b39f6176172c13d773b99e11b4d0ac8c1307d4", + null + ], + [ + "0xe2327533e28e723bb66ff3d0bfcb32bad0c070e8935d13c0a36a3126c8f3b7d082c76ea3624a6f4e2d62c5375b3dc0a86bf69810715bc7fddebc92937cd4b0e8", + null + ], + [ + "0xf3d9b0595399d791bb7b2e91630175518483c8c517d1097e4b67b0ae78c1bb21eab78f0a8534330a079c8179e08f594fe8ed3483c2df1c46e7177e4839ab2114", + null + ], + [ + "0xf47e6584af17667881494720b50dd063d0900b288438df7a37e2e91440aedd2388e177538a14ffca910f9c6b56f6bed99d1f748fa103093f372ecbe9db819e38", + null + ], + [ + "0xf90218a0d85f3204700a79681b53fd8304a44141bd2af2fe44b2265aed636c16ed6fe223a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794249bdb4499bd7c683664c149276c1d86108e2137a09ce4561dfadb4fb022debc7c013141a36a23d467f4268c0e620a7bba05b174f8a0ec4c8ec02281c196e3f882b4061c05f4f0843a0eaf72a3ee4715077220934bbca00dfecab5d97a4da361b92c29157cd6a5089b55facee007eea16005bfa8d82694b9010019827c809158a49a418c22c32ac510b061e8807074412014a10a0000260c24e72a4a0345b08e0c50805158030213038e9205244c5100b7223068018ef164800936740064d6428223d415104ebcc1a16566206424a707d0514481a60080090a345e00072446d000210129c58c138a5822ac68620618e7acaacd34af1655343aca334011d04063a29203a5a2229c44407a9681ed3a60960330706201764a382a170e8c20810005ec000078c19589cfd08033508015e14a053022890b4e60b20a51349024866b0c00a7005b31410673c6c5144872f284b9e612bc180018d152f142801ba52780a3d2080d90ec9605c6fbcf4934019192443014440896b64f504df88707f26d7f80c9a2839165ed8398243083981192845e5040299743727578706f6f6c205050532f6e696365686173682d31a0174c6e45dc64f9ba8ff83f63eac9a51fd602da5118003689ae944986a309d682888ee012321dd76cd3", + null + ], + [ + "0xf99a2e4fdc253c869f26e9e13e8794c41a3bebb7498fe90513ff1996378db3d2a7d918baae6f3c07d7d0cbfac129925e9080f61cd113b60dc3970edff82f7338", + null + ] + ] +} \ No newline at end of file diff --git a/axiom-query/data/test/input_mmr_proof_for_header.json b/axiom-query/data/test/input_mmr_proof_for_header.json new file mode 100644 index 00000000..f6222e57 --- /dev/null +++ b/axiom-query/data/test/input_mmr_proof_for_header.json @@ -0,0 +1,47 @@ +{ + "historicalMmr": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x348bab8f4d2acdd4022f4b7becb8ba36a260bd9f699aa4df0a0660dc772ddcfc", + "0xfff5804d8cd309c79ff26f19ecc6a9cef13556c9d5ddb18afae277d71ad7d411", + "0x4f1a2ab29b70f47ff5c88dc7048bb23fcc4a59fbcc3b9f0e9529297afb28cb8a", + "0x995310788441a017c79a1fc0edab3074e6d5495712b35e120c446301b8b60d8c", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xc4933b4a3d6a41bf6c8009e24436a19e3e91a6f4198a0f498ade1179d2978500", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xdff0f5a47ed23e4dd682fa5b20c861c8d616f0124bf3366def9f620e1569ade6", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x4755084f1c80a73395852c7ede586dddb3b8db7828e4ad2cf3259f693ac4f875" + ], + "blockHash": "0xc68b30cb54482f31743afe91abb6841704d275a6a378f7faa389d387c384d568", + "logsBloom": "0x19827c809158a49a418c22c32ac510b061e8807074412014a10a0000260c24e72a4a0345b08e0c50805158030213038e9205244c5100b7223068018ef164800936740064d6428223d415104ebcc1a16566206424a707d0514481a60080090a345e00072446d000210129c58c138a5822ac68620618e7acaacd34af1655343aca334011d04063a29203a5a2229c44407a9681ed3a60960330706201764a382a170e8c20810005ec000078c19589cfd08033508015e14a053022890b4e60b20a51349024866b0c00a7005b31410673c6c5144872f284b9e612bc180018d152f142801ba52780a3d2080d90ec9605c6fbcf4934019192443014440896b64f504df8", + "mmrProof": [ + "0xd85f3204700a79681b53fd8304a44141bd2af2fe44b2265aed636c16ed6fe223", + "0x250018a435d17b5473e05f884ea1af54be447730a201fd72a707da51a820345c", + "0x19e99f81e6a4933a1dd41191733bd7b11ee9dbdf74f3a508e04514eb5053a57e", + "0x5a58cfc592d45892857d908e3c8c2da8c40b1cab2d4c941546ed9232114d1b37", + "0x752448ae955fe7ab9d28a97fb104fccca709d7ef6c73fd814ae3fefb5d211d7b", + "0xbb33fff29f3094bd9de4d3668fd20f2a45f8028c0c532f4e10b2368ab5c783aa", + "0xf99a2e4fdc253c869f26e9e13e8794c41a3bebb7498fe90513ff1996378db3d2", + "0x1eeaaf21efadf32e39e4db63eedb3b791e5586e373bcdc783c42ea6a8028b489", + "0x82dd59d4c6cb74831ca6eebc0b9beb938a7c0e33e000c160264520780daecd87", + "0x791c7933517944067f8dcf8cdbb4bf9c634d07c0f1f91ad3648920bc29a6884d", + "0xf3d9b0595399d791bb7b2e91630175518483c8c517d1097e4b67b0ae78c1bb21", + "0x82c76ea3624a6f4e2d62c5375b3dc0a86bf69810715bc7fddebc92937cd4b0e8", + "0x1b7b0f134de7422015a5421b9d4c0fbd370493f06144b53b5ca3ebf419756f00", + "0xa3e48aa8f6637e4a196bf2e5c51d63b766cee11645a1e2cb20e38fd6c2f67616", + "0xcea8ad8ff6608360c6c43b229e482664a5ce0f23090fab77acd12803c6094c03", + "0x223b4e206927956a87b2bd3be0b39f6176172c13d773b99e11b4d0ac8c1307d4", + "0x5079605c269d43ce5a544d19e7832911a862f74b0df14d7b604232869b39ed78", + "0xfb07e7779638fe0e542e6a55e3e4068d1a0a71e178637553e2abe5177c3a3d7d", + "0x93071e157ae565f7dbfea93c3da2e03caf192a8d97e1e4f752cad4b03dad40a8", + "0xb2838c6d8d70917ab69e952493e18b67e0e1f22745f3b30528bcb4346e47455b", + "0x68d509b78ca15e200209038efa53ab5fc30eef61b87bd1c3417fd4eab256c255", + "0xc3c2b03b5831d3cf7bfc8c96e38b1f5e1c3a19745b220cdc5f9eec15f6a1f9c1", + "0x16fed94ec0cf0aa4b075e19cb2b6ff780a6c11e9b1c60195b036c1b8622c0147", + "0xf47e6584af17667881494720b50dd063d0900b288438df7a37e2e91440aedd23" + ] +} \ No newline at end of file diff --git a/axiom-query/data/test/input_results_root.json b/axiom-query/data/test/input_results_root.json new file mode 100644 index 00000000..0c435cab --- /dev/null +++ b/axiom-query/data/test/input_results_root.json @@ -0,0 +1,671 @@ +{ + "encodedSubqueries": [ + "0x0004009165ed004400000036", + "0x0001009165ed00000001", + "0x0001009165ed00000047", + "0x0001009165ed0000000c", + "0x0001009165ed00000003", + "0x0001009165ed00000004", + "0x0001009165ed00000005", + "0x0005009165ed004400000066000000017f4091b46c33e918a0f3aa42307641d17bb67029427a5369e54b353984238705", + "0x0002009165eddac17f958d2ee523a2206206994597c13d831ec700000002", + "0x0003009165eddac17f958d2ee523a2206206994597c13d831ec7ac33ff75c19e70fe83507db0d683fd3465c996598dc972688b7ace676c89077b", + "0x0006009165eddac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000000000000000002010000000000000000000000000000000000000000000000000000000000000000", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036", + "0x0004009165ed004400000036" + ], + "numSubqueries": 11, + "promiseHeader": { + "results": [ + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 71 + }, + "value": "0x2a4a0345b08e0c50805158030213038e9205244c5100b7223068018ef1648009" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 12 + }, + "value": "0x43727578706f6f6c205050532f6e696365686173682d31000000000000000000" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 3 + }, + "value": "0x9ce4561dfadb4fb022debc7c013141a36a23d467f4268c0e620a7bba05b174f8" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 4 + }, + "value": "0xec4c8ec02281c196e3f882b4061c05f4f0843a0eaf72a3ee4715077220934bbc" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 5 + }, + "value": "0x0dfecab5d97a4da361b92c29157cd6a5089b55facee007eea16005bfa8d82694" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + }, + "promiseAccount": { + "results": [ + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "fieldIdx": 2 + }, + "value": "0x3bddd93ffcce5169c2ce5d08b91f9fda3ed657dbf3b78beffb44dc833f6308a2" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "fieldIdx": 2 + }, + "value": "0x3bddd93ffcce5169c2ce5d08b91f9fda3ed657dbf3b78beffb44dc833f6308a2" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "fieldIdx": 2 + }, + "value": "0x3bddd93ffcce5169c2ce5d08b91f9fda3ed657dbf3b78beffb44dc833f6308a2" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "fieldIdx": 2 + }, + "value": "0x3bddd93ffcce5169c2ce5d08b91f9fda3ed657dbf3b78beffb44dc833f6308a2" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "fieldIdx": 2 + }, + "value": "0x3bddd93ffcce5169c2ce5d08b91f9fda3ed657dbf3b78beffb44dc833f6308a2" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "fieldIdx": 2 + }, + "value": "0x3bddd93ffcce5169c2ce5d08b91f9fda3ed657dbf3b78beffb44dc833f6308a2" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "fieldIdx": 2 + }, + "value": "0x3bddd93ffcce5169c2ce5d08b91f9fda3ed657dbf3b78beffb44dc833f6308a2" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "fieldIdx": 2 + }, + "value": "0x3bddd93ffcce5169c2ce5d08b91f9fda3ed657dbf3b78beffb44dc833f6308a2" + } + ] + }, + "promiseStorage": { + "results": [ + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "slot": "0xac33ff75c19e70fe83507db0d683fd3465c996598dc972688b7ace676c89077b" + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "slot": "0xac33ff75c19e70fe83507db0d683fd3465c996598dc972688b7ace676c89077b" + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "slot": "0xac33ff75c19e70fe83507db0d683fd3465c996598dc972688b7ace676c89077b" + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "slot": "0xac33ff75c19e70fe83507db0d683fd3465c996598dc972688b7ace676c89077b" + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "slot": "0xac33ff75c19e70fe83507db0d683fd3465c996598dc972688b7ace676c89077b" + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "slot": "0xac33ff75c19e70fe83507db0d683fd3465c996598dc972688b7ace676c89077b" + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "slot": "0xac33ff75c19e70fe83507db0d683fd3465c996598dc972688b7ace676c89077b" + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "slot": "0xac33ff75c19e70fe83507db0d683fd3465c996598dc972688b7ace676c89077b" + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + } + ] + }, + "promiseSolidityMapping": { + "results": [ + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "mappingSlot": "0x2", + "mappingDepth": 1, + "keys": [ + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "mappingSlot": "0x2", + "mappingDepth": 1, + "keys": [ + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "mappingSlot": "0x2", + "mappingDepth": 1, + "keys": [ + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "mappingSlot": "0x2", + "mappingDepth": 1, + "keys": [ + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "mappingSlot": "0x2", + "mappingDepth": 1, + "keys": [ + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "mappingSlot": "0x2", + "mappingDepth": 1, + "keys": [ + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "mappingSlot": "0x2", + "mappingDepth": 1, + "keys": [ + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + }, + { + "subquery": { + "blockNumber": 9528813, + "addr": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "mappingSlot": "0x2", + "mappingDepth": 1, + "keys": [ + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] + }, + "value": "0x000000000000000000000000000000000000000000000000000000000b532b80" + } + ] + }, + "promiseTx": { + "results": [ + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrCalldataIdx": 54 + }, + "value": "0x00000000000000000000000000000000000000000000000000000000013efd8b" + }, + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrCalldataIdx": 54 + }, + "value": "0x00000000000000000000000000000000000000000000000000000000013efd8b" + }, + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrCalldataIdx": 54 + }, + "value": "0x00000000000000000000000000000000000000000000000000000000013efd8b" + }, + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrCalldataIdx": 54 + }, + "value": "0x00000000000000000000000000000000000000000000000000000000013efd8b" + }, + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrCalldataIdx": 54 + }, + "value": "0x00000000000000000000000000000000000000000000000000000000013efd8b" + }, + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrCalldataIdx": 54 + }, + "value": "0x00000000000000000000000000000000000000000000000000000000013efd8b" + }, + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrCalldataIdx": 54 + }, + "value": "0x00000000000000000000000000000000000000000000000000000000013efd8b" + }, + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrCalldataIdx": 54 + }, + "value": "0x00000000000000000000000000000000000000000000000000000000013efd8b" + } + ] + }, + "promiseReceipt": { + "results": [ + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrLogIdx": 102, + "topicOrDataOrAddressIdx": 1, + "eventSchema": "0x7f4091b46c33e918a0f3aa42307641d17bb67029427a5369e54b353984238705" + }, + "value": "0x000000000000000000000000263fc4d9eb6da1ed296ea6d189b41e546a188d8a" + }, + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrLogIdx": 102, + "topicOrDataOrAddressIdx": 1, + "eventSchema": "0x7f4091b46c33e918a0f3aa42307641d17bb67029427a5369e54b353984238705" + }, + "value": "0x000000000000000000000000263fc4d9eb6da1ed296ea6d189b41e546a188d8a" + }, + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrLogIdx": 102, + "topicOrDataOrAddressIdx": 1, + "eventSchema": "0x7f4091b46c33e918a0f3aa42307641d17bb67029427a5369e54b353984238705" + }, + "value": "0x000000000000000000000000263fc4d9eb6da1ed296ea6d189b41e546a188d8a" + }, + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrLogIdx": 102, + "topicOrDataOrAddressIdx": 1, + "eventSchema": "0x7f4091b46c33e918a0f3aa42307641d17bb67029427a5369e54b353984238705" + }, + "value": "0x000000000000000000000000263fc4d9eb6da1ed296ea6d189b41e546a188d8a" + }, + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrLogIdx": 102, + "topicOrDataOrAddressIdx": 1, + "eventSchema": "0x7f4091b46c33e918a0f3aa42307641d17bb67029427a5369e54b353984238705" + }, + "value": "0x000000000000000000000000263fc4d9eb6da1ed296ea6d189b41e546a188d8a" + }, + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrLogIdx": 102, + "topicOrDataOrAddressIdx": 1, + "eventSchema": "0x7f4091b46c33e918a0f3aa42307641d17bb67029427a5369e54b353984238705" + }, + "value": "0x000000000000000000000000263fc4d9eb6da1ed296ea6d189b41e546a188d8a" + }, + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrLogIdx": 102, + "topicOrDataOrAddressIdx": 1, + "eventSchema": "0x7f4091b46c33e918a0f3aa42307641d17bb67029427a5369e54b353984238705" + }, + "value": "0x000000000000000000000000263fc4d9eb6da1ed296ea6d189b41e546a188d8a" + }, + { + "subquery": { + "blockNumber": 9528813, + "txIdx": 68, + "fieldOrLogIdx": 102, + "topicOrDataOrAddressIdx": 1, + "eventSchema": "0x7f4091b46c33e918a0f3aa42307641d17bb67029427a5369e54b353984238705" + }, + "value": "0x000000000000000000000000263fc4d9eb6da1ed296ea6d189b41e546a188d8a" + } + ] + }, + "promiseKeccak": { + "leaves": [ + { + "commit": "a60daac95d772a0f94dec507dd35e8bceb7f48aa5c404fd62127234b48e5b214", + "capacity": 0 + } + ], + "shards": [ + [ + 0, + { + "responses": [], + "capacity": 0 + } + ] + ] + }, + "keccakMerkleMaxHeight": 3 +} \ No newline at end of file diff --git a/axiom-query/data/test/input_results_root_for_agg.json b/axiom-query/data/test/input_results_root_for_agg.json new file mode 100644 index 00000000..20929eb8 --- /dev/null +++ b/axiom-query/data/test/input_results_root_for_agg.json @@ -0,0 +1,73 @@ +{ + "subqueries": { + "rows": [ + { + "key": [ + "0100000000000000000000000000000000000000000000000000000000000000", + "ed65910000000000000000000000000000000000000000000000000000000000", + "0100000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "value": [ + "1ad4ccb667b585ab7a5dc7dee84dcc1d00000000000000000000000000000000", + "4793d440fd42a1f013748a941b4512d300000000000000000000000000000000" + ] + }, + { + "key": [ + "0100000000000000000000000000000000000000000000000000000000000000", + "ed65910000000000000000000000000000000000000000000000000000000000", + "4700000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "value": [ + "8e03130203585180500c8eb045034a2a00000000000000000000000000000000", + "098064f18e01683022b700514c24059200000000000000000000000000000000" + ] + }, + { + "key": [ + "0100000000000000000000000000000000000000000000000000000000000000", + "ed65910000000000000000000000000000000000000000000000000000000000", + "0c00000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "value": [ + "63696e2f535050206c6f6f707875724300000000000000000000000000000000", + "000000000000000000312d687361686500000000000000000000000000000000" + ] + } + ] + }, + "num_subqueries": "0300000000000000000000000000000000000000000000000000000000000000" +} \ No newline at end of file diff --git a/axiom-query/data/test/input_verify_compute.json b/axiom-query/data/test/input_verify_compute.json new file mode 100644 index 00000000..cf3e26bf --- /dev/null +++ b/axiom-query/data/test/input_verify_compute.json @@ -0,0 +1 @@ +{"sourceChainId":1,"subqueryResults":{"results":[{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":1,"encodedSubqueryData":"0x009165ed00000001"},"value":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"},{"subquery":{"subqueryType":1,"encodedSubqueryData":"0x009165ed00000047"},"value":"0x2a4a0345b08e0c50805158030213038e9205244c5100b7223068018ef1648009"},{"subquery":{"subqueryType":1,"encodedSubqueryData":"0x009165ed0000000c"},"value":"0x43727578706f6f6c205050532f6e696365686173682d31000000000000000000"},{"subquery":{"subqueryType":1,"encodedSubqueryData":"0x009165ed00000003"},"value":"0x9ce4561dfadb4fb022debc7c013141a36a23d467f4268c0e620a7bba05b174f8"},{"subquery":{"subqueryType":1,"encodedSubqueryData":"0x009165ed00000004"},"value":"0xec4c8ec02281c196e3f882b4061c05f4f0843a0eaf72a3ee4715077220934bbc"},{"subquery":{"subqueryType":1,"encodedSubqueryData":"0x009165ed00000005"},"value":"0x0dfecab5d97a4da361b92c29157cd6a5089b55facee007eea16005bfa8d82694"},{"subquery":{"subqueryType":5,"encodedSubqueryData":"0x009165ed004400000066000000017f4091b46c33e918a0f3aa42307641d17bb67029427a5369e54b353984238705"},"value":"0x000000000000000000000000263fc4d9eb6da1ed296ea6d189b41e546a188d8a"},{"subquery":{"subqueryType":2,"encodedSubqueryData":"0x009165eddac17f958d2ee523a2206206994597c13d831ec700000002"},"value":"0x3bddd93ffcce5169c2ce5d08b91f9fda3ed657dbf3b78beffb44dc833f6308a2"},{"subquery":{"subqueryType":3,"encodedSubqueryData":"0x009165eddac17f958d2ee523a2206206994597c13d831ec7ac33ff75c19e70fe83507db0d683fd3465c996598dc972688b7ace676c89077b"},"value":"0x000000000000000000000000000000000000000000000000000000000b532b80"},{"subquery":{"subqueryType":6,"encodedSubqueryData":"0x009165eddac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000000000000000002010000000000000000000000000000000000000000000000000000000000000000"},"value":"0x000000000000000000000000000000000000000000000000000000000b532b80"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"},{"subquery":{"subqueryType":4,"encodedSubqueryData":"0x009165ed004400000036"},"value":"0x00000000000000000000000000000000000000000000000000000000013efd8b"}],"subqueryHashes":["0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0x162136872c2a5889ac1f6f816de9a0915382f52d308ccfb85bf51405f01f6e57","0xec9c61c069dc9edfb82f6e76435797487af1d8b8f040b26deda52deff1c9e73b","0xf36a01afa5be89a2e56cb5656e45a5547d917141f6e3c153b140213abb204059","0xf19a33edeb981cb5f27e1b3fb44e5fff804ff8b8cedc159eeffac35c581696a1","0x4c66a19802db648e516bab8397218bdece92a9ed5dda5ef82dc3febc219b37ae","0x8eeaf7da7945c33f68480eef5a7105d7fc3b6a9a2da69a674ed6566dadcabc2c","0xe04631a5d855c6d964d036cf6627553e37d83d976d901347d37b4bda18324476","0xef5efbe0d6fef44a2ea8bc264ceb32df78cac2dc0fa4f80d2869f275216c4400","0x18c1b820a02537b67bf11b8b99014de0de98dbcf62cc2265dfd336edd3fbf634","0xf3b2d0386f6b7d605219b6dc6daedeb55ff74c9a53161707dc604b561f51def8","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd","0xf31f93d425de42ffc0eabe3b2c4fd395d293f9393447c68b86e88f9af7fd99fd"],"numSubqueries":11},"computeQuery":{"k":14,"resultLen":11,"vkey":["0x0001000009000100000004010000010080000000000000000000000000000000","0x17862c05c0a4491a407a6beadf90ab0412a2dd1c9e30c1e4a296d36d558efa25","0x7a75d7385d618442668530435a6926c8d39f686e78ca020a69969ec8be555615","0x0000000000000000000000000000000000000000000000000000000000000080","0x0000000000000000000000000000000000000000000000000000000000000080","0x0000000000000000000000000000000000000000000000000000000000000080","0x0000000000000000000000000000000000000000000000000000000000000080","0x0000000000000000000000000000000000000000000000000000000000000080","0xea4ec579288ca155462bd5dfd2222cf4a7adaa79b11b48f53ec2cdbdec87fc4b","0xb8ee25cbcf8ca69803ac92ba6b5542662d00fb00d2ad6a9c03fea6399316220a","0x9a8a73c61379755ce23a0e5370405cd4cc79d66af6d9d30e61a4ea3e8bd4ab47","0xc8822d094658052bcfc4e79e3e8341db5cb3ece6dc77a7d369f466f986334a03","0x7084bd1bce301a47e81c5d6d2f7ce4df819e6fe92a1426da80727e87b89f9818","0xfcc0f0a01464e41ebe5f5d15015e2a51fdfa9da60c8f944d6a68f420f2d7d162","0xf95482ca7783df6b833f02ae060e34330f0245944eefe5613136d874bd36ba50"],"computeProof":"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013efd8b1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493472a4a0345b08e0c50805158030213038e9205244c5100b7223068018ef164800943727578706f6f6c205050532f6e696365686173682d310000000000000000009ce4561dfadb4fb022debc7c013141a36a23d467f4268c0e620a7bba05b174f8ec4c8ec02281c196e3f882b4061c05f4f0843a0eaf72a3ee4715077220934bbc0dfecab5d97a4da361b92c29157cd6a5089b55facee007eea16005bfa8d82694000000000000000000000000263fc4d9eb6da1ed296ea6d189b41e546a188d8a3bddd93ffcce5169c2ce5d08b91f9fda3ed657dbf3b78beffb44dc833f6308a2000000000000000000000000000000000000000000000000000000000b532b80000000000000000000000000000000000000000000000000000000000b532b800c5a17fe553cd122e05ceaf5af0005ecc2246d09a17509536025f9a6f128eb1929705080a37fc138b01f20bbb5f3ad6757eae93c4b4a14927ece5b8ccba6a507e871550d2e2cfd942da732b97d00bf5b32d80ff569c51f42542e74ee4885bd1d78aee4a0fc48b69b8125f4604753840771bd7e26e1d002d0f65286ec03572717fecd7e914529e8c504f67c321ab5a72418679b722c079556c3837c30ce23265d0057fe4bd86bcaac922502940c1cc70f8d9d21bc859a60de8c99418ece614000945f2c438d3695ce944762ee93dcee7392f8aebcdb22e6e9f7caa02af911a641a783007764894f4832b362851fc4906cf099463c5efac17f4d3d6a7eebfc031f0c6a432f93525dcb019cd7b9ccfc2411e62b287b38e6cb449afcd3e8f966ab4f351cfbc72f1aa48cf6d7aab77ad8a2ba048544139237e8115586ea2349bfd055ab0ea7c51cef22c9795d7c1846673e4b4cd25e90de851fbd29ea89ee5e09d94b5bb7929db7eebe7b0d8f693a0b8a6c57122eae6b9ec58b3afb970061d066a601538ce9a8e5a5ac315e6abd786694e7838219bb0645aef1c3cf0c2585b1d10b09b0b838fa2a62c66f59326cb4ef89f67d9f436a788898231d73d90c30e9edfb55eec0c8c0586bcdb25725e27a2230f35bc34220c32b187398074d79a75cac414ee8d735d97eebbea2f0e4eda2479bb12000e5df69bb0cb00142d4acfa79ed621d79c6e07b3634d0e1058a7a4d857d9067efac2848b9eeadd2517fe98193c41d2ebbb99c9e584ed5c07b2131b6add773ac66f39edc2469b5fe1b5772957775680f1db398a91eb6617162a0ac385dc606830039099a27d8638c4f64a5f9403ac31185af97605d8a13a1d674253b1b7681a32ee8dfeccf3143dadfcf280a73640900a8f8f94d1c42c54f341f0f4bc93f98933270c9fc2ef61b6185f38a950635e0152c6923bbef3cdd696765eb09b2bcb09454454dbabfdcaa1910174f5ee7593b1ea4a7f853f080d0a04437d56f269272ad62707b6cecab8d6ef8c7381c646dac114e38a75c026ab28112dc17fc0e2af00d63e73231711c081623920dfb588d0f2ab2fd91b739cb81a25a60ba6beaa59fa86f91f011ea68329ce51f6404b90b2d26b288ebc7206a65c26e839795fbf108bd36fca4a5933b2d9b2493ec5e84964b045002c6f228282df8c27bc6277995cb71d95e3e8eb71268eeb1e060eee7d8852255ad34e4a6b99cd12c4be6f9d70f5a371bf85f68254fea35ccba90f801ac4c0b63620eac365abbaf43743995790cc28d978373e0f7e3ef4982315ae059069c2762052401abfd8aad5b5111e6e911173c232e0be59b993a40a740b78c580f2c2f49384bea107fca170e7124511af29e2ccef7c49998d9524fd11ac242bbd51c094e2bb8f714999a44f7b3629e503d0a1e6a6cd7f4d7b3f7d8db195a2a9934850a1f012fb518e9e15a3d333499a8e0c35a2821140993440cb6f022db8274c7e60600000000000000000000000000000000000000000000000000000000000000008720ac10b082cdf2c03a8aecaebee74ab9c65c85cbbf5176cf14278f822fee2b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009b1ee9ee2bd995cb89ba6edc46a4b233bc4e16c3bf89645cc9733d7373c9a609d143f20491e42ce3db074ff4785669c7a102f27a304d77b372a35a3a77f4012a6ba22603a92ebfebeea1d79ce2af41ce570385187b18d9376978abfb8f6ab32d5f2df2ec7f6ed6c1567726c127015849fcc28846991fa5b078cab1cc80758220ecdc0f99a8a31119a368d6707d3cf47396abecf40b6abff0617506ee2c39290521385c4846527ced939f70a6d6d4a34969d780684ae25532adf02c9de2a59e190aa853584c4ecce7cf88297bb1ecd0703bf76218e2e7ab8ac31beb4901a6b81c8f03109aa62b1bb9988a5f259a3ce7a4da2937d676171bb569490cb0d1b37e0c0464f9fca0940ef15da148d3f70fc2509bed05a8ed0fbda4af1be2fbcf67ab211ad57172fa021af88e19aa35fdc8f467742513f52129a746be7c4841c839c200693316954a9f3aa982896a0b03fafcc723da566494dfc9cc65a04b746e343d00d1831de406fb3b4c913d7221410cdd4c63cc73e4d1cc9b36ae699f8c0f47041361e32d8abe2efd68cb8f17140864d4a045e5ec68055185a9d91249ddf2adf2275617d4678c4f6ecffebf3dffdc5ebdf8934a68509e80ae537926ceb34b08d30ce5f74b02cad07d83255edaa575a68f5434de21798206c34e6efffefc9c38d51399de07f01113632b26dbdd9c5a27ed108f9bde4351ae6c550f4656e4ff6736196d5f75f790b0512ff38900e0002332d10250bc124b8920dbf15063fb922e6f0c092583dc5d970e73b52a9f5cb167e9deafc438ff5512b4513e8359db11040606c6170fd16a96c4641d86e496ce108b11c34b7a3126f877600f60c45fab1edc24773636b1e44bb37ff7858bcbe4923226dd0a7d8eb21e8ab576cc99f08b0b151127ee6acc57d8a0a0df6e06a9f284e7f845434e82f05040d85d40f64cddd55e306c4977bde42b79d2b523dfb95e4cffc0b69a1598bb1579ae367fbe4c55808b17537f07f106e13c10f93b9790389ec26f41be04b9f63b838c0a50ed3234671708f7f6fbf572200ea30c9d06b20f973649856fb16453d4497b478a4f15b46d662689498faff02abead46729adaf724def10cbd619428ddc932c2b6cb5fa6c97609879c32360634a8013b2714e98e1e9e77c2e8726208293df101f1fd511a342c70"}} \ No newline at end of file diff --git a/axiom-query/data/test/input_verify_compute_for_agg.json b/axiom-query/data/test/input_verify_compute_for_agg.json new file mode 100644 index 00000000..a230042a --- /dev/null +++ b/axiom-query/data/test/input_verify_compute_for_agg.json @@ -0,0 +1 @@ +{"sourceChainId":1,"subqueryResults":{"results":[{"subquery":{"subqueryType":1,"encodedSubqueryData":"0x009165ed00000001"},"value":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"},{"subquery":{"subqueryType":1,"encodedSubqueryData":"0x009165ed00000047"},"value":"0x2a4a0345b08e0c50805158030213038e9205244c5100b7223068018ef1648009"},{"subquery":{"subqueryType":1,"encodedSubqueryData":"0x009165ed0000000c"},"value":"0x43727578706f6f6c205050532f6e696365686173682d31000000000000000000"}],"subqueryHashes":["0x162136872c2a5889ac1f6f816de9a0915382f52d308ccfb85bf51405f01f6e57","0xec9c61c069dc9edfb82f6e76435797487af1d8b8f040b26deda52deff1c9e73b","0xf36a01afa5be89a2e56cb5656e45a5547d917141f6e3c153b140213abb204059"],"numSubqueries":3},"computeQuery":{"k":14,"resultLen":2,"vkey":["0x0001000009000100000004010000010080000000000000000000000000000000","0x35b8ef309e3b65bfbeab8266c382fcd073284769cc8e15fde28ce260b0e02b28","0x7a75d7385d618442668530435a6926c8d39f686e78ca020a69969ec8be555615","0x0000000000000000000000000000000000000000000000000000000000000080","0x0000000000000000000000000000000000000000000000000000000000000080","0x0000000000000000000000000000000000000000000000000000000000000080","0x0000000000000000000000000000000000000000000000000000000000000080","0x0000000000000000000000000000000000000000000000000000000000000080","0xea4ec579288ca155462bd5dfd2222cf4a7adaa79b11b48f53ec2cdbdec87fc4b","0xe38d2d4d1d79a078ce281e41a3f4b9e5316909f5c7c5388141c00bcee2042424","0x9a8a73c61379755ce23a0e5370405cd4cc79d66af6d9d30e61a4ea3e8bd4ab47","0xc8822d094658052bcfc4e79e3e8341db5cb3ece6dc77a7d369f466f986334a03","0x7084bd1bce301a47e81c5d6d2f7ce4df819e6fe92a1426da80727e87b89f9818","0xfcc0f0a01464e41ebe5f5d15015e2a51fdfa9da60c8f944d6a68f420f2d7d162","0xb3dc5cc56d6cd5148c12906ea8715aeb14b627a43236083ecfd7786edc541641"],"computeProof":"0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493472a4a0345b08e0c50805158030213038e9205244c5100b7223068018ef164800987d532ace52383a0b5682956dc477135ed31253cbe2477b959f6bfac282e3214eccbd6ed1acad6639d66f291592c7391d8fd4124df300c73f5fd2fc5dd2d7c4e4b0ec319e7dad51d5fb8d5f8779d2940ecf0c7815ad11c015a81b56a86525222f8cadce02976c8348a7c190c269b949785faf65b2a94c6784d2e07469f33e014ce2c4a867c190b5a0888d1dbc3f7f5b038dcba717268c6690e8dff359aad134974229a9b1693f90114eb1a65fc978e2a4911d56bf422187bfdef4a98170fca457f392c45fd3af8f1c0ce10c771376df53b6d15635334e475806a11dac5d5bd056fdb6641762d4881e74e19fb05cd027eb7dcb696965a4fc5a41e9aa1519209161e7690e797276e9002a5359a935f300735c79fed6e8941b71eb148f0eb4ed354a30bef0c1077ed2e58ca5868cb9df86147707a4bfdd0f437efe84d549900c70a6db8f9a7a7c3056fc9e765a930d867ac5b7913844be1249b31c93dd329d1ed6a4cadc5227abbf36b3d67f91c1d5920c0073460897b5e68cf3731afd94a39d118e733de216e63a2598ff00f4421cdc139b327a19f15db4e28932fe302716e3415aae366692fe052fa7b3cd2ff1eb1ed8bd1bc3d31c009cabf5d797bb136b023021543d4830df7f33a82f64051d2ef06605e8051de8aa3ddfeff1c74fd11a5f16f08e6b763550ffc6c25c2c780eb193324007541cac9cb89b9102806f055d8fc6d67480da32621700ea91b3803e147721b65424d1fc9bb4ac1eb7fbcbca7c265053ce0a358d2fceb2119086a02d1987fbe94688f607b2164950ee9db16dadd142f0154996e00faffcab77c440b4b9190767a2c62984e3502c6ddae6640d8b37707054fc7066689eb908501ea79d689ee9bad83529e7ce6149aea0d073f4048c4087d4736bffd49f9797f9fb37b792ef529a04bc58b11465508bf84ca108e663c292b9156c487d115fedc0ae58fdfee0edd570e88a0653e3920ac319da42b89d90ce4c6b3ca4da385ac07fb24708a153e86dfe3d9b322ba755c1f62505fb282491c704132f2497b5a84d5c638ff6b0dec79da81aa711aaa6d255c4f82ffe869110a3834e70f4525ecd1c08c202427719d4cad2d5585a364753faa4f6a4ac2aed01f657bf176821809667c5e995788cb9bffb917688e54df15e990ca6ad9e7725d07b2ad33c7c5622fbfea8be4a4241876fb84180d8f86f54fa4897bc14f47acbc2e04b251109e995493a19a4feb9621df65f5ec42be7acfa16f1d174964761cdb081e54711bd452fd0eb1be3f719a2bb788bb5ebae76a71f7e65d4bb950ce16b71475982e553d3484e272f38b6bb18533b8903798ec4845f4c0cf0500048ebae4159886ab22dbfa14e6c4a5cc021e16f66861606aced41374d9b5f625d77ed0b613ada9227ac6f913d7628f2f3531d6ee046539129e47ceb75bd984fbfb700797133c4cee039803e18912bb82cb68b73fbb1dea7b884d17a5b36cf15ef64756890000000000000000000000000000000000000000000000000000000000000000009c4953811a956be91eb1d8718412027d9486f54e5523703e00d5dbe25526351e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000976fc938729f688d789caaeeedab85de6b34f9d6ddf4f2593e3f3ac09012102b9d9cab3f74331a33308ff77920ae33ab593b4ce2dbefed5c90daaa719fcaca02ba8e3afccb8cc4f322e0b2522582650c88db6541bdb8fd289ebf6abd266f2c10321eaf7e17244e0ee101b951729008e0e7e956c3002940030e981f4916236308e1f784bace2038f78f3a03fc73563817324ab570bd7b91e6dde2a5c0a293b105e1575a0fc52ca36948a3ede870349f170c1afb8388fa6d9d36e0721c99968e0b0388ca8b5addfb8bccee458b0dcafe1945abe84653eedc641b90f1b7dad84e18eb31d350adff47640f4d543907719f8ede887b44279148cc3018014792e39d060b857b4eb70c7b4659b3c8d08a146d9f150f6bbdc2439201c8ad449e9c1c910283637b97584e8e747eabad1c87e88ed4f14ef4c74e637d726d9928204d8ecc108bd1b612c2465b12d8338e34215e54f1306706fde6f54e2fe819d7a78472fb2330eb174b40c6450b9709d2cee0e8b59bd3760b0aa6db616345d47220676c020d2783ed73f1eab646d3efb3a0437fd4e2fee30ba6890a902b7c69f3c4b242590147799f8558feaa3363822e3742c0e9e25e294464e7eea0465645c5a6f9a320281e95c0e91823c5ce9a2d723b4d63f0fbf76ed6a82f1128dfa6b51a784a58a40c498f65bda7ce26cb4b910c85728befd794d28ab13cf4c6505aaf4813603b83180497afe193ae72aebc38ed1688065015a04bdb147b2af5d2d9deafd26c04bf1f6c0c9ff7855c145c7e0abea10b3d333a6d0b28db5db9900891153bd61597a81392a5506568a0852dc2fb5220a37d49de3913ec8c4f15e1affd945117655e922289a2b49f577309d897093e700991f767332b08c969d679a2a8173df4913ce5218690b276dd99b35aadadb0e97f6e9574af997cfb7ba5375f3deaffc3a8d1972aed9dfe7d96adedd065f1db4a78f88826e4bd5402c62d483f8e70d9a2b40e73094c0ed919da4a69384ecfe4c3c504834186e34f81fc278e159a7c5d5391a0e308b90ffbabe6372b1f33082234c8b0bfa62196b7800a3d18d9e0581ece59556e2d889c509b5d9787069306c5283d3b3c0729f7bbd1dad1a24534f6daad74b36c28cbed899959594d878e5a80e2e4a62020aa705c867b9ba02a027d979768dc3c18"}} \ No newline at end of file diff --git a/axiom-query/data/test/promise_results_header_for_agg.json b/axiom-query/data/test/promise_results_header_for_agg.json new file mode 100644 index 00000000..40ec3290 --- /dev/null +++ b/axiom-query/data/test/promise_results_header_for_agg.json @@ -0,0 +1,25 @@ +{ + "results": [ + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 1 + }, + "value": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 71 + }, + "value": "0x2a4a0345b08e0c50805158030213038e9205244c5100b7223068018ef1648009" + }, + { + "subquery": { + "blockNumber": 9528813, + "fieldIdx": 12 + }, + "value": "0x43727578706f6f6c205050532f6e696365686173682d31000000000000000000" + } + ] +} \ No newline at end of file diff --git a/axiom-query/data/test/promise_results_keccak_for_agg.json b/axiom-query/data/test/promise_results_keccak_for_agg.json new file mode 100644 index 00000000..021610b5 --- /dev/null +++ b/axiom-query/data/test/promise_results_keccak_for_agg.json @@ -0,0 +1 @@ +{"responses":[["0x0e12a985789e947d5c94b2f0dfee712cdff776434e0a8c13379a81ec9cabbd90fb07e7779638fe0e542e6a55e3e4068d1a0a71e178637553e2abe5177c3a3d7d",null],["0x19e99f81e6a4933a1dd41191733bd7b11ee9dbdf74f3a508e04514eb5053a57ea1aba88a1e5835420920b9de5a5c9489f57d8bee868c142f47f3716b9e1352c6",null],["0x1eeaaf21efadf32e39e4db63eedb3b791e5586e373bcdc783c42ea6a8028b48907449018b55a87abbc033d85102853995a41aeaa8544452dc2240aebda5e9cb3",null],["0x3020fe1044a376596e399087fc0e5cb8783a853e2c85c7206514dc2cf71e42f31b7b0f134de7422015a5421b9d4c0fbd370493f06144b53b5ca3ebf419756f00",null],["0x4755084f1c80a73395852c7ede586dddb3b8db7828e4ad2cf3259f693ac4f8750000000000000000000000000000000000000000000000000000000000000000",null],["0x477c055e69de14e3bbfe2af0389e6c3ac28ffb5b0cc8fa26b543ac47857fd64616fed94ec0cf0aa4b075e19cb2b6ff780a6c11e9b1c60195b036c1b8622c0147",null],["0x5079605c269d43ce5a544d19e7832911a862f74b0df14d7b604232869b39ed78a0d8feace83653ed55151dbf0959b0524c9e3187f928e41aab067daed472d269",null],["0x5459bcc28c212f4f7ba9b7075fad7c3f2292e83997007998dd27d5369d469616752448ae955fe7ab9d28a97fb104fccca709d7ef6c73fd814ae3fefb5d211d7b",null],["0x594f03e3c07dfc61489a41b9645c2715fa0cbeebd2c13d25b30add4bbfd315950000000000000000000000000000000000000000000000000000000000000000",null],["0x5a58cfc592d45892857d908e3c8c2da8c40b1cab2d4c941546ed9232114d1b373409945b63c2e1f8a04cc3236cf0d4ac7a340695c8f5641a526dc6c783ca91e0",null],["0x615ab700eca77f00034ff0b208a7c9e876cde7cd947aaebb2cb004ce461a1e43b2838c6d8d70917ab69e952493e18b67e0e1f22745f3b30528bcb4346e47455b",null],["0x655090a6192edeb76e76b01476071607739746e8197158c58ed6d3a001021e43c3c2b03b5831d3cf7bfc8c96e38b1f5e1c3a19745b220cdc5f9eec15f6a1f9c1",null],["0x68d509b78ca15e200209038efa53ab5fc30eef61b87bd1c3417fd4eab256c255c317e0c0c4ac8c49716549e55a7ab0252ba4116ea7f7a518fb1441d02ef1e248",null],["0x6d495f5672d5d7482e1063eb1e1381edb72cabfdfa2c218cc38461136cc513970000000000000000000000000000000000000000000000000000000000000000",null],["0x79a9b620dd7c6e8a40e973ae2193a378de5c45d9ff7917e3f31d01b019ac0527791c7933517944067f8dcf8cdbb4bf9c634d07c0f1f91ad3648920bc29a6884d",null],["0x79e3279ab366ab5dc5c1f8ad4aad3ce7689086e4f8e25572c835f43d4a826432250018a435d17b5473e05f884ea1af54be447730a201fd72a707da51a820345c",null],["0x82dd59d4c6cb74831ca6eebc0b9beb938a7c0e33e000c160264520780daecd8737d210b023995e28e43315bb1a98eeddde85a35b7321d7162f51c9425fd5f905",null],["0x833b246c8bdd751a108869f958b69cfaee4a03c036905b90c39bfc986bda80ee0000000000000000000000000000000000000000000000000000000000000000",null],["0x9071e895206e9ac84c8ae51e7180e9479827ca3288175f4615fdc724e28008d793071e157ae565f7dbfea93c3da2e03caf192a8d97e1e4f752cad4b03dad40a8",null],["0xa3e48aa8f6637e4a196bf2e5c51d63b766cee11645a1e2cb20e38fd6c2f676163eb43d4625448a26adb10215f7f092a6fc8622a21b35048c2b2ef1d991162d84",null],["0xa60d89ad021ef63b49250141377cc3e835945e5d4136ffc1909047c4ecca3af70000000000000000000000000000000000000000000000000000000000000000",null],["0xbb33fff29f3094bd9de4d3668fd20f2a45f8028c0c532f4e10b2368ab5c783aa22b2e40d32a936ed08307b41db986d35160fe5917c8d2609d27e7bd02f4b767e",null],["0xbe8c5ff2ada208b4577bd49d7660fdaf3e17983530f5d1d574126c5ee743de900000000000000000000000000000000000000000000000000000000000000000",null],["0xc364b3480c792d14112d17f167581d8fcf74b6d7805bc1ef1526f5fa7415be400000000000000000000000000000000000000000000000000000000000000000",null],["0xcea8ad8ff6608360c6c43b229e482664a5ce0f23090fab77acd12803c6094c03f7533e213fc68258934880ec7189125fc6071e01f20aa465e56f7c7ecfccb800",null],["0xd85f3204700a79681b53fd8304a44141bd2af2fe44b2265aed636c16ed6fe223c68b30cb54482f31743afe91abb6841704d275a6a378f7faa389d387c384d568",null],["0xdad303cf18a459358a346f84e5e3a3065256a3584769c3cfae5b1729e168ed52223b4e206927956a87b2bd3be0b39f6176172c13d773b99e11b4d0ac8c1307d4",null],["0xe2327533e28e723bb66ff3d0bfcb32bad0c070e8935d13c0a36a3126c8f3b7d082c76ea3624a6f4e2d62c5375b3dc0a86bf69810715bc7fddebc92937cd4b0e8",null],["0xf3d9b0595399d791bb7b2e91630175518483c8c517d1097e4b67b0ae78c1bb21eab78f0a8534330a079c8179e08f594fe8ed3483c2df1c46e7177e4839ab2114",null],["0xf47e6584af17667881494720b50dd063d0900b288438df7a37e2e91440aedd2388e177538a14ffca910f9c6b56f6bed99d1f748fa103093f372ecbe9db819e38",null],["0xf99a2e4fdc253c869f26e9e13e8794c41a3bebb7498fe90513ff1996378db3d2a7d918baae6f3c07d7d0cbfac129925e9080f61cd113b60dc3970edff82f7338",null],["0xf90218a0d85f3204700a79681b53fd8304a44141bd2af2fe44b2265aed636c16ed6fe223a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794249bdb4499bd7c683664c149276c1d86108e2137a09ce4561dfadb4fb022debc7c013141a36a23d467f4268c0e620a7bba05b174f8a0ec4c8ec02281c196e3f882b4061c05f4f0843a0eaf72a3ee4715077220934bbca00dfecab5d97a4da361b92c29157cd6a5089b55facee007eea16005bfa8d82694b9010019827c809158a49a418c22c32ac510b061e8807074412014a10a0000260c24e72a4a0345b08e0c50805158030213038e9205244c5100b7223068018ef164800936740064d6428223d415104ebcc1a16566206424a707d0514481a60080090a345e00072446d000210129c58c138a5822ac68620618e7acaacd34af1655343aca334011d04063a29203a5a2229c44407a9681ed3a60960330706201764a382a170e8c20810005ec000078c19589cfd08033508015e14a053022890b4e60b20a51349024866b0c00a7005b31410673c6c5144872f284b9e612bc180018d152f142801ba52780a3d2080d90ec9605c6fbcf4934019192443014440896b64f504df88707f26d7f80c9a2839165ed8398243083981192845e5040299743727578706f6f6c205050532f6e696365686173682d31a0174c6e45dc64f9ba8ff83f63eac9a51fd602da5118003689ae944986a309d682888ee012321dd76cd3",null],["0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000348bab8f4d2acdd4022f4b7becb8ba36a260bd9f699aa4df0a0660dc772ddcfcfff5804d8cd309c79ff26f19ecc6a9cef13556c9d5ddb18afae277d71ad7d4114f1a2ab29b70f47ff5c88dc7048bb23fcc4a59fbcc3b9f0e9529297afb28cb8a995310788441a017c79a1fc0edab3074e6d5495712b35e120c446301b8b60d8c0000000000000000000000000000000000000000000000000000000000000000c4933b4a3d6a41bf6c8009e24436a19e3e91a6f4198a0f498ade1179d29785000000000000000000000000000000000000000000000000000000000000000000dff0f5a47ed23e4dd682fa5b20c861c8d616f0124bf3366def9f620e1569ade60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004755084f1c80a73395852c7ede586dddb3b8db7828e4ad2cf3259f693ac4f875",null],["0x0001009165ed00000001",null],["0x0001009165ed0000000c",null],["0x0001009165ed00000047",null],["0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493472a4a0345b08e0c50805158030213038e9205244c5100b7223068018ef1648009",null],["0x0000000000000001162136872c2a5889ac1f6f816de9a0915382f52d308ccfb85bf51405f01f6e57ec9c61c069dc9edfb82f6e76435797487af1d8b8f040b26deda52deff1c9e73bf36a01afa5be89a2e56cb5656e45a5547d917141f6e3c153b140213abb204059",null],["0x0e00020f000100000900010000000401000001008000000000000000000000000000000035b8ef309e3b65bfbeab8266c382fcd073284769cc8e15fde28ce260b0e02b287a75d7385d618442668530435a6926c8d39f686e78ca020a69969ec8be55561500000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000080ea4ec579288ca155462bd5dfd2222cf4a7adaa79b11b48f53ec2cdbdec87fc4be38d2d4d1d79a078ce281e41a3f4b9e5316909f5c7c5388141c00bcee20424249a8a73c61379755ce23a0e5370405cd4cc79d66af6d9d30e61a4ea3e8bd4ab47c8822d094658052bcfc4e79e3e8341db5cb3ece6dc77a7d369f466f986334a037084bd1bce301a47e81c5d6d2f7ce4df819e6fe92a1426da80727e87b89f9818fcc0f0a01464e41ebe5f5d15015e2a51fdfa9da60c8f944d6a68f420f2d7d162b3dc5cc56d6cd5148c12906ea8715aeb14b627a43236083ecfd7786edc541641",null],["0x0200000000000000016d4b33819da0c6366eee8749885f19a752fa5aacb826ade03dd97528269c562c0e00020f000100000900010000000401000001008000000000000000000000000000000035b8ef309e3b65bfbeab8266c382fcd073284769cc8e15fde28ce260b0e02b287a75d7385d618442668530435a6926c8d39f686e78ca020a69969ec8be55561500000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000080ea4ec579288ca155462bd5dfd2222cf4a7adaa79b11b48f53ec2cdbdec87fc4be38d2d4d1d79a078ce281e41a3f4b9e5316909f5c7c5388141c00bcee20424249a8a73c61379755ce23a0e5370405cd4cc79d66af6d9d30e61a4ea3e8bd4ab47c8822d094658052bcfc4e79e3e8341db5cb3ece6dc77a7d369f466f986334a037084bd1bce301a47e81c5d6d2f7ce4df819e6fe92a1426da80727e87b89f9818fcc0f0a01464e41ebe5f5d15015e2a51fdfa9da60c8f944d6a68f420f2d7d162b3dc5cc56d6cd5148c12906ea8715aeb14b627a43236083ecfd7786edc541641000008a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493472a4a0345b08e0c50805158030213038e9205244c5100b7223068018ef164800987d532ace52383a0b5682956dc477135ed31253cbe2477b959f6bfac282e3214eccbd6ed1acad6639d66f291592c7391d8fd4124df300c73f5fd2fc5dd2d7c4e4b0ec319e7dad51d5fb8d5f8779d2940ecf0c7815ad11c015a81b56a86525222f8cadce02976c8348a7c190c269b949785faf65b2a94c6784d2e07469f33e014ce2c4a867c190b5a0888d1dbc3f7f5b038dcba717268c6690e8dff359aad134974229a9b1693f90114eb1a65fc978e2a4911d56bf422187bfdef4a98170fca457f392c45fd3af8f1c0ce10c771376df53b6d15635334e475806a11dac5d5bd056fdb6641762d4881e74e19fb05cd027eb7dcb696965a4fc5a41e9aa1519209161e7690e797276e9002a5359a935f300735c79fed6e8941b71eb148f0eb4ed354a30bef0c1077ed2e58ca5868cb9df86147707a4bfdd0f437efe84d549900c70a6db8f9a7a7c3056fc9e765a930d867ac5b7913844be1249b31c93dd329d1ed6a4cadc5227abbf36b3d67f91c1d5920c0073460897b5e68cf3731afd94a39d118e733de216e63a2598ff00f4421cdc139b327a19f15db4e28932fe302716e3415aae366692fe052fa7b3cd2ff1eb1ed8bd1bc3d31c009cabf5d797bb136b023021543d4830df7f33a82f64051d2ef06605e8051de8aa3ddfeff1c74fd11a5f16f08e6b763550ffc6c25c2c780eb193324007541cac9cb89b9102806f055d8fc6d67480da32621700ea91b3803e147721b65424d1fc9bb4ac1eb7fbcbca7c265053ce0a358d2fceb2119086a02d1987fbe94688f607b2164950ee9db16dadd142f0154996e00faffcab77c440b4b9190767a2c62984e3502c6ddae6640d8b37707054fc7066689eb908501ea79d689ee9bad83529e7ce6149aea0d073f4048c4087d4736bffd49f9797f9fb37b792ef529a04bc58b11465508bf84ca108e663c292b9156c487d115fedc0ae58fdfee0edd570e88a0653e3920ac319da42b89d90ce4c6b3ca4da385ac07fb24708a153e86dfe3d9b322ba755c1f62505fb282491c704132f2497b5a84d5c638ff6b0dec79da81aa711aaa6d255c4f82ffe869110a3834e70f4525ecd1c08c202427719d4cad2d5585a364753faa4f6a4ac2aed01f657bf176821809667c5e995788cb9bffb917688e54df15e990ca6ad9e7725d07b2ad33c7c5622fbfea8be4a4241876fb84180d8f86f54fa4897bc14f47acbc2e04b251109e995493a19a4feb9621df65f5ec42be7acfa16f1d174964761cdb081e54711bd452fd0eb1be3f719a2bb788bb5ebae76a71f7e65d4bb950ce16b71475982e553d3484e272f38b6bb18533b8903798ec4845f4c0cf0500048ebae4159886ab22dbfa14e6c4a5cc021e16f66861606aced41374d9b5f625d77ed0b613ada9227ac6f913d7628f2f3531d6ee046539129e47ceb75bd984fbfb700797133c4cee039803e18912bb82cb68b73fbb1dea7b884d17a5b36cf15ef64756890000000000000000000000000000000000000000000000000000000000000000009c4953811a956be91eb1d8718412027d9486f54e5523703e00d5dbe25526351e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000976fc938729f688d789caaeeedab85de6b34f9d6ddf4f2593e3f3ac09012102b9d9cab3f74331a33308ff77920ae33ab593b4ce2dbefed5c90daaa719fcaca02ba8e3afccb8cc4f322e0b2522582650c88db6541bdb8fd289ebf6abd266f2c10321eaf7e17244e0ee101b951729008e0e7e956c3002940030e981f4916236308e1f784bace2038f78f3a03fc73563817324ab570bd7b91e6dde2a5c0a293b105e1575a0fc52ca36948a3ede870349f170c1afb8388fa6d9d36e0721c99968e0b0388ca8b5addfb8bccee458b0dcafe1945abe84653eedc641b90f1b7dad84e18eb31d350adff47640f4d543907719f8ede887b44279148cc3018014792e39d060b857b4eb70c7b4659b3c8d08a146d9f150f6bbdc2439201c8ad449e9c1c910283637b97584e8e747eabad1c87e88ed4f14ef4c74e637d726d9928204d8ecc108bd1b612c2465b12d8338e34215e54f1306706fde6f54e2fe819d7a78472fb2330eb174b40c6450b9709d2cee0e8b59bd3760b0aa6db616345d47220676c020d2783ed73f1eab646d3efb3a0437fd4e2fee30ba6890a902b7c69f3c4b242590147799f8558feaa3363822e3742c0e9e25e294464e7eea0465645c5a6f9a320281e95c0e91823c5ce9a2d723b4d63f0fbf76ed6a82f1128dfa6b51a784a58a40c498f65bda7ce26cb4b910c85728befd794d28ab13cf4c6505aaf4813603b83180497afe193ae72aebc38ed1688065015a04bdb147b2af5d2d9deafd26c04bf1f6c0c9ff7855c145c7e0abea10b3d333a6d0b28db5db9900891153bd61597a81392a5506568a0852dc2fb5220a37d49de3913ec8c4f15e1affd945117655e922289a2b49f577309d897093e700991f767332b08c969d679a2a8173df4913ce5218690b276dd99b35aadadb0e97f6e9574af997cfb7ba5375f3deaffc3a8d1972aed9dfe7d96adedd065f1db4a78f88826e4bd5402c62d483f8e70d9a2b40e73094c0ed919da4a69384ecfe4c3c504834186e34f81fc278e159a7c5d5391a0e308b90ffbabe6372b1f33082234c8b0bfa62196b7800a3d18d9e0581ece59556e2d889c509b5d9787069306c5283d3b3c0729f7bbd1dad1a24534f6daad74b36c28cbed899959594d878e5a80e2e4a62020aa705c867b9ba02a027d979768dc3c18",null]],"capacity":200} \ No newline at end of file diff --git a/axiom-query/data/test/subquery_aggregation_for_agg.snark.json b/axiom-query/data/test/subquery_aggregation_for_agg.snark.json new file mode 100644 index 00000000..a4354b0d --- /dev/null +++ b/axiom-query/data/test/subquery_aggregation_for_agg.snark.json @@ -0,0 +1 @@ +{"inner":"FAAAAAAAAAAAABAAAAAAAAHBptBV4dis9eg0O8UVrhJF8CV9MapNnhZySpxsS2QweDNngjEeDi9bnQTa+jledHDmICW2AmSF4y30H09GFCpfLUSrvavoWuozp/Ad06Oy4EeZpE+/7J6vK4O/2LANIgAtAAAAAAAAAP1wJQgtQLtYxZ1UeINFm9U9TTxHDLPrfBE0D/b775EmPl1tgtomNWELsHXB2NowpSKuA5v8Nc86V3s76mtgXiYKXW5zBpg+qfIs9I5cAO15zGVMkQaks8/L3wVTXXewD63eNbh7H71R1ehsVUMzdMYqQrya2jANm8sCpWEmGoAYj4lY8NsGPShduinYDy1ye1qplMXRyStcM3yWs7YAEAwecBaSZ0SxzTfksz2EEQNJkGyq/nAWnZIoV4lvUgsOKS3WLLzQ4wLJpCQCZXrIQuRdM56nxWWp0LaUsyViOzAmvyeOK2O5LwT9V5DY/u0rapNtd02GEfCcbNwBCP6TQC7rDL+EbtDzzVWDP5YCgy0GokQLTp3O/oyrMKbhNKjlFEYfk2EQ97zzrhTm8QYa9xIS3yf4EerEn9s1RkKHNdZoctnsVhW6f6p5iJ9F+9czQ1mTPoFi9ufv6gmXAJZNoxP5x8xXE05gO/s8lbqqM2wVDPXNR6uLU3RVBE/ZmfdNQHLU/cg4l0uGr9n8i6uP4vk6N6+RAPjatx+ZEF2i+Y9SISuZvMNFmz2kv8R04D9ryazVRcAIh8hdDlEB7XS8ZgN+ARK5xcqSMghFTvgjb1nybM3o7K8ePpDswJjsCnk7Eh1DiSunJBYWC9vLT+IQLJIDG0n34hBvZNRA/SIGXetoAd4eXIR/A8rmwKj2/lgGYJHjmlRwZnO9x9NRIW4A3CPqo8mcwJ1xJfGlNoktJBFac4H0hJpGFqt0162FRDlNU0TTFzmbpwVo7WzIxntn4Z3Nx0I0V1bcFwmeaHIbi3VuRJyFDAsOjR2VenzfnVuvjfdWSZHOW54yyYscR6MFEGc4IASYPHbqnsOg3J/9rsxdWM/j69OB+Pu08RUIVJw0VlzTSvNwCFHzUnc8X/SQKyOIb6qaOVn00rd/wJRPZl8e9Uni4vN1+wRKt/3wz4tc3CFhs437VAgW/CeCZNndsQbLnsiQGhPGi3sU3o8jxRKRlNPT5zTuVN+k6i5cOWvDbf3xHP3lUnNh1bfsjzpfgqW/pD1th/+VL4bB6hAtqahIvcyMhcWCnfTrggKg8wTu0KtO2tfNlP6iy2BXBuFUIhoDz6Wfie7HG1PLzwD1atZa2bweRAmMrT6Juh0Fe3INa993dROftykQTjeAF0Xaub8bfuJU2Px/MpH5mv9qRdcc+ZnBTG9NaeT9g9+yfqlluLWkbwciWXUoF8mZhqORIRRIOeenKqdkXXeI/1WQC/OSFa8UUWIHLbQsO3+KOwOcU0PnJAReQHJOIr7Ce7+wCBLcgh28fcvZUcUNN0O36nwe+dBG5jMQW8fsOikVHtfS65XKTU+uwt+pJj1cNqjbsmlI64HrW2EFUKSmZXm7pe/wdKlecbJPYLT8tuSKfTD4aAMA4lz0hW1xY0x2SX+FfOX1xXWtfBlXdndicwTqz38YWqtbKM5BFVTIXSfiTPgFwfRjIOurzoPSrJSoTrjXw0S2aOUyeMyMAV1Ik2jRi7kHN2leNtyMAcRk853u9z6yWqj59phINK+9IhQC58m+pBcsIqpLVWTFr1RUnoFGnCtgcTQ52u+MktEd5XN8w49qEfYI+WV6CnfnNU4x7dSnLV1/l8trE6469JPp1X8pBGk8GZQrgRmeNaa3QzQ8WiwLDLgTUiQ39LSxc7qOeQoI+/KTc6ABoXtJoG5pZV08pOZcEVgVseuzKGxFsYim5NMJonhaZVgaEyZmt5nXJTX05wN0esw2QtLVU1YB23pw5Uc9e/VYaHOSh+/mwfvbAl1DSWdBHWOjK93KJD/4VYrtSmZadbTkL/zssKtAc4VIM0hMq86z0byeZWpmCh0SXb5u5PSAR2PGwlM7S5iAGkGKhR1ZctcwSRiVdPi6gC0yfCf1qPW1tHxhttHsy8c5zCOPSAEAAAAAAAAAEgAAAAAAAAADAAAAAAAAABYAAAAAAAAABgAAAAAAAAAQAAAAAAAAAAMAAAAAAAAAAQAAAAAAAAACAAAAAAAAAAEAAAAAAAAArwAAAAAAAAAuAAAAAAAAAAAAAAAuAAAAAAAAAAEAAAAuAAAAAAAAAAIAAAAuAAAAAAAAAAMAAAAvAAAAAAAAAAAAAAAvAAAAAAAAAAEAAAAvAAAAAAAAAAIAAAAvAAAAAAAAAAMAAAAwAAAAAAAAAAAAAAAwAAAAAAAAAAEAAAAwAAAAAAAAAAIAAAAwAAAAAAAAAAMAAAAxAAAAAAAAAAAAAAAxAAAAAAAAAAEAAAAxAAAAAAAAAAIAAAAxAAAAAAAAAAMAAAAyAAAAAAAAAAAAAAAyAAAAAAAAAAEAAAAyAAAAAAAAAAIAAAAyAAAAAAAAAAMAAAAzAAAAAAAAAAAAAAAzAAAAAAAAAAEAAAAzAAAAAAAAAAIAAAAzAAAAAAAAAAMAAAA0AAAAAAAAAAAAAAA0AAAAAAAAAAEAAAA0AAAAAAAAAAIAAAA0AAAAAAAAAAMAAAA1AAAAAAAAAAAAAAA1AAAAAAAAAAEAAAA1AAAAAAAAAAIAAAA1AAAAAAAAAAMAAAA2AAAAAAAAAAAAAAA2AAAAAAAAAAEAAAA2AAAAAAAAAAIAAAA2AAAAAAAAAAMAAAA3AAAAAAAAAAAAAAA3AAAAAAAAAAEAAAA3AAAAAAAAAAIAAAA3AAAAAAAAAAMAAAA4AAAAAAAAAAAAAAA4AAAAAAAAAAEAAAA4AAAAAAAAAAIAAAA4AAAAAAAAAAMAAAA5AAAAAAAAAAAAAAA5AAAAAAAAAAEAAAA5AAAAAAAAAAIAAAA5AAAAAAAAAAMAAAA6AAAAAAAAAAAAAAA6AAAAAAAAAAEAAAA6AAAAAAAAAAIAAAA6AAAAAAAAAAMAAAA7AAAAAAAAAAAAAAA7AAAAAAAAAAEAAAA7AAAAAAAAAAIAAAA7AAAAAAAAAAMAAAA8AAAAAAAAAAAAAAA8AAAAAAAAAAEAAAA8AAAAAAAAAAIAAAA8AAAAAAAAAAMAAAA9AAAAAAAAAAAAAAA9AAAAAAAAAAEAAAA9AAAAAAAAAAIAAAA9AAAAAAAAAAMAAAA+AAAAAAAAAAAAAAA+AAAAAAAAAAEAAAA+AAAAAAAAAAIAAAA+AAAAAAAAAAMAAAA/AAAAAAAAAAAAAAA/AAAAAAAAAAEAAAA/AAAAAAAAAAIAAAA/AAAAAAAAAAMAAABAAAAAAAAAAAAAAABAAAAAAAAAAAEAAABAAAAAAAAAAAIAAABAAAAAAAAAAAMAAABBAAAAAAAAAAAAAABCAAAAAAAAAAAAAABDAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAJAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAANAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAARAAAAAAAAAAAAAAASAAAAAAAAAAAAAAATAAAAAAAAAAAAAAAUAAAAAAAAAAAAAABZAAAAAAAAAAAAAAAVAAAAAAAAAAAAAAAWAAAAAAAAAAAAAAAXAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAZAAAAAAAAAAAAAAAaAAAAAAAAAAAAAAAbAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAdAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAhAAAAAAAAAAAAAAAiAAAAAAAAAAAAAAAjAAAAAAAAAAAAAAAkAAAAAAAAAAAAAAAlAAAAAAAAAAAAAAAmAAAAAAAAAAAAAAAnAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAApAAAAAAAAAAAAAAAqAAAAAAAAAAAAAAArAAAAAAAAAAAAAAAsAAAAAAAAAAAAAABKAAAAAAAAAAAAAABKAAAAAAAAAAEAAABKAAAAAAAAAPn///9LAAAAAAAAAAAAAABLAAAAAAAAAAEAAABLAAAAAAAAAPn///9MAAAAAAAAAAAAAABMAAAAAAAAAAEAAABMAAAAAAAAAPn///9NAAAAAAAAAAAAAABNAAAAAAAAAAEAAABNAAAAAAAAAPn///9OAAAAAAAAAAAAAABOAAAAAAAAAAEAAABOAAAAAAAAAPn///9PAAAAAAAAAAAAAABPAAAAAAAAAAEAAABPAAAAAAAAAPn///9QAAAAAAAAAAAAAABQAAAAAAAAAAEAAABQAAAAAAAAAPn///9RAAAAAAAAAAAAAABRAAAAAAAAAAEAAABRAAAAAAAAAPn///9SAAAAAAAAAAAAAABSAAAAAAAAAAEAAABSAAAAAAAAAPn///9TAAAAAAAAAAAAAABTAAAAAAAAAAEAAABTAAAAAAAAAPn///9UAAAAAAAAAAAAAABUAAAAAAAAAAEAAABUAAAAAAAAAPn///9VAAAAAAAAAAAAAABVAAAAAAAAAAEAAABWAAAAAAAAAAAAAABWAAAAAAAAAAEAAABEAAAAAAAAAAAAAABEAAAAAAAAAP////9FAAAAAAAAAAAAAABXAAAAAAAAAAAAAABXAAAAAAAAAAEAAABGAAAAAAAAAAAAAABGAAAAAAAAAP////9HAAAAAAAAAAAAAABYAAAAAAAAAAAAAABYAAAAAAAAAAEAAABIAAAAAAAAAAAAAABIAAAAAAAAAP////9JAAAAAAAAAAAAAACwAAAAAAAAAC4AAAAAAAAAAAAAAC4AAAAAAAAAAQAAAC4AAAAAAAAAAgAAAC4AAAAAAAAAAwAAAC8AAAAAAAAAAAAAAC8AAAAAAAAAAQAAAC8AAAAAAAAAAgAAAC8AAAAAAAAAAwAAADAAAAAAAAAAAAAAADAAAAAAAAAAAQAAADAAAAAAAAAAAgAAADAAAAAAAAAAAwAAADEAAAAAAAAAAAAAADEAAAAAAAAAAQAAADEAAAAAAAAAAgAAADEAAAAAAAAAAwAAADIAAAAAAAAAAAAAADIAAAAAAAAAAQAAADIAAAAAAAAAAgAAADIAAAAAAAAAAwAAADMAAAAAAAAAAAAAADMAAAAAAAAAAQAAADMAAAAAAAAAAgAAADMAAAAAAAAAAwAAADQAAAAAAAAAAAAAADQAAAAAAAAAAQAAADQAAAAAAAAAAgAAADQAAAAAAAAAAwAAADUAAAAAAAAAAAAAADUAAAAAAAAAAQAAADUAAAAAAAAAAgAAADUAAAAAAAAAAwAAADYAAAAAAAAAAAAAADYAAAAAAAAAAQAAADYAAAAAAAAAAgAAADYAAAAAAAAAAwAAADcAAAAAAAAAAAAAADcAAAAAAAAAAQAAADcAAAAAAAAAAgAAADcAAAAAAAAAAwAAADgAAAAAAAAAAAAAADgAAAAAAAAAAQAAADgAAAAAAAAAAgAAADgAAAAAAAAAAwAAADkAAAAAAAAAAAAAADkAAAAAAAAAAQAAADkAAAAAAAAAAgAAADkAAAAAAAAAAwAAADoAAAAAAAAAAAAAADoAAAAAAAAAAQAAADoAAAAAAAAAAgAAADoAAAAAAAAAAwAAADsAAAAAAAAAAAAAADsAAAAAAAAAAQAAADsAAAAAAAAAAgAAADsAAAAAAAAAAwAAADwAAAAAAAAAAAAAADwAAAAAAAAAAQAAADwAAAAAAAAAAgAAADwAAAAAAAAAAwAAAD0AAAAAAAAAAAAAAD0AAAAAAAAAAQAAAD0AAAAAAAAAAgAAAD0AAAAAAAAAAwAAAD4AAAAAAAAAAAAAAD4AAAAAAAAAAQAAAD4AAAAAAAAAAgAAAD4AAAAAAAAAAwAAAD8AAAAAAAAAAAAAAD8AAAAAAAAAAQAAAD8AAAAAAAAAAgAAAD8AAAAAAAAAAwAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAQAAAEAAAAAAAAAAAgAAAEAAAAAAAAAAAwAAAEEAAAAAAAAAAAAAAEIAAAAAAAAAAAAAAEMAAAAAAAAAAAAAAEoAAAAAAAAAAAAAAEoAAAAAAAAAAQAAAEsAAAAAAAAAAAAAAEsAAAAAAAAAAQAAAEwAAAAAAAAAAAAAAEwAAAAAAAAAAQAAAE0AAAAAAAAAAAAAAE0AAAAAAAAAAQAAAE4AAAAAAAAAAAAAAE4AAAAAAAAAAQAAAE8AAAAAAAAAAAAAAE8AAAAAAAAAAQAAAFAAAAAAAAAAAAAAAFAAAAAAAAAAAQAAAFEAAAAAAAAAAAAAAFEAAAAAAAAAAQAAAFIAAAAAAAAAAAAAAFIAAAAAAAAAAQAAAFMAAAAAAAAAAAAAAFMAAAAAAAAAAQAAAFQAAAAAAAAAAAAAAFQAAAAAAAAAAQAAAFUAAAAAAAAAAAAAAFUAAAAAAAAAAQAAAFQAAAAAAAAA+f///1MAAAAAAAAA+f///1IAAAAAAAAA+f///1EAAAAAAAAA+f///1AAAAAAAAAA+f///08AAAAAAAAA+f///04AAAAAAAAA+f///00AAAAAAAAA+f///0wAAAAAAAAA+f///0sAAAAAAAAA+f///0oAAAAAAAAA+f///1YAAAAAAAAAAAAAAEQAAAAAAAAAAAAAAEUAAAAAAAAAAAAAAEQAAAAAAAAA/////1YAAAAAAAAAAQAAAFcAAAAAAAAAAAAAAEYAAAAAAAAAAAAAAEcAAAAAAAAAAAAAAEYAAAAAAAAA/////1cAAAAAAAAAAQAAAFgAAAAAAAAAAAAAAEgAAAAAAAAAAAAAAEkAAAAAAAAAAAAAAEgAAAAAAAAA/////1gAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAkAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAsAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAA0AAAAAAAAAAAAAAA4AAAAAAAAAAAAAAA8AAAAAAAAAAAAAABAAAAAAAAAAAAAAABEAAAAAAAAAAAAAABIAAAAAAAAAAAAAABMAAAAAAAAAAAAAABQAAAAAAAAAAAAAABUAAAAAAAAAAAAAABYAAAAAAAAAAAAAABcAAAAAAAAAAAAAABgAAAAAAAAAAAAAABkAAAAAAAAAAAAAABoAAAAAAAAAAAAAABsAAAAAAAAAAAAAABwAAAAAAAAAAAAAAB0AAAAAAAAAAAAAAB4AAAAAAAAAAAAAAB8AAAAAAAAAAAAAACAAAAAAAAAAAAAAACEAAAAAAAAAAAAAACIAAAAAAAAAAAAAACMAAAAAAAAAAAAAACQAAAAAAAAAAAAAACUAAAAAAAAAAAAAACYAAAAAAAAAAAAAACcAAAAAAAAAAAAAACgAAAAAAAAAAAAAACkAAAAAAAAAAAAAACoAAAAAAAAAAAAAACsAAAAAAAAAAAAAACwAAAAAAAAAAAAAAFoAAAAAAAAAAAAAAFkAAAAAAAAAAAAAAAEAAAAAAAAACAAAADsAAAAAAAAABgAAAAIAAAACAAAAAAAAAAAAAAAFAAAABQAAAAIAAAAuAAAAAAAAAAAAAAAGAAAAAgAAAC4AAAAAAAAAAQAAAAIAAAAuAAAAAAAAAAIAAAAEAAAAAgAAAC4AAAAAAAAAAwAAAAYAAAACAAAAAwAAAAAAAAAAAAAABQAAAAUAAAACAAAALwAAAAAAAAAAAAAABgAAAAIAAAAvAAAAAAAAAAEAAAACAAAALwAAAAAAAAACAAAABAAAAAIAAAAvAAAAAAAAAAMAAAAGAAAAAgAAAAQAAAAAAAAAAAAAAAUAAAAFAAAAAgAAADAAAAAAAAAAAAAAAAYAAAACAAAAMAAAAAAAAAABAAAAAgAAADAAAAAAAAAAAgAAAAQAAAACAAAAMAAAAAAAAAADAAAABgAAAAIAAAAFAAAAAAAAAAAAAAAFAAAABQAAAAIAAAAxAAAAAAAAAAAAAAAGAAAAAgAAADEAAAAAAAAAAQAAAAIAAAAxAAAAAAAAAAIAAAAEAAAAAgAAADEAAAAAAAAAAwAAAAYAAAACAAAABgAAAAAAAAAAAAAABQAAAAUAAAACAAAAMgAAAAAAAAAAAAAABgAAAAIAAAAyAAAAAAAAAAEAAAACAAAAMgAAAAAAAAACAAAABAAAAAIAAAAyAAAAAAAAAAMAAAAGAAAAAgAAAAcAAAAAAAAAAAAAAAUAAAAFAAAAAgAAADMAAAAAAAAAAAAAAAYAAAACAAAAMwAAAAAAAAABAAAAAgAAADMAAAAAAAAAAgAAAAQAAAACAAAAMwAAAAAAAAADAAAABgAAAAIAAAAIAAAAAAAAAAAAAAAFAAAABQAAAAIAAAA0AAAAAAAAAAAAAAAGAAAAAgAAADQAAAAAAAAAAQAAAAIAAAA0AAAAAAAAAAIAAAAEAAAAAgAAADQAAAAAAAAAAwAAAAYAAAACAAAACQAAAAAAAAAAAAAABQAAAAUAAAACAAAANQAAAAAAAAAAAAAABgAAAAIAAAA1AAAAAAAAAAEAAAACAAAANQAAAAAAAAACAAAABAAAAAIAAAA1AAAAAAAAAAMAAAAGAAAAAgAAAAoAAAAAAAAAAAAAAAUAAAAFAAAAAgAAADYAAAAAAAAAAAAAAAYAAAACAAAANgAAAAAAAAABAAAAAgAAADYAAAAAAAAAAgAAAAQAAAACAAAANgAAAAAAAAADAAAABgAAAAIAAAALAAAAAAAAAAAAAAAFAAAABQAAAAIAAAA3AAAAAAAAAAAAAAAGAAAAAgAAADcAAAAAAAAAAQAAAAIAAAA3AAAAAAAAAAIAAAAEAAAAAgAAADcAAAAAAAAAAwAAAAYAAAACAAAADAAAAAAAAAAAAAAABQAAAAUAAAACAAAAOAAAAAAAAAAAAAAABgAAAAIAAAA4AAAAAAAAAAEAAAACAAAAOAAAAAAAAAACAAAABAAAAAIAAAA4AAAAAAAAAAMAAAAGAAAAAgAAAA0AAAAAAAAAAAAAAAUAAAAFAAAAAgAAADkAAAAAAAAAAAAAAAYAAAACAAAAOQAAAAAAAAABAAAAAgAAADkAAAAAAAAAAgAAAAQAAAACAAAAOQAAAAAAAAADAAAABgAAAAIAAAAOAAAAAAAAAAAAAAAFAAAABQAAAAIAAAA6AAAAAAAAAAAAAAAGAAAAAgAAADoAAAAAAAAAAQAAAAIAAAA6AAAAAAAAAAIAAAAEAAAAAgAAADoAAAAAAAAAAwAAAAYAAAACAAAADwAAAAAAAAAAAAAABQAAAAUAAAACAAAAOwAAAAAAAAAAAAAABgAAAAIAAAA7AAAAAAAAAAEAAAACAAAAOwAAAAAAAAACAAAABAAAAAIAAAA7AAAAAAAAAAMAAAAGAAAAAgAAABAAAAAAAAAAAAAAAAUAAAAFAAAAAgAAADwAAAAAAAAAAAAAAAYAAAACAAAAPAAAAAAAAAABAAAAAgAAADwAAAAAAAAAAgAAAAQAAAACAAAAPAAAAAAAAAADAAAABgAAAAIAAAARAAAAAAAAAAAAAAAFAAAABQAAAAIAAAA9AAAAAAAAAAAAAAAGAAAAAgAAAD0AAAAAAAAAAQAAAAIAAAA9AAAAAAAAAAIAAAAEAAAAAgAAAD0AAAAAAAAAAwAAAAYAAAACAAAAEgAAAAAAAAAAAAAABQAAAAUAAAACAAAAPgAAAAAAAAAAAAAABgAAAAIAAAA+AAAAAAAAAAEAAAACAAAAPgAAAAAAAAACAAAABAAAAAIAAAA+AAAAAAAAAAMAAAAGAAAAAgAAABMAAAAAAAAAAAAAAAUAAAAFAAAAAgAAAD8AAAAAAAAAAAAAAAYAAAACAAAAPwAAAAAAAAABAAAAAgAAAD8AAAAAAAAAAgAAAAQAAAACAAAAPwAAAAAAAAADAAAABgAAAAIAAAAUAAAAAAAAAAAAAAAFAAAABQAAAAIAAABAAAAAAAAAAAAAAAAGAAAAAgAAAEAAAAAAAAAAAQAAAAIAAABAAAAAAAAAAAIAAAAEAAAAAgAAAEAAAAAAAAAAAwAAAAYAAAABAAAAAQAAAAAAAAAFAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAIAAABKAAAAAAAAAAAAAAAGAAAAAQAAAAEAAAD5////BQAAAAYAAAACAAAAVQAAAAAAAAAAAAAAAgAAAFUAAAAAAAAAAAAAAAQAAAACAAAAVQAAAAAAAAAAAAAABgAAAAEAAAABAAAAAAAAAAUAAAACAAAASwAAAAAAAAAAAAAABAAAAAIAAABKAAAAAAAAAPn///8GAAAAAQAAAAEAAAAAAAAABQAAAAIAAABMAAAAAAAAAAAAAAAEAAAAAgAAAEsAAAAAAAAA+f///wYAAAABAAAAAQAAAAAAAAAFAAAAAgAAAE0AAAAAAAAAAAAAAAQAAAACAAAATAAAAAAAAAD5////BgAAAAEAAAABAAAAAAAAAAUAAAACAAAATgAAAAAAAAAAAAAABAAAAAIAAABNAAAAAAAAAPn///8GAAAAAQAAAAEAAAAAAAAABQAAAAIAAABPAAAAAAAAAAAAAAAEAAAAAgAAAE4AAAAAAAAA+f///wYAAAABAAAAAQAAAAAAAAAFAAAAAgAAAFAAAAAAAAAAAAAAAAQAAAACAAAATwAAAAAAAAD5////BgAAAAEAAAABAAAAAAAAAAUAAAACAAAAUQAAAAAAAAAAAAAABAAAAAIAAABQAAAAAAAAAPn///8GAAAAAQAAAAEAAAAAAAAABQAAAAIAAABSAAAAAAAAAAAAAAAEAAAAAgAAAFEAAAAAAAAA+f///wYAAAABAAAAAQAAAAAAAAAFAAAAAgAAAFMAAAAAAAAAAAAAAAQAAAACAAAAUgAAAAAAAAD5////BgAAAAEAAAABAAAAAAAAAAUAAAACAAAAVAAAAAAAAAAAAAAABAAAAAIAAABTAAAAAAAAAPn///8GAAAAAQAAAAEAAAAAAAAABQAAAAIAAABVAAAAAAAAAAAAAAAEAAAAAgAAAFQAAAAAAAAA+f///wYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAABKAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAAAQAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAAVAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAAuAAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAABYAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAAEoAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAABAAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAALgAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAAoukz5btWDoclP5ZejolfW3FuyNSqJuxkyvDGIm5rIgkBAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAABLAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAALwAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAAXAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAAwAAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAABgAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAAEsAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAAvAAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAAArNxDfun0nPHO9gaKHe0IRwpiPA4Fg0f4V6S/o1GCzEwEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAAMAAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAAUj4xq5sTidFOhU28jtFb1tSSpHbgwT9nApO96SPfrxgBAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAABMAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAAMQAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAAZAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAAyAAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAABoAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAAEwAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAAxAAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAABC+4rasD/Dz8HxuI/kVUHnKOJwsy+4tz7Ipq8TujahAAEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAAMgAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAAnWkIkL756kQ9WGR4B9nzHyWhAdKNBZLUF19Uzg11uS4BAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAABNAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAAMwAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAAbAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAA0AAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAABwAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAAE0AAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAAzAAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAAAbQnFsr2M4U3LluNu4dr5N1VF8eCK3ev/6jElvkw5wDAEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAANAAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAAS0hg2BrMj6FjByaMt1naWeNn9soexvLrRi7mtwaVgiABAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAABOAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAANQAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAAdAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAA2AAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAAB4AAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAAE4AAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAA1AAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAAAn5TBIsH5NStIzBSS+EDLLioLaBlnTMOOp6oSDQSByEgEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAANgAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAAFOAujNwn9ZgMFqDjXi6h/seM1vkURQrMkV7D9vgJRS4BAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAABPAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAANwAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAAfAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAA4AAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAACAAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAAE8AAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAA3AAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAAAHZhgcDfiby3j6OKvK320SbAqjNEgcjfGiQBBxOFIIIwEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAAOAAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAA4MAjESpKAW+jY3rCE49V6RziSZIa0RzJe5UqUyRkyREBAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAABQAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAAOQAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAAhAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAA6AAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAACIAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAAFAAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAA5AAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAAC2SIg9SgwjLCo0FUkZi4Namy0z6knxXB9d+OXgTPpDBQEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAAOgAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAA4s8XdEcNARYunxXUde4e732Izj8wVmi5Iz9Tf9o2/xABAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAABRAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAAOwAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAAjAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAA8AAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAACQAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAAFEAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAA7AAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAACL0BkSB0CVXe2uRI5pfq3FyEww7/NFrksCMjSQ+9anKQEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAAPAAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAADR9iGWVq3PwOhkjSfj+J3Xy/FImn/WImP2tV4+qKcSoBAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAABSAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAAPQAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAAlAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAA+AAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAACYAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAAFIAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAA9AAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAAB0Pyh6QPldOZRSs6FufgXAxjQMn7oWXQjA8SE8EcoUCQEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAAPgAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAADkOzbDb5WRHNt/xQYG375+np/7kVVNZ6J7EP59tQ/R0BAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAABTAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAAPwAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAAnAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAABAAAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAACgAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAAFMAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAAA/AAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAAAps5LQ3aFG6+0AcVqatIQXw4C6b/mhaNGDY7dC5QIZHgEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAAQAAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAAwG1VK++txNZiTR4U3knvQjCZB926obpw1UyaGJ8RRhUBAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAABUAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAAQQAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAApAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAABCAAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAACoAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAAFQAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAABBAAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAACrbvz6NMj+vzROHp6XxKTyn3wt4i5PGUkDLVExWAEuKQEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAAQgAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAAipQBLV7GVNWBm3tJmzMJ7sdmXr16qF9sGmZs5JyF2CkBAAAAAAAAAAMAAAACAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAIAAABVAAAAAAAAAAEAAAAGAAAABQAAAAUAAAACAAAAQwAAAAAAAAAAAAAABgAAAAMAAAABAAAAAAAAAAIAAAArAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAFAAAABQAAAAIAAAAtAAAAAAAAAAAAAAAGAAAAAwAAAAEAAAAAAAAAAgAAACwAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAAAgAAAFUAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAIAAABDAAAAAAAAAAAAAAAGAAAABgAAAAMAAAABAAAAAAAAAAAAAADe/eEQsDLCSiUQOSjN9ARJHQ/R1Hjvi04REURQx1tBEQEAAAAAAAAAAwAAAAIAAAAAAAAABQAAAAUAAAACAAAALQAAAAAAAAAAAAAABgAAAAYAAAADAAAAAQAAAAAAAAAAAAAAGkKw3aKCZNuXuywdiKshySOghdQe068qT34BjgbTSAgBAAAAAAAAAAMAAAACAAAAAAAAAAYAAAABAAAAAQAAAAAAAAAFAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAIAAABWAAAAAAAAAAAAAAAGAAAAAQAAAAEAAAD5////BQAAAAYAAAACAAAAVgAAAAAAAAAAAAAAAgAAAFYAAAAAAAAAAAAAAAQAAAACAAAAVgAAAAAAAAAAAAAABgAAAAUAAAAFAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAEAAAABAAAA+f///wQAAAAFAAAABQAAAAUAAAAFAAAABQAAAAEAAAABAAAA+v///wEAAAABAAAA+////wEAAAABAAAA/P///wEAAAABAAAA/f///wEAAAABAAAA/v///wEAAAABAAAA/////wUAAAAGAAAABgAAAAIAAABWAAAAAAAAAAEAAAAFAAAAAgAAAEQAAAAAAAAAAAAAAAMAAAABAAAAAAAAAAUAAAACAAAARQAAAAAAAAAAAAAAAwAAAAIAAAAAAAAABAAAAAYAAAAGAAAAAgAAAFYAAAAAAAAAAAAAAAUAAAAIAAAAAQAAAAAAAAACAAAAQQAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAwAAAAEAAAAAAAAABQAAAAgAAAABAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAGAAAAAQAAAAEAAAAAAAAABQAAAAIAAABEAAAAAAAAAAAAAAAEAAAAAgAAAEUAAAAAAAAAAAAAAAYAAAAGAAAABQAAAAUAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAQAAAAEAAAD5////BAAAAAUAAAAFAAAABQAAAAUAAAAFAAAAAQAAAAEAAAD6////AQAAAAEAAAD7////AQAAAAEAAAD8////AQAAAAEAAAD9////AQAAAAEAAAD+////AQAAAAEAAAD/////BQAAAAIAAABEAAAAAAAAAAAAAAAEAAAAAgAAAEUAAAAAAAAAAAAAAAUAAAACAAAARAAAAAAAAAAAAAAABAAAAAIAAABEAAAAAAAAAP////8GAAAAAQAAAAEAAAAAAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAACAAAAVwAAAAAAAAAAAAAABgAAAAEAAAABAAAA+f///wUAAAAGAAAAAgAAAFcAAAAAAAAAAAAAAAIAAABXAAAAAAAAAAAAAAAEAAAAAgAAAFcAAAAAAAAAAAAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAABgAAAAYAAAACAAAAVwAAAAAAAAABAAAABQAAAAIAAABGAAAAAAAAAAAAAAADAAAAAQAAAAAAAAAFAAAAAgAAAEcAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAQAAAAGAAAABgAAAAIAAABXAAAAAAAAAAAAAAAFAAAACAAAAAEAAAAAAAAAAgAAAEIAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAMAAAABAAAAAAAAAAUAAAAIAAAAAQAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAwAAAAIAAAAAAAAABgAAAAEAAAABAAAAAAAAAAUAAAACAAAARgAAAAAAAAAAAAAABAAAAAIAAABHAAAAAAAAAAAAAAAGAAAABgAAAAUAAAAFAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAEAAAABAAAA+f///wQAAAAFAAAABQAAAAUAAAAFAAAABQAAAAEAAAABAAAA+v///wEAAAABAAAA+////wEAAAABAAAA/P///wEAAAABAAAA/f///wEAAAABAAAA/v///wEAAAABAAAA/////wUAAAACAAAARgAAAAAAAAAAAAAABAAAAAIAAABHAAAAAAAAAAAAAAAFAAAAAgAAAEYAAAAAAAAAAAAAAAQAAAACAAAARgAAAAAAAAD/////BgAAAAEAAAABAAAAAAAAAAUAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAgAAAFgAAAAAAAAAAAAAAAYAAAABAAAAAQAAAPn///8FAAAABgAAAAIAAABYAAAAAAAAAAAAAAACAAAAWAAAAAAAAAAAAAAABAAAAAIAAABYAAAAAAAAAAAAAAAGAAAABQAAAAUAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAQAAAAEAAAD5////BAAAAAUAAAAFAAAABQAAAAUAAAAFAAAAAQAAAAEAAAD6////AQAAAAEAAAD7////AQAAAAEAAAD8////AQAAAAEAAAD9////AQAAAAEAAAD+////AQAAAAEAAAD/////BQAAAAYAAAAGAAAAAgAAAFgAAAAAAAAAAQAAAAUAAAACAAAASAAAAAAAAAAAAAAAAwAAAAEAAAAAAAAABQAAAAIAAABJAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAEAAAABgAAAAYAAAACAAAAWAAAAAAAAAAAAAAABQAAAAgAAAABAAAAAAAAAAIAAABDAAAAAAAAAAAAAAADAAAAAAAAAAAAAAADAAAAAQAAAAAAAAAFAAAACAAAAAEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAYAAAABAAAAAQAAAAAAAAAFAAAAAgAAAEgAAAAAAAAAAAAAAAQAAAACAAAASQAAAAAAAAAAAAAABgAAAAYAAAAFAAAABQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAAAAQAAAPn///8EAAAABQAAAAUAAAAFAAAABQAAAAUAAAABAAAAAQAAAPr///8BAAAAAQAAAPv///8BAAAAAQAAAPz///8BAAAAAQAAAP3///8BAAAAAQAAAP7///8BAAAAAQAAAP////8FAAAAAgAAAEgAAAAAAAAAAAAAAAQAAAACAAAASQAAAAAAAAAAAAAABQAAAAIAAABIAAAAAAAAAAAAAAAEAAAAAgAAAEgAAAAAAAAA/////wMAAAADAAAAAAAAAAFVvVqq2RH/lNXbTDf3xg2SbmMxeiz1VgvoP25xfKYPFQAAAQAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACQAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAEAAAAAAAAAEgAAAAAAAAAmrv1e1E3oxPWBKAAAAAAAAAAAAAAAAAAAAAAAAAAAAFILzHvZcKhpN+UxAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ6ObISmyLx+QBAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAGLU78KQFmgqBpgAAAAAAAAAAAAAAAAAAAAAAAAAAADyvELDGT+tyC2D2AAAAAAAAAAAAAAAAAAAAAAAAAAAAjZ2JLG4roPWnIwAAAAAAAAAAAAAAAAAAAAAAAAAAAACYXjoP1zywy40m3QAAAAAAAAAAAAAAAAAAAAAAAAAAAGFssvPbEy9a4csKAAAAAAAAAAAAAAAAAAAAAAAAAAAAUtt6jI3K9XW1EgAAAAAAAAAAAAAAAAAAAAAAAAAAAABcs8MPpaNkbYRc6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAS0MDn6a2DSc+BIAAAAAAAAAAAAAAAAAAAAAAAAAAAA1rK1nNC7W6LoBQAAAAAAAAAAAAAAAAAAAAAAAAAAAACJM2m7B+/iq6m+FbOkLamOAO6r11KteiAz931+kl/+DRzk2fx3vhng0DBaqpQFXzgIy7fnzFdjpGRXPBumkXQNrJTB3wNpxDNVAcj79O2pdVEDB/2JmFXcOGnVDZO1TyGZQBxqj9IE5CgI/ERnzNLYUBvGwwZengu8/ZDGLhKVJaxhY/0AFZuzFmDAkiYcG7kAAAAAAAAAAAAAAAAAAAAA8cMq/B7olRSm4wjcePBNzgAAAAAAAAAAAAAAAAAAAAAAHAAAAAAAAI198xAY0ywrcBcUsNC+8D6jWDeQSQ1iloCWQkl36ptDqNFBBG2d89G8zxUnD9dzJnPP7gUF+VmYRHS/fPxOOxFDugYYnyYfFFQiNVNLV6GsfZsAiqRpsAYUmtyh6gZFYxxQWt/WCjLsgC9MqxWWUjfvEDsXFm4YNllZBfsiJzQWR795FoWYG0esdo+dIX1i8az0rRIlaPB/eJJEJSeYqhgVquHdhsH4LVBJsp1O69I56/mPISuii/6JUcR+VdagKva2eJqhJIztVne/CiBKpZRwmr8hTWlJL4e2BCgURMAM2YaJ+9u55ssK/SAneD0JQo4w9rELf/kY4dh/qXxsimMrWAAMqMdO6Scrg4YWLlUApTZ/ly6u03xwgPvclZ+1DX5lagNE57GUpTTow/YxNqBzQ3LGdLhkvpfJMkYpdr1MFsI+51vg2khOj2fJMGlJHobKFejIsxOxx3XfV/3FL1UaZawM0CrCdG1SRmQaAqZpfDlYyxIc6A9g5Xfj0ltgGjigxheQCdb6y//SyT+vl7GZgatbMbzazLEEFoD+hqARyihqfyOopHPqDiPmoEpzeg4Sq3z4RB2ZW8Fs0I+jUwdeTNUaNtl6gmoBDotbdY9SRGwn+V+hn5omPTEEoxlUYZX0ZzXOIrce7byki9wm7hpPdkS6YTiAKIXwj8MsP+dWlLA4LhvFfXdV7NRQhk1WiCVuGzrqyVjPee3Ntloxdx/Vo167MUF91ntd29kye04Hsy82eRGNrLwj+onc/v0PUGzkAdE5/WBF5qvygIva6dh3md3AwSHLPD/TvpFKDuRJT94coLS3h6OcKN3mufbMRno48AvPFsZaT8XU08ysjSZl6sf+nmo7xb6FXSw3IW1i6RGHDT8pMAnTGQhTtnjNHEga9E+TCpDeyvDDnu88r+Qh3DJ6uwiDVHy3S9UeY7ASRMj+ErwSx/L4+2pj9C7m0nB35oOnG8wR1sItGKGMpFen+a7vbejpNGC97/324HDZFpPh4N7ZXiYoKR+7Pg3gUpcfBX3Vqx10/xooI6EflQPvY6m4gACgcWukUcA6xDpb55I/hGFOC35SlsZjnsz8xUJWHFn46GOjj1TqbWMI0R2CE3Azb+W+/rSHGEhCRkdHIe//JFkuPu0AvElYHzR8KPwDi+Zr+FdbQKW4eEka2ZW2tT+gZpocutRoQo6sB6ZoPYndCpUvKXZJtVsXCjZjmqssLUvAOrGhsTJYqie60k2c6hmgWd7ptTiMg14zmDWx56AuRiHiqakfMn3RkfMXGIB5zng1JwQEVnHhTGmxw3GG9cdQKuboYKQLv2luYkBif+V6N1qqlt0NzNJR11n6CWUsodD+GJYQEFRDn+5jOVT5g8Oy2t2XhaKg6npAMfSFhaD77amJ8+q0WE3nFaqKQyfuPuhon7M88r1Iljp5KkAfdAcQ+wnssQKQQIh+IfER/aNRkct1UhJq5EgHKnp6Szpl1GRCoNBQMPrgfcnVLh1vj0Ezt0M3DZesO7+5x1XnWBuzSeGEBtiL1JEIQAtpSL8/2HBlP5jcX5GyKVAhawa6R+e8JC5xSgx5AgDRMw1Stw/6WklrrJRkAFoxqsA5pfHT4aquNIzaiRu6IXl9el0RdqglLnx8fDf0Cb0G4AZJbswkaEcW//bgj8uj/W7NRtqFHtpXR9+5Y3XSiTsS3TNPhKSJpDEEfklyUUjcPBgCpQOxFWiM9AXcL9neUNr5GXQ2wTc8ceZhP9tcGcCxdl9ExAHZOPy3u59+GXgq7GCv7sb5T9yGcYUQTCE5pNdKUkoxEpvjFZO6IYuHAM2S6QfdwUizBG8/1gZHM4YhkapQHUgjTTCmwLms40x9aHIkMFI49aGdGmcbvPY8Oi/rjwvWWjciBjdVHeriXpI3/SfMcb69Wmw+gvZiWHeZ3llNW2Ob+Bpk0AkOIHSBo7olh2T4+rCIEwAPqG0f9Doc9m9Wqz2iucxZfXBZpt6+VuFW4fUbdrJTxHKW6UON/ToC10gXKvURrAS9FNbCa74Eg7G/sWR+Rgzym3Gb9cn+XLsEDniHo9uq6RihXc4zLAoUgLeN+D5iw4w9OJ6QFAdIJ9MRDm5JeRmhnqVZz0zrvKj48Ff2KwqNdcZHEiLcajQ3fxVs8m7wi+00jrd1Sfziimohley/fE01BkmTPki6m14lAOcPPNx0M1e8+dNnaemRriqKBx3JXwPPE72adtbjyM8M9JQI5Ap23gdm5B76vimnWit3lhSYU0goHbBeku+wvyZ1PD8T1//9m4IeKK3eV59QIU4UHDG6AS0pjMBSrNyHIU1aYPikIbOrwnSG7WwNMBADVevzJGYQ/Kb0PRPzy44K11c8UBIVWcVwiVdEGbisJJN9b9lsNIArTx4Fs0z/LxbFWVqIEond+EA+V3WlCOmHfnQ9aoPVQkWt26KaGNZsKm6fliUMMKNRyRqbVEcM0reyJojAlEjBT8HdlEjgooAoH0ZfEVDZmLHg8i9iQbFkYmzAqEbs+Yz4HC0r9JwgDSVPt3++YQV4MBWEWTzx18o/jTvYZ/8QZcH1rCtvI4n/BdP/YQehcv8uLXHxpmR6RaqrSjJVP4qDtc5GDGvQIvcmacNZY/NTWdQhldweqmC/wNjjhH77X541xR7VteEcGitduVMCV+h9ou1z2QBgRMtdwAWgleZyZuNGkgXApGhQBoJ5llKodfnqAxUukLt7VBy1gmvSJHGPZMJrNEH4k/IV5YR7T9z9YeVFhSjjw5VQvt0j21OjMb5CmeJvEmpN4B2vEkvYeCrAKA61C2ETFDVZn/zZToGNOZPQKbsfpdmsBSc/jg942SXQfERodkFni/xMEdhDTb0D7fpZL6zXwoUazyXKcs4SBYh2aqxQnLe3kj1W7kQTz0j5VhgimcZIHhHD6x+e4GAxtb+rIDZFu3S6hrcBhQeIZLmMDnzqSdQgEeEQG7KGmD+71w8F4LKWRLc351I1KomXviJf9xpVp+Ar3NQF86HID54oxD1xxosYw57rUwrH7BDQ5QvVN+C0dgLcUearUbOW1AHGMUoXXYA4apHRvlsaUxBC7UC+MpgoGtdaTy1S7DIUOIRgCkVfGmZvkNK6xs+ebLMnU5nTjtMnde1AqibSjFg8TwimZYtFAQy5m0cyfSjzJ9+0DrZozAHKeUS09Kqx8JeZC/c8APjwxN+tZv0XDz0sZ54BstZ0Jm2Sk4Mk2MnD2lKJpf1Ss37V5eEqibihHQwFk7FSiWEBfA1al3kz7ixtUL9cAabS5HHxVH11a04o4dl0uU218RHVyxksYWoQPqOo0Dy/n7EJU82CcwN5IiwAYt/dDfN3DF4QAsRLPQBH598myRRrQVJbYXw3mVJNTzFTUyzoYHUpwnyE9rdE6Otpnl9TpGRNT40kng8h0sdKC+J/mxRTHAM3La5YQLMz9sKerZJQJ35jmX6OGTf8cZi12Wd1bj2zDZXlyZQ61mr97amQHVyJI2lcZ6u3BfgSc2+ltHkwJz0E81smoTZ8yJ4g8BgzrlD9dg3tZduEc5nNIf68x4wEYRj3ym8HZ3/dDy9lc7DeiTP23bzIVSzsntxyLsTIBa6UFG0Fgh4dTBOVOX+etEs3sq+G7aXsUep7RM50pgF0zeoA+MZ3JLIf7qB66hkMALwgYc1G4g4QJTLpP233D3jg+SKSo/N5E9HAX2XcO5N6HQ2dMaQN0GxoTQ4OzwfIXHneGtSWBITMqPGqKMehlOZQdU4U7mF9CRYoSAo6cpXi5v8qSiYPPFsV8rtHwg1yT9wPl0gvtw/n+SMtYrvbpbmvnQ+MXMi8MctlUh7BuPeqY+isww6fa0YBIT7H1I0yrP6FBP1dJ5hc4Iff/Iw3xtz0bKmOCLrV12URMPJhRsxQu0QhAh7h6MFT/SXz3dVipUs4VIzLKKDz9MbKCHjHLrqXAB09+9RvY/6t5SYYneAuRkJy5TyZ9lzztVqCkKrY3B3UFUHYZxEQXbqZjwhqlNRBZ67mLZlvTssa1ui+5Gboikkhdm2b36bnS8+gLub6GHASuMoPfWAoNC9lblRsxDyWFxI+3Xx+ehJiA5ef2fliJgHyId7N3TzOdAQvIHra/50EDNUIEbkewFrXGKQ4vDsS1xNuvq1Fvq+Zl0GVsl6y8vEAdrSYa2VjlV8bB/GBZzcumqXaOD/Su4aso59Pf13Wuywmffo9uldo60X6g4etdstPd4fsVQeYsnTHhBWcb4o7BN7UclMopZcpLXAdts84cO2z0DEt+bbSkdo8ytViw8AdU9p7FhwxxlX6+uWZgg7aK7QFvwMQvnxsfTbqvOX6mSBixnQuuV29d+7043w9KQOeavFPKJibHWgiZ0IkaPWGFm1wiBzWKP3ZuTrqTHo6eS/DucaalEXp/6AYbS6kN1swU61gppvLi7mIEsH4R3hS5v1uKNfcSH3gaAnuUI86Tgxq6OgLQvP9vVjFKJx+ivwGo0kUqsVRiCxPRin0k9lyBpyIH3mlhU027sdsul0XFyyUcmUD5LksMrhWZySeO1cg+5rH+sdxidK+VO8E4BvHi3HNbxQlTF8Ettm4zFgMaSQ+emfVwAwFfpayrV+SRjUUwwaZvgupcJePRCEKllBqJy807QAxlHpIWhO+YLJBNKrlJwlm8BFzsJ+GFWjpTW0JxoGbGJXJi+T4OIPfktZ8TNEjP5pR4hvENspDKVb6PCILrDscbcmhPkA7ZuKvO6itbfBDOw9dGGexgBhZTLtOHW2DetqmCt5FGVRtY/4NZ62qVHYaUJIIZzW5m2CHUYAUr55ejZuUW5ENA8tvN3UU/ZZATXX5j3BSa46jSqv/CRqVVnTBwoTnJ2WchDGFmTTp+QkO+aZMqc4999p2luxpDnY5cUA+RQyilRT4c53Fo05iIojogJgGXQG+XaC0UJYHgfIIoJNVzOg8PNNfSAxAsLPLCVc/7swsARXvAHoULgo3AhEgxsk9ycwuTX9ZcZAbRkxZMXXdj0lbHChbCkFSDp13XQgxRnU66T6gonIbAUyfSu4UbnFAGE+odf4tndYBrVvilAcAKTksYch7VdYfle8hpWqpEKZUYIi2s89IZC0+Vk4C3V+vd0a/XmIpSdRUJW4PGCy5Rz4+CfygqZ1vJc8Mpj9Iik/vUeOdMb8DemD0lk2EK1vSlNK1dyss6CsW+o7duSxDIReuNubiPzHA2GI/WJ2xBvegI4m3YMuu0iFkFfS8M7/hs0Jxdy27Ru/4qp5tJeYu+fxpjKQ1JibcL+4elUNlX/jUqSfmIQvNW7bqMA/zG5RzShv6H88rv/IgPWCRb+jYFMRqyeJlxWhwOY3pND/Pwr39d+lWyIN3/AaYPwvpH0qt6cFWHUCIbgNGl+OhGp7ARaad2UNkTRPcIhoHXbsTpRdC6zz/N2OticidAhGuj8PUa3dBhZn5zWgApXuQxm8M+730Yubs6BW5ZluT/dhzxctMnn95L4ProAptN1a/rbbduxy/0cgcTCDHUOEh/7DXsxo5snFvGOv4JtIhijtBzRA88uKIDqPpALAYsdCZQC726DR3rNLOrbkjSMN16NftZ/7LsrGTxZd8VFuxAciR802Y8UU89gPn/hucOqzJ3I6p/jH7+6EeBE9TqTr2AqhRebR6Ep69oAQbLisOQ85ZQZDBxazp/yYt0uBsLIZzvWL/2N7bgktjHXkj+QGu3kK6vSmK/S9N+oW3Cuctc5mFNe++eZOtUgqkGw8sNmPSP7Efu/KZyWnIXnpAP1hUAvyiLw3PxdQsJN7DCYOOZ4lkQZbgd/t+d7aetLVKat5NV8Nqm//wV65LY1MO3JzHPWpGQR5hHhMkF/mjHGaMyxaYnFxhRvqfVQs+hyyGvRQKqcZ2bdsTixx0uRCGYRHttiZPz7ybh62S5UkAEfxKYHj0uUbEDVTj1QRn1ZTzGJsNdf1Zt90aNSH05bYKtZnsEu51JSpi0ZSn6k3OMwhVm5bXHba9YcgrgG3RCCQCuaNeIGRNpWvuu5fxKI085bu8i/8bqkbz0boZwajzBb00XVSNQouJuP3O+xVWYi+FazpxBFX/WCvf6PJKk+MYMSM3+SrnTbDt+n8h7rl0/CmqqsZs46ymcjtF/z0BzQEMTczUP1emNULnhm+hM1+HXisC4RF8cKl4T3fyC8qdLyDdoNGmn72ProtOb3ZKAvd5v0vhXqJCh+ZJdWFN5IUCVl/HrbiEiZ+12gL8YWi9FYyc8e+K2eu65caLkU8bMiTMRKyNMTnZvIMNib8sP+jiM8pROnyMnhwJEE4Db7YqLibNg/vk/JQfVT1VJ1vSYJd6ikC85BtMX3tRj2cKeuMH67+InXwP6m46h9W8yDcfk6LbZrKnK1s+PE9rE1bdjAH+r7Eh3ITyg750/3NqJ8rL34GVwHztkwTCi4Bk0G3IFL2SoR0g3SuvzekfALHM9jgbOy9KESfAgGellA9+e4MoOZ9JCYNwVga9pfOTZiE+05KYOzk5Brp6WTFyakzWBSPUAHo365xorQ4Hy0Fyh4EDXFt/A5ulrRTwO9sba+MuLERPgQc7vFFFUEtyvmdzOoRWt4wf2oBWogOeRw/3y4UnUz8/AwU0IvG8pYoibikPm55+swJhlewtoL5QvtiGUiCbppg00EOFhNQVKsXjl3FFCg+/M9BWulBGQJWIAHquHWzqcTBuA8Qu6QZcyLRBEdFirGB1Hctx0VCcMgT5mAkRYz3JZIY79j+v1b4MmhN0VrG4wFdYx86sOOSb6FRHRS4EgcpfbUNN1RfgxvpvIhedXPAajeU3/QxZAIrp6N6tExpK9uZdBgfqRps1ufIAWMu19ZuxcGz53QK/m3skbEwGJaZVV2HpO+Oz0ANwxjldHK+ZRYwMrttQhMIrE3vpmQd23B8fH0+QBhSzx/p9MH2rsp8FJiKZVzib8xEc4TPZH8w0uvfLz8zmeQBaiqqJOs8YRUYgK602D3wnCj7tX5orKBDlE+t5Nbs0JnhOcthihVOTJQl3YjN6wMl1bz3AdwdyKto7rOZSlGEcefeApByXzQn7Tq8paXQbAbOPjHbXGoZfOVB7gFAZrhU4h/ltqrANEYobzevuduRYxCtrF+wQn8gurDfwhwZF8yPrEjS8Fs28y50oALOm2Yu9ps5rbCufQVvvjd0V8qTVgo0hYm7Cw9DGEc/eHpQrIb3kb+gnIMP2Mn7rXndqHIthtV7+6G2ft+3bxCbJxNHzoYIxgJcFKxADqFKPZrBgZab+DflJdKOccAwJZvVtzep5P9XTlSE/J+W0NT/iVOrMFkH8xfTzCcT/KK+hacUNCIjSXDGVLs4julUMlkefeKoxNlj6SBp211OUF5qnCNURN8B/2tsI1ZzuSiC03Z/LTo6Z35y0qTgsEmIfu0iWvHSNFbV6vQ4DW2aue8XOFxMvpB/TD3OoNULxl5pWt5GgT0C0oTbYDybJck6EfFh6BdfwnXzzJg2L3xNFhOm+1l3dzCuWzc8mpBGdY1YbZPGUiDdf6odUeHoBIM3XSV5wkLtQgOdzbQE4HLrOBcjXhjgpu+pZopiopkTvAaJGqYqJVyRi0Iw3FCIKBEsc6tmnO/bl43jB7SPZ5oVz7Nx8JvyTJ6LX3A0kQfOqEnEduT7zSSO0m11DkU+gBWgjdLTtp7z0cR6bISrERoxZBiIGdTDFER5Ch5yut71fWcdZ2XTjSE9BgnDRL86a32PKgbxyqS6aWpTTm/eWX3nN8vBAGcoe1YRJT1Us4wUqEq/H9vLo0DH5SQBdENBbJ8cjHyRTgImXUFGRlh+KjNwLeLz3FKoj1VVFKXy7Ch0PoQyo23adkjPZqM0VHpn8+zCp2Lk9eC1VLIn89CNcRZg3jPvVfPI9I3YxknggZya6Wv3d3oMa1x1ASjW6eringaLTbLaQhkbblAX0rxr9ST44FlN19+RFmXM1VrWLY4pzEwYU5U2uE16jjftPDFX4lkn1EkBZiSjNebUf5t4rOpcE/lhc5cQSZUIEZR8BFX/OBMObmswkgZcfTMdiFvS3PFxKmLuoHHOhmRwLayk9lmXw5Jso2ik1OOsu2ZpAekwXKvQZmcrK3hl7J2QrFdwJUE9ACWx6ufe8v7SwEOLtrxDXCCPxhT8HxUDg8TweO4hEVOzb1DcZgddKmgDMM9ZbXKzdUPjNAEvRgjq1OAXNumdYPjzM+P1U7P5n9Y1YKW4xV+AGlVHOxB+0RXniDoxroMiQrmZDGCydWMD1f+eLvjYStOI/rrWGjbFXvKAd+zpS1G2cJCpSZ3iVlSQ3FitCjo5kamEHGaYvkILHSSlatuZr7JqmgT7AYdTFwRnvNHRi76ZcUsOfGy8gzx1XDW4vVsr+guTTZOGhCxhVWtKxMJDrXgS+WP3Iyk+V8uQGu6uRJpn7GiuiUkeY86jG48OZKjVrfAVNf6By7sda4hPFVESVkRkKvFoEPEsIohFbWwKNirkav9xn2XOKxKZ7BbmpiNcjq4mSggfhBu2iR72T1sKhnaa1LIDMEmSyKg4wRqU+jIMTCo22uldDJQRBxX8YcgyTKKiK0/0PvvmDmBY/7x45m7m0gPlIGlXzBCKJs2SAjEANiFtsJ/61XgeQCbnUYss1r6wjw+7JFweuUpl3823RVpQ4DdkDQzR5Jk8qXzLtCxS5VfNLqQQ5Kr6TcJ2KuSwH5kacZMavEetJWQ90+id44En+I45ivXLpoT45NtjNQDZMqExGgVHGu7UbCDaYRsg9OjTK2tBLgGrS+3FIqlgGeAVAFkZPK7NEc/ATK1+LMSFz/1/n4iOOFR7pKqC0KfrmL1I0qkrkF1buly8MGE/BNeLhhavvobZJzs27F22RuskR369DHCpTD9BlGWWy2l0eDfD+3faY+zBMgBmlF7i/5MsqlGYhku94XqAacYJsnbEZoe6DUaF+TAqkI4siooJ5RbAAyL54ovqbiCax1V/w/TYv76ndJhj7t9T9UJSTIe6ns24vnP21IYYFA9jUbKGF6ydeDiaIXdBBd6yk/ux9wnAUrpupNjtFoDcCinYILPRC0fH5H8GlDZJhGehkWE6Cb7Y55xq+IeJ/HxlxbxjAO0D+KTHEsKwk8pyqErvAJ61EgyWrdIAoqnQCFOfAUGjN0jDc1a47Y6FC6ae/y4GNlMC2bTxbrv9v7MkTgszw9BAdlyh1aJhIaeuEGeyF8Fc5RWq1Jg9IYWphJCvy4H6Lzrbuxb7FSqH8EnepAlJWTvngz90JynDCxAvIDP3A354TMLpx7IOhNjV47mkIvGIusHmoo90zOKOlydQcU8qDM//MlZSZc7SENt0W4mr8NdzJwxrVkV02gACBYhKw+hyhy5s3IFf8XRX1z+Bxxn4Bg5dDh/bPxJ3/tHKbGX6c+/qyK3Nad/cbHm4DVnlsmn0GrVGjlHPNCciH3bgfi7NS9OLHYt2ktAd++fRqFDq+oArjqIK0NkIUUfIyGAZQ/k1VyeJwgYrw9Nh0ps8eAt/HsdwIG9rjnkmqsV46AYvuoFuMyMVwDvNbT1RJotXjExQSJrqK22RYyXRnAolmF3ZB8n442DIdniKmuurrOri3313cW3sSNE0AolX20lk=","aggVkHashIdx":1} \ No newline at end of file diff --git a/axiom-query/src/axiom_aggregation1/circuit.rs b/axiom-query/src/axiom_aggregation1/circuit.rs new file mode 100644 index 00000000..a0dffe47 --- /dev/null +++ b/axiom-query/src/axiom_aggregation1/circuit.rs @@ -0,0 +1,138 @@ +use anyhow::{bail, Result}; +use axiom_eth::{ + halo2_base::gates::{circuit::CircuitBuilderStage, GateChip}, + halo2_proofs::poly::kzg::commitment::ParamsKZG, + halo2curves::bn256::Bn256, + snark_verifier_sdk::{ + halo2::{aggregation::AggregationCircuit, POSEIDON_SPEC}, + SHPLONK, + }, + utils::{ + build_utils::pinning::aggregation::AggregationCircuitPinning, + component::types::{ComponentPublicInstances, PoseidonHasher}, + snark_verifier::{ + create_universal_aggregation_circuit, AggregationCircuitParams, NUM_FE_ACCUMULATOR, + }, + }, +}; + +use crate::{ + axiom_aggregation1::types::LogicalPublicInstanceAxiomAggregation, + subquery_aggregation::types::{ + LogicalPublicInstanceSubqueryAgg, SUBQUERY_AGGREGATION_AGG_VKEY_HASH_IDX, + }, + verify_compute::types::LogicalPisVerifyComputeWithoutAccumulator, +}; + +use super::types::InputAxiomAggregation1; + +impl InputAxiomAggregation1 { + /// Builds general circuit + /// + /// Warning: this MUST return a circuit implementing `CircuitExt` with accumulator indices provided. + /// In particular, do not return `BaseCircuitBuilder`. + pub fn build( + self, + stage: CircuitBuilderStage, + circuit_params: AggregationCircuitParams, + kzg_params: &ParamsKZG, + ) -> Result { + let agg_vkey_hash_indices = vec![None, Some(SUBQUERY_AGGREGATION_AGG_VKEY_HASH_IDX), None]; + let snarks = [self.snark_verify_compute, self.snark_subquery_agg, self.snark_keccak_agg]; + for (i, snark) in snarks.iter().enumerate() { + if snark.agg_vk_hash_idx != agg_vkey_hash_indices[i] { + bail!("[AxiomAggregation1] agg_vkey_hash_idx mismatch in snark {i}"); + } + } + let (mut circuit, previous_instances, agg_vkey_hash) = + create_universal_aggregation_circuit::( + stage, + circuit_params, + kzg_params, + snarks.map(|s| s.inner).to_vec(), + agg_vkey_hash_indices, + ); + + let builder = &mut circuit.builder; + let ctx = builder.main(0); + + let [pis_verify_compute, instances_subquery_agg, mut instances_keccak]: [_; 3] = + previous_instances.try_into().unwrap(); + let pis_verify_compute = ComponentPublicInstances::try_from(pis_verify_compute)?; + + let LogicalPisVerifyComputeWithoutAccumulator { + source_chain_id, + compute_results_hash, + query_hash, + query_schema, + results_root_poseidon: promise_results_root_poseidon, + promise_subquery_hashes, + } = pis_verify_compute.other.try_into()?; + + let LogicalPublicInstanceSubqueryAgg { + promise_keccak, + agg_vkey_hash: _, // already read in create_universal_aggregation_circuit + results_root_poseidon, + commit_subquery_hashes, + mmr_keccak, + } = instances_subquery_agg.try_into()?; + + log::debug!("promise_results_root_poseidon: {:?}", promise_results_root_poseidon.value()); + log::debug!("results_root_poseidon: {:?}", results_root_poseidon.value()); + log::debug!("promise_subquery_hashes: {:?}", promise_subquery_hashes.value()); + log::debug!("commit_subquery_hashes: {:?}", commit_subquery_hashes.value()); + ctx.constrain_equal(&promise_results_root_poseidon, &results_root_poseidon); + ctx.constrain_equal(&promise_subquery_hashes, &commit_subquery_hashes); + + let commit_keccak = instances_keccak.pop().unwrap(); + // Await keccak promises: + // * The promise_keccak from SubqueryAggregation should directly equal the output commit of keccak component + // * The promise_result_commit from VerifyCompute should equal poseidon_hash([commit_keccak]) + log::debug!( + "subquery_agg promise_keccak: {:?} commit_keccak: {:?}", + promise_keccak.value(), + commit_keccak.value() + ); + ctx.constrain_equal(&promise_keccak, &commit_keccak); + // ======== Create Poseidon hasher =========== + let gate = GateChip::default(); + let mut hasher = PoseidonHasher::new(POSEIDON_SPEC.clone()); + hasher.initialize_consts(ctx, &gate); + let hashed_commit_keccak = hasher.hash_fix_len_array(ctx, &gate, &[commit_keccak]); + log::debug!("hash(commit_keccak): {:?}", hashed_commit_keccak.value()); + log::debug!( + "verify_compute promise_commit: {:?}", + pis_verify_compute.promise_result_commit.value() + ); + ctx.constrain_equal(&pis_verify_compute.promise_result_commit, &hashed_commit_keccak); + + let logical_pis = LogicalPublicInstanceAxiomAggregation { + source_chain_id, + compute_results_hash, + query_hash, + query_schema, + blockhash_mmr_keccak: mmr_keccak, + agg_vkey_hash, + payee: None, + }; + + if builder.assigned_instances.len() != 1 { + bail!("should only have 1 instance column"); + } + assert_eq!(builder.assigned_instances[0].len(), NUM_FE_ACCUMULATOR); + builder.assigned_instances[0].extend(logical_pis.flatten()); + + Ok(circuit) + } + + /// Circuit for witness generation only + pub fn prover_circuit( + self, + pinning: AggregationCircuitPinning, + kzg_params: &ParamsKZG, + ) -> Result { + Ok(self + .build(CircuitBuilderStage::Prover, pinning.params, kzg_params)? + .use_break_points(pinning.break_points)) + } +} diff --git a/axiom-query/src/axiom_aggregation1/mod.rs b/axiom-query/src/axiom_aggregation1/mod.rs new file mode 100644 index 00000000..3a15bf97 --- /dev/null +++ b/axiom-query/src/axiom_aggregation1/mod.rs @@ -0,0 +1,14 @@ +//! # Axiom Aggregation 1 Circuit +//! +//! The first layer of Axiom aggregation. +//! +//! This aggregates the Subquery Aggregation circuit, the Verify Compute circuit, and the Keccak final aggregation circuit. +//! It checks that the commitments to subquery results and subquery hashes from Subquery Aggregation circuit +//! match those in the Verify Compute circuit. +//! It also checks that all Keccak commitments agree among the circuits. + +pub mod circuit; +pub mod types; + +#[cfg(test)] +pub mod tests; diff --git a/axiom-query/src/axiom_aggregation1/tests.rs b/axiom-query/src/axiom_aggregation1/tests.rs new file mode 100644 index 00000000..d463498b --- /dev/null +++ b/axiom-query/src/axiom_aggregation1/tests.rs @@ -0,0 +1,171 @@ +use std::fs::File; + +use anyhow::Result; +use axiom_eth::{ + halo2_base::{gates::circuit::CircuitBuilderStage, utils::fs::gen_srs}, + halo2curves::bn256::Fr, + keccak::types::OutputKeccakShard, + snark_verifier_sdk::{ + gen_pk, + halo2::{gen_snark_shplonk, read_snark}, + CircuitExt, + }, + utils::{ + build_utils::pinning::PinnableCircuit, + merkle_aggregation::InputMerkleAggregation, + snark_verifier::{AggregationCircuitParams, EnhancedSnark, NUM_FE_ACCUMULATOR}, + }, + zkevm_hashes::keccak::component::circuit::shard::{ + KeccakComponentShardCircuit, KeccakComponentShardCircuitParams, + }, +}; +use itertools::Itertools; +use test_log::test; + +use crate::axiom_aggregation1::types::FINAL_AGG_VKEY_HASH_IDX; + +use super::types::InputAxiomAggregation1; + +fn get_keccak_snark() -> Result { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + // single shard + let output_shard: OutputKeccakShard = serde_json::from_reader(File::open(format!( + "{cargo_manifest_dir}/data/test/promise_results_keccak_for_agg.json" + ))?)?; + let k = 18u32; + let mut keccak_params = + KeccakComponentShardCircuitParams::new(k as usize, 109, output_shard.capacity, false); + keccak_params.base_circuit_params = + KeccakComponentShardCircuit::::calculate_base_circuit_params(&keccak_params); + + let params = gen_srs(k); + let keygen_circuit = + KeccakComponentShardCircuit::::new(vec![], keccak_params.clone(), false); + let pk = gen_pk(¶ms, &keygen_circuit, None); + let break_points = keygen_circuit.base_circuit_break_points(); + + let inputs = output_shard.responses.iter().map(|(k, _)| k.to_vec()).collect_vec(); + let prover_circuit = KeccakComponentShardCircuit::::new(inputs, keccak_params, true); + prover_circuit.set_base_circuit_break_points(break_points); + let snark_path = format!("{cargo_manifest_dir}/data/test/keccak_shard_for_agg.snark"); + let snark = gen_snark_shplonk(¶ms, &pk, prover_circuit, Some(snark_path)); + + let k = 20u32; + let params = gen_srs(k); + let agg_input = InputMerkleAggregation::new([EnhancedSnark::new(snark, None)]); + + let circuit_params = + AggregationCircuitParams { degree: k, lookup_bits: k as usize - 1, ..Default::default() }; + let mut keygen_circuit = + agg_input.clone().build(CircuitBuilderStage::Keygen, circuit_params, ¶ms)?; + keygen_circuit.calculate_params(Some(20)); + let name = "keccak_for_agg"; + let pinning_path = format!("{cargo_manifest_dir}/configs/test/{name}.json"); + let pk_path = format!("{cargo_manifest_dir}/data/test/{name}.pk"); + let snark_path = format!("{cargo_manifest_dir}/data/test/{name}.snark"); + let (pk, pinning) = keygen_circuit.create_pk(¶ms, pk_path, pinning_path)?; + + let prover_circuit = agg_input.prover_circuit(pinning, ¶ms)?; + let snark = gen_snark_shplonk(¶ms, &pk, prover_circuit, Some(snark_path)); + Ok(EnhancedSnark::new(snark, None)) +} + +fn get_test_input() -> Result { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + let verify_compute_snark = + read_snark(format!("{cargo_manifest_dir}/data/test/verify_compute_for_agg.snark"))?; + let snark_verify_compute = EnhancedSnark::new(verify_compute_snark, None); + + let snark_subquery_agg: EnhancedSnark = serde_json::from_reader(File::open(format!( + "{cargo_manifest_dir}/data/test/subquery_aggregation_for_agg.snark.json" + ))?)?; + + let snark_keccak_agg = if let Ok(snark) = + read_snark(format!("{cargo_manifest_dir}/data/test/keccak_for_agg.snark")) + { + EnhancedSnark::new(snark, None) + } else { + get_keccak_snark()? + }; + + Ok(InputAxiomAggregation1 { snark_verify_compute, snark_subquery_agg, snark_keccak_agg }) +} + +#[test] +#[ignore = "prover"] +fn test_prover_axiom_agg1() -> anyhow::Result<()> { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + + let k = 22; + let params = gen_srs(k as u32); + + let input = get_test_input()?; + let mut keygen_circuit = input.clone().build( + CircuitBuilderStage::Keygen, + AggregationCircuitParams { degree: k as u32, lookup_bits: k - 1, ..Default::default() }, + ¶ms, + )?; + keygen_circuit.calculate_params(Some(20)); + let instance1 = keygen_circuit.instances(); + let abs_agg_vk_hash_idx = NUM_FE_ACCUMULATOR + FINAL_AGG_VKEY_HASH_IDX; + let name = "axiom_aggregation1_for_agg"; + let pinning_path = format!("{cargo_manifest_dir}/configs/test/{name}.json"); + let pk_path = format!("{cargo_manifest_dir}/data/test/{name}.pk"); + let (pk, pinning) = keygen_circuit.create_pk(¶ms, pk_path, pinning_path)?; + keygen_circuit.builder.clear(); + drop(keygen_circuit); + + #[cfg(all(feature = "keygen", not(debug_assertions)))] + { + // test keygen + use crate::subquery_aggregation::types::SUBQUERY_AGGREGATION_AGG_VKEY_HASH_IDX; + use axiom_eth::halo2_proofs::{plonk::keygen_vk, SerdeFormat}; + use axiom_eth::snark_verifier_sdk::{halo2::gen_dummy_snark_from_protocol, SHPLONK}; + use axiom_eth::utils::build_utils::aggregation::get_dummy_aggregation_params; + let [dum_snark_verify_comp, mut dum_snark_sub_agg, dum_snark_keccak] = + [&input.snark_verify_compute, &input.snark_subquery_agg, &input.snark_keccak_agg] + .map(|s| gen_dummy_snark_from_protocol::(s.inner.protocol.clone())); + let subquery_abs_agg_vk_hash_idx = + NUM_FE_ACCUMULATOR + SUBQUERY_AGGREGATION_AGG_VKEY_HASH_IDX; + // The correct one from the subquery agg circuit + let subquery_agg_vk_hash = + input.snark_subquery_agg.inner.instances[0][subquery_abs_agg_vk_hash_idx]; + // Put correct one into the dummy + dum_snark_sub_agg.instances[0][subquery_abs_agg_vk_hash_idx] = subquery_agg_vk_hash; + let input = InputAxiomAggregation1 { + snark_verify_compute: EnhancedSnark::new(dum_snark_verify_comp, None), + snark_subquery_agg: EnhancedSnark::new( + dum_snark_sub_agg, + Some(SUBQUERY_AGGREGATION_AGG_VKEY_HASH_IDX), + ), + snark_keccak_agg: EnhancedSnark::new(dum_snark_keccak, None), + }; + let mut circuit = + input.build(CircuitBuilderStage::Keygen, get_dummy_aggregation_params(k), ¶ms)?; + circuit.calculate_params(Some(20)); + let vk = keygen_vk(¶ms, &circuit)?; + if pk.get_vk().to_bytes(SerdeFormat::RawBytes) != vk.to_bytes(SerdeFormat::RawBytes) { + panic!("vk mismatch"); + } + let instance2 = circuit.instances(); + assert_eq!( + instance1[0][abs_agg_vk_hash_idx], instance2[0][abs_agg_vk_hash_idx], + "agg vkey hash mismatch" + ); + } + + let mut prover_circuit = input.build(CircuitBuilderStage::Prover, pinning.params, ¶ms)?; + prover_circuit.set_break_points(pinning.break_points); + let instance3 = prover_circuit.instances(); + assert_eq!( + instance1[0][abs_agg_vk_hash_idx], instance3[0][abs_agg_vk_hash_idx], + "agg vkey hash mismatch" + ); + + let snark = gen_snark_shplonk(¶ms, &pk, prover_circuit, None::<&str>); + let snark = EnhancedSnark { inner: snark, agg_vk_hash_idx: Some(FINAL_AGG_VKEY_HASH_IDX) }; + + let snark_path = format!("{cargo_manifest_dir}/data/test/{name}.snark.json"); + serde_json::to_writer(File::create(snark_path)?, &snark)?; + Ok(()) +} diff --git a/axiom-query/src/axiom_aggregation1/types.rs b/axiom-query/src/axiom_aggregation1/types.rs new file mode 100644 index 00000000..e6af42ec --- /dev/null +++ b/axiom-query/src/axiom_aggregation1/types.rs @@ -0,0 +1,75 @@ +use std::iter; + +use axiom_codec::HiLo; +use axiom_eth::utils::snark_verifier::EnhancedSnark; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InputAxiomAggregation1 { + pub snark_verify_compute: EnhancedSnark, + pub snark_subquery_agg: EnhancedSnark, + /// Snark of aggregation circuit for keccak component shards + pub snark_keccak_agg: EnhancedSnark, +} + +const NUM_LOGICAL_INSTANCE_NO_PAYEE: usize = 1 + 1 + 2 + 2 + 2 + 2; +pub const NUM_LOGICAL_INSTANCE_WITH_PAYEE: usize = NUM_LOGICAL_INSTANCE_NO_PAYEE + 1; +pub const FINAL_AGG_VKEY_HASH_IDX: usize = NUM_LOGICAL_INSTANCE_NO_PAYEE - 1; + +/// The public instances of the AxiomAggregation1 and AxiomAggregation2 circuits, +/// excluding the accumulator at the beginning. +/// The `payee` field is only provided and exposed in AxiomAggregation2. +/// We use the same struct for both circuits for uniformity. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)] +#[serde(rename_all = "camelCase")] +pub struct LogicalPublicInstanceAxiomAggregation { + pub source_chain_id: T, + pub compute_results_hash: HiLo, + pub query_hash: HiLo, + pub query_schema: HiLo, + pub blockhash_mmr_keccak: HiLo, + pub agg_vkey_hash: T, + #[serde(skip_serializing_if = "Option::is_none")] + pub payee: Option, +} + +impl LogicalPublicInstanceAxiomAggregation { + pub fn flatten(&self) -> Vec { + iter::once(self.source_chain_id) + .chain(self.compute_results_hash.hi_lo()) + .chain(self.query_hash.hi_lo()) + .chain(self.query_schema.hi_lo()) + .chain(self.blockhash_mmr_keccak.hi_lo()) + .chain([self.agg_vkey_hash]) + .chain(self.payee) + .collect() + } +} + +impl TryFrom> for LogicalPublicInstanceAxiomAggregation { + type Error = anyhow::Error; + fn try_from(value: Vec) -> Result { + if value.len() != NUM_LOGICAL_INSTANCE_NO_PAYEE + && value.len() != NUM_LOGICAL_INSTANCE_NO_PAYEE + 1 + { + anyhow::bail!("invalid number of instances"); + } + let source_chain_id = value[0]; + let compute_results_hash = HiLo::from_hi_lo([value[1], value[2]]); + let query_hash = HiLo::from_hi_lo([value[3], value[4]]); + let query_schema = HiLo::from_hi_lo([value[5], value[6]]); + let blockhash_mmr_keccak = HiLo::from_hi_lo([value[7], value[8]]); + let agg_vkey_hash = value[9]; + let payee = value.get(NUM_LOGICAL_INSTANCE_NO_PAYEE).copied(); + Ok(Self { + source_chain_id, + compute_results_hash, + query_hash, + query_schema, + blockhash_mmr_keccak, + agg_vkey_hash, + payee, + }) + } +} diff --git a/axiom-query/src/axiom_aggregation2/circuit.rs b/axiom-query/src/axiom_aggregation2/circuit.rs new file mode 100644 index 00000000..6ec551d8 --- /dev/null +++ b/axiom-query/src/axiom_aggregation2/circuit.rs @@ -0,0 +1,80 @@ +use anyhow::{bail, Result}; +use axiom_codec::utils::native::encode_addr_to_field; +use axiom_eth::{ + halo2_base::gates::circuit::CircuitBuilderStage, + halo2_proofs::poly::kzg::commitment::ParamsKZG, + halo2curves::bn256::Bn256, + snark_verifier_sdk::{halo2::aggregation::AggregationCircuit, SHPLONK}, + utils::{ + build_utils::pinning::aggregation::AggregationCircuitPinning, + snark_verifier::{ + create_universal_aggregation_circuit, AggregationCircuitParams, EnhancedSnark, + NUM_FE_ACCUMULATOR, + }, + }, +}; +use ethers_core::types::Address; +use serde::{Deserialize, Serialize}; + +use crate::axiom_aggregation1::types::{ + LogicalPublicInstanceAxiomAggregation, FINAL_AGG_VKEY_HASH_IDX, +}; + +#[derive(Clone, Debug, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InputAxiomAggregation2 { + pub payee: Address, + /// Snark from AxiomAggregation1 + pub snark_axiom_agg1: EnhancedSnark, +} + +impl InputAxiomAggregation2 { + pub fn build( + self, + stage: CircuitBuilderStage, + circuit_params: AggregationCircuitParams, + kzg_params: &ParamsKZG, + ) -> anyhow::Result { + if self.snark_axiom_agg1.agg_vk_hash_idx != Some(FINAL_AGG_VKEY_HASH_IDX) { + bail!("AxiomAggregation1 snark agg_vkey_hash_idx exception"); + } + let (mut circuit, mut previous_instances, agg_vkey_hash) = + create_universal_aggregation_circuit::( + stage, + circuit_params, + kzg_params, + vec![self.snark_axiom_agg1.inner], + vec![Some(FINAL_AGG_VKEY_HASH_IDX)], + ); + let instances = previous_instances.pop().unwrap(); + let prev_pis = LogicalPublicInstanceAxiomAggregation::try_from(instances)?; + + let builder = &mut circuit.builder; + let payee = builder.main(0).load_witness(encode_addr_to_field(&self.payee)); + + let logical_pis = LogicalPublicInstanceAxiomAggregation { + agg_vkey_hash, // use new agg_vkey_hash + payee: Some(payee), // previously there was no payee + ..prev_pis // re-expose previous public instances + }; + + if builder.assigned_instances.len() != 1 { + bail!("should only have 1 instance column"); + } + assert_eq!(builder.assigned_instances[0].len(), NUM_FE_ACCUMULATOR); + builder.assigned_instances[0].extend(logical_pis.flatten()); + + Ok(circuit) + } + + /// Circuit for witness generation only + pub fn prover_circuit( + self, + pinning: AggregationCircuitPinning, + kzg_params: &ParamsKZG, + ) -> Result { + Ok(self + .build(CircuitBuilderStage::Prover, pinning.params, kzg_params)? + .use_break_points(pinning.break_points)) + } +} diff --git a/axiom-query/src/axiom_aggregation2/integration_test.sh b/axiom-query/src/axiom_aggregation2/integration_test.sh new file mode 100644 index 00000000..0dc3c1c3 --- /dev/null +++ b/axiom-query/src/axiom_aggregation2/integration_test.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +if [ -z $PARAMS_DIR ]; then + echo "PARAMS_DIR not set" + exit 1 +fi + +repo_root=$(git rev-parse --show-toplevel) +cd $repo_root/axiom-query + +set -e + +rm -f data/test/*.pk +rm -f data/test/*.snark +export CARGO_PROFILE_DEV_DEBUG_ASSERTIONS=false +export RUST_LOG=debug +export RUST_BACKTRACE=1 + +cargo t test_mock_header_subquery +cargo t test_mock_results_root_header_only_for_agg -- --ignored +cargo t test_verify_compute_prepare_for_agg -- --ignored +cargo t test_merge_keccak_shards_for_agg -- --ignored + +cargo t test_prover_subquery_agg -- --ignored --nocapture +cargo t test_verify_compute_prover_for_agg -- --ignored --nocapture +cargo t test_prover_axiom_agg1 -- --ignored --nocapture +cargo t test_prover_axiom_agg2 --features revm -- --ignored --nocapture diff --git a/axiom-query/src/axiom_aggregation2/mod.rs b/axiom-query/src/axiom_aggregation2/mod.rs new file mode 100644 index 00000000..ca324414 --- /dev/null +++ b/axiom-query/src/axiom_aggregation2/mod.rs @@ -0,0 +1,10 @@ +//! # Axiom Aggregation 2 Circuit +//! +//! The second (and currently final) layer of Axiom aggregation. +//! This circuit aggregates the single snark of the Axiom Aggregation 1 circuit. It exposes the same +//! public instances but also adds a `payee` instance. + +pub mod circuit; + +#[cfg(test)] +pub mod tests; diff --git a/axiom-query/src/axiom_aggregation2/readme.md b/axiom-query/src/axiom_aggregation2/readme.md new file mode 100644 index 00000000..5e336784 --- /dev/null +++ b/axiom-query/src/axiom_aggregation2/readme.md @@ -0,0 +1,18 @@ +To run all the tests leading up to the final `AxiomAggregation2` test, run the following in the `axiom-query` directory: + +Remove any `data/test/*.snark` and `data/test/*.pk` files if you don't want caching. + +```bash +cargo t test_prover_subquery_agg -- --ignored --nocapture +cargo t test_verify_compute_prover_for_agg -- --ignored --nocapture +cargo t test_prover_axiom_agg1 -- --ignored --nocapture +cargo t test_prover_axiom_agg2 --features revm -- --ignored --nocapture +``` +If feature "keygen" is on, then you must run with `CARGO_PROFILE_DEV_DEBUG_ASSERTIONS=false` because we check keygen using dummy snarks that do not pass debug assertions. + +The final test will generate the EVM proof and try to run it against the snark verifier smart contract if you enable feature "revm". For the latter you need Solidity version 0.8.19 (the pragma is fixed to 0.8.19 in snark verifier). + +Note the computeProof was generated using the trusted setup [here ](https://docs.axiom.xyz/transparency-and-security/kzg-trusted-setup) so for aggregation circuits to be consistent you will also need to download the same trusted setup and either put it in `axiom-query/params` or set `PARAMS_DIR` environmental variable. + +## Regenerating test inputs +The tests above depend on certain pre-generated input files. To regenerate them and run all commands above, use the [integration_test.sh](./integration_test.sh) script. diff --git a/axiom-query/src/axiom_aggregation2/tests.rs b/axiom-query/src/axiom_aggregation2/tests.rs new file mode 100644 index 00000000..164c9536 --- /dev/null +++ b/axiom-query/src/axiom_aggregation2/tests.rs @@ -0,0 +1,138 @@ +use std::{fs::File, io::Write, path::Path, str::FromStr}; + +use anyhow::Result; +use axiom_eth::{ + halo2_base::{gates::circuit::CircuitBuilderStage, utils::fs::gen_srs}, + halo2_proofs::dev::MockProver, + snark_verifier_sdk::{ + evm::{encode_calldata, gen_evm_proof_shplonk, gen_evm_verifier_shplonk}, + halo2::aggregation::AggregationCircuit, + CircuitExt, + }, + utils::{ + build_utils::pinning::PinnableCircuit, + snark_verifier::{AggregationCircuitParams, EnhancedSnark, NUM_FE_ACCUMULATOR}, + }, +}; +use ethers_core::types::Address; +use hex::encode; + +use crate::axiom_aggregation1::types::FINAL_AGG_VKEY_HASH_IDX; + +use super::circuit::InputAxiomAggregation2; + +fn get_test_input() -> Result { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + let snark_axiom_agg1: EnhancedSnark = serde_json::from_reader(File::open(format!( + "{cargo_manifest_dir}/data/test/axiom_aggregation1_for_agg.snark.json" + ))?)?; + + Ok(InputAxiomAggregation2 { + snark_axiom_agg1, + payee: Address::from_str("0x00000000000000000000000000000000deadbeef")?, + }) +} + +#[test] +#[ignore = "requires real SRS"] +fn test_mock_axiom_agg2() -> anyhow::Result<()> { + let k = 22; + let params = gen_srs(k as u32); + + let input = get_test_input()?; + let mut circuit = input.build( + CircuitBuilderStage::Mock, + AggregationCircuitParams { degree: k as u32, lookup_bits: k - 1, ..Default::default() }, + ¶ms, + )?; + circuit.calculate_params(Some(20)); + let instances = circuit.instances(); + MockProver::run(k as u32, &circuit, instances).unwrap().assert_satisfied(); + + Ok(()) +} + +// cargo t test_prover_axiom_agg2 --features revm -- --ignored --nocapture +// feature "revm" requires solc 0.8.19 installed +#[test] +#[ignore = "prover"] +fn test_prover_axiom_agg2() -> anyhow::Result<()> { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + + let k = 23; + let params = gen_srs(k as u32); + + let input = get_test_input()?; + let mut keygen_circuit = input.clone().build( + CircuitBuilderStage::Keygen, + AggregationCircuitParams { degree: k as u32, lookup_bits: k - 1, ..Default::default() }, + ¶ms, + )?; + keygen_circuit.calculate_params(Some(20)); + let instance1 = keygen_circuit.instances(); + let abs_agg_vk_hash_idx = NUM_FE_ACCUMULATOR + FINAL_AGG_VKEY_HASH_IDX; + let name = "axiom_aggregation2"; + let pinning_path = format!("{cargo_manifest_dir}/configs/test/{name}.json"); + let pk_path = format!("{cargo_manifest_dir}/data/test/{name}.pk"); + let (pk, pinning) = keygen_circuit.create_pk(¶ms, pk_path, pinning_path)?; + keygen_circuit.builder.clear(); + drop(keygen_circuit); + + #[cfg(all(feature = "keygen", not(debug_assertions)))] + { + // test keygen + use axiom_eth::halo2_proofs::{plonk::keygen_vk, SerdeFormat}; + use axiom_eth::snark_verifier_sdk::{halo2::gen_dummy_snark_from_protocol, SHPLONK}; + use axiom_eth::utils::build_utils::aggregation::get_dummy_aggregation_params; + let mut dum_snark_axiom_agg1 = + gen_dummy_snark_from_protocol::(input.snark_axiom_agg1.inner.protocol.clone()); + // The correct one from the subquery agg circuit + let axiom_agg1_agg_vk_hash = input.snark_axiom_agg1.inner.instances[0][abs_agg_vk_hash_idx]; + // Put correct one into the dummy + dum_snark_axiom_agg1.instances[0][abs_agg_vk_hash_idx] = axiom_agg1_agg_vk_hash; + let input = InputAxiomAggregation2 { + snark_axiom_agg1: EnhancedSnark::new( + dum_snark_axiom_agg1, + Some(FINAL_AGG_VKEY_HASH_IDX), + ), + payee: Default::default(), + }; + let mut circuit = + input.build(CircuitBuilderStage::Keygen, get_dummy_aggregation_params(k), ¶ms)?; + circuit.calculate_params(Some(20)); + let vk = keygen_vk(¶ms, &circuit)?; + if pk.get_vk().to_bytes(SerdeFormat::RawBytes) != vk.to_bytes(SerdeFormat::RawBytes) { + panic!("vk mismatch"); + } + let instance2 = circuit.instances(); + assert_eq!( + instance1[0][abs_agg_vk_hash_idx], instance2[0][abs_agg_vk_hash_idx], + "agg vkey hash mismatch" + ); + } + + let mut prover_circuit = input.build(CircuitBuilderStage::Prover, pinning.params, ¶ms)?; + prover_circuit.set_break_points(pinning.break_points); + let instance3 = prover_circuit.instances(); + assert_eq!( + instance1[0][abs_agg_vk_hash_idx], instance3[0][abs_agg_vk_hash_idx], + "agg vkey hash mismatch" + ); + + let sol_path = format!("{cargo_manifest_dir}/data/test/{name}.sol"); + let _solidity_code = gen_evm_verifier_shplonk::( + ¶ms, + pk.get_vk(), + prover_circuit.num_instance(), + Some(Path::new(&sol_path)), + ); + let instances = prover_circuit.instances(); + let proof = gen_evm_proof_shplonk(¶ms, &pk, prover_circuit, instances.clone()); + let evm_proof = encode(encode_calldata(&instances, &proof)); + let mut f = File::create(format!("{cargo_manifest_dir}/data/test/{name}.evm_proof"))?; + write!(f, "{evm_proof}")?; + #[cfg(feature = "revm")] + axiom_eth::snark_verifier_sdk::evm::evm_verify(_solidity_code, instances, proof); + + Ok(()) +} diff --git a/axiom-query/src/bin/keygen.rs b/axiom-query/src/bin/keygen.rs new file mode 100644 index 00000000..a7f78989 --- /dev/null +++ b/axiom-query/src/bin/keygen.rs @@ -0,0 +1,81 @@ +use std::{ + env, + fs::{self, File}, + path::PathBuf, +}; + +use anyhow::Context; +use axiom_eth::{ + snark_verifier_sdk::{evm::gen_evm_verifier_shplonk, halo2::aggregation::AggregationCircuit}, + utils::build_utils::keygen::read_srs_from_dir, +}; +use axiom_query::keygen::{ + agg::{common::AggTreePinning, SupportedAggPinning}, + ProvingKeySerializer, SupportedPinning, SupportedRecursiveIntent, +}; +use clap::Parser; + +#[derive(Parser, Debug)] +pub struct Cli { + #[arg(long = "srs-dir")] + pub srs_dir: PathBuf, + #[arg(long = "data-dir")] + pub data_dir: Option, + #[arg(long = "intent")] + pub intent_path: PathBuf, + /// Tag for proof tree file. + /// Defaults to the aggregate vkey hash (Fr, big-endian) if the circuit is a universal aggregation circuit, or to the root circuit ID otherwise. + #[arg(short, long = "tag")] + pub tag: Option, +} + +fn main() -> anyhow::Result<()> { + env_logger::try_init().unwrap(); + let cli = Cli::parse(); + let srs_dir = cli.srs_dir; + let data_dir = cli.data_dir.unwrap_or_else(|| { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + PathBuf::from(cargo_manifest_dir).join("data").join("playground") + }); + fs::create_dir_all(&data_dir).context("Create data dir")?; + // Directly deserializing from yaml doesn't work, but going to json first does?? + let intent_json: serde_json::Value = + serde_yaml::from_reader(File::open(&cli.intent_path).with_context(|| { + format!("Failed to open intent file {}", cli.intent_path.display()) + })?)?; + let intent: SupportedRecursiveIntent = serde_json::from_value(intent_json)?; + let (proof_node, pk, pinning) = intent.create_and_serialize_proving_key(&srs_dir, &data_dir)?; + println!("Circuit id: {}", proof_node.circuit_id); + + let tag = cli.tag.unwrap_or_else(|| { + if let Some((_, agg_vk_hash)) = pinning.agg_vk_hash_data() { + format!("{:?}", agg_vk_hash) + } else { + proof_node.circuit_id.clone() + } + }); + + let tree_path = data_dir.join(format!("{tag}.tree")); + let f = File::create(&tree_path).with_context(|| { + format!("Failed to create aggregation tree file {}", tree_path.display()) + })?; + serde_json::to_writer_pretty(f, &proof_node)?; + println!("Wrote aggregation tree to {}", tree_path.display()); + + if let SupportedPinning::Agg(SupportedAggPinning::AxiomAgg2(pinning)) = pinning { + log::debug!("Creating verifier contract"); + let num_instance = pinning.num_instance(); + let solc_path = data_dir.join(&proof_node.circuit_id).with_extension("sol"); + let k = pinning.params.agg_params.degree; + let kzg_params = read_srs_from_dir(&srs_dir, k)?; + gen_evm_verifier_shplonk::( + &kzg_params, + pk.get_vk(), + num_instance, + Some(&solc_path), + ); + println!("Axiom Aggregation 2 snark-verifier contract written to {}", solc_path.display()); + } + + Ok(()) +} diff --git a/axiom-query/src/bin/readme.md b/axiom-query/src/bin/readme.md new file mode 100644 index 00000000..c96855ff --- /dev/null +++ b/axiom-query/src/bin/readme.md @@ -0,0 +1,22 @@ +# Proving and Verifying Key Generation + +To recursively run proving key generation on all circuits in an aggregation tree specified by an intent file, you can install the keygen binary to your path via: + +```bash +cargo install --path axiom-query --force +``` +This builds `axiom-query-keygen` binary in release mode and installs it to your path. +Then run: +```bash +axiom-query-keygen --srs-dir --intent configs/templates/axiom_agg_2.yml --tag --data-dir +``` +to actually generate the proving keys. + +For faster compile times, you can run the keygen binary directly in dev mode (still with `opt-level=3`) via: +```bash +CARGO_PROFILE_DEV_DEBUG_ASSERTIONS=false cargo run --bin axiom-query-keygen -- --srs-dir --intent configs/templates/axiom_agg_2.yml --tag --data-dir +``` +Debug assertions needs to be **off** as we use dummy witnesses that do not pass certain debug assertions. + +* A file with full aggregation tree of circuit IDs will be output to a `.tree` file as a JSON. +* `` defaults to `` if not specified. diff --git a/axiom-query/src/bin/rename_snark_verifier.sh b/axiom-query/src/bin/rename_snark_verifier.sh new file mode 100644 index 00000000..74fb95e3 --- /dev/null +++ b/axiom-query/src/bin/rename_snark_verifier.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Check if a file name is provided +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +# File to be modified +FILE="$1" + +new_file="AxiomV2QueryVerifier.sol" + +# The first and third lines are empty +# Remove the first and third line from the file +sed '1d;3d' $FILE > $new_file + +# Replace 'contract Halo2Verifier' with 'contract AxiomV2QueryVerifier' +sed 's/contract Halo2Verifier/contract AxiomV2QueryVerifier/g' $new_file > $new_file.tmp && mv $new_file.tmp $new_file + +echo "Modifications complete. Written to $new_file" + +echo "To diff is:" +diff $FILE $new_file + +echo "Running forge fmt on $new_file" +forge fmt $new_file diff --git a/axiom-query/src/components/mod.rs b/axiom-query/src/components/mod.rs new file mode 100644 index 00000000..51eddade --- /dev/null +++ b/axiom-query/src/components/mod.rs @@ -0,0 +1,17 @@ +use axiom_eth::rlc::circuit::RlcCircuitParams; + +/// Computes results root (keccak), results root (poseidon), and subquery hashes by making promise calls to subquery circuits. +pub mod results; +/// The subquery circuits +pub mod subqueries; + +pub const MAX_MERKLE_TREE_HEIGHT_FOR_KECCAK_RESULTS: usize = 3; +/// Helper function for testing to create a dummy RlcCircuitParams +pub fn dummy_rlc_circuit_params(k: usize) -> RlcCircuitParams { + let mut circuit_params = RlcCircuitParams::default(); + circuit_params.base.k = k; + circuit_params.base.lookup_bits = Some(8); + circuit_params.base.num_instance_columns = 1; + circuit_params.num_rlc_columns = 1; + circuit_params +} diff --git a/axiom-query/src/components/results/circuit.rs b/axiom-query/src/components/results/circuit.rs new file mode 100644 index 00000000..4ce71009 --- /dev/null +++ b/axiom-query/src/components/results/circuit.rs @@ -0,0 +1,199 @@ +use axiom_codec::constants::{MAX_SUBQUERY_OUTPUTS, NUM_SUBQUERY_TYPES}; +use axiom_eth::{ + component_type_list, + halo2_base::{ + gates::{GateInstructions, RangeInstructions}, + halo2_proofs::plonk::ConstraintSystem, + AssignedValue, + QuantumCell::Constant, + }, + keccak::{types::ComponentTypeKeccak, KeccakChip}, + rlc::circuit::builder::RlcCircuitBuilder, + utils::{ + build_utils::aggregation::CircuitMetadata, + component::{ + circuit::{ + ComponentBuilder, ComponentCircuitImpl, CoreBuilder, CoreBuilderOutput, + CoreBuilderOutputParams, CoreBuilderParams, + }, + promise_collector::PromiseCaller, + promise_loader::{ + combo::PromiseBuilderCombo, multi::MultiPromiseLoader, single::PromiseLoader, + }, + types::FixLenLogical, + utils::create_hasher, + }, + }, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::{ + components::subqueries::{ + account::types::ComponentTypeAccountSubquery, + block_header::types::ComponentTypeHeaderSubquery, + receipt::types::ComponentTypeReceiptSubquery, + solidity_mappings::types::ComponentTypeSolidityNestedMappingSubquery, + storage::types::ComponentTypeStorageSubquery, transaction::types::ComponentTypeTxSubquery, + }, + Field, +}; + +use super::{ + results_root::get_results_root, + subquery_hash::get_subquery_hash, + types::{ + CircuitInputResultsRootShard, ComponentTypeResultsRoot, LogicalPublicInstanceResultsRoot, + RlcAdapterResultsRoot, SubqueryResultCall, VirtualComponentType, + }, +}; + +/// Core builder for results root component. +pub struct CoreBuilderResultsRoot { + pub input: Option>, + pub params: CoreParamsResultRoot, +} + +/// Specify the output format of ResultRoot component. +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct CoreParamsResultRoot { + /// - `enabled_types[subquery_type as u16]` is true if the subquery type is enabled. + /// - `enabled_types[0]` corresponds to the Null type. It doesn't matter whether it's enabled or disabled; behavior remains the same. + pub enabled_types: [bool; NUM_SUBQUERY_TYPES], + /// Maximum total number of subquery results supported + pub capacity: usize, +} +impl CoreBuilderParams for CoreParamsResultRoot { + fn get_output_params(&self) -> CoreBuilderOutputParams { + CoreBuilderOutputParams::new(vec![]) + } +} + +/// Subquery dependencies of ResultsRoot component. +pub type SubqueryDependencies = component_type_list!( + F, + ComponentTypeHeaderSubquery, + ComponentTypeAccountSubquery, + ComponentTypeStorageSubquery, + ComponentTypeTxSubquery, + ComponentTypeReceiptSubquery, + ComponentTypeSolidityNestedMappingSubquery +); + +pub type PromiseLoaderResultsRoot = PromiseBuilderCombo< + F, + PromiseLoader>, // keccak + MultiPromiseLoader< + F, + VirtualComponentType, + SubqueryDependencies, + RlcAdapterResultsRoot, + >, // Grouped subquery results +>; +pub type ComponentCircuitResultsRoot = + ComponentCircuitImpl, PromiseLoaderResultsRoot>; + +impl CircuitMetadata for CoreBuilderResultsRoot { + const HAS_ACCUMULATOR: bool = false; + fn num_instance(&self) -> Vec { + // currently implemented at the ComponentCircuitImpl level + unreachable!() + } +} + +impl ComponentBuilder for CoreBuilderResultsRoot { + type Params = CoreParamsResultRoot; + fn new(params: Self::Params) -> Self { + Self { input: None, params } + } + fn get_params(&self) -> Self::Params { + self.params.clone() + } + fn clear_witnesses(&mut self) {} + fn calculate_params(&mut self) -> Self::Params { + self.params.clone() + } + fn configure_with_params(_: &mut ConstraintSystem, _: Self::Params) {} +} + +impl CoreBuilder for CoreBuilderResultsRoot { + type CompType = ComponentTypeResultsRoot; + type PublicInstanceValue = LogicalPublicInstanceResultsRoot; + type PublicInstanceWitness = LogicalPublicInstanceResultsRoot>; + type CoreInput = CircuitInputResultsRootShard; + + fn feed_input(&mut self, mut input: Self::CoreInput) -> anyhow::Result<()> { + if input.subqueries.len() > self.params.capacity { + anyhow::bail!( + "Subquery results table is greater than capcaity - {} > {}", + input.subqueries.len(), + self.params.capacity + ); + } + let cap = self.params.capacity; + input.subqueries.rows.resize(cap, input.subqueries.rows[0]); + self.input = Some(input); + Ok(()) + } + fn virtual_assign_phase0( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_caller: PromiseCaller, + ) -> CoreBuilderOutput { + let keccak = + KeccakChip::new_with_promise_collector(builder.range_chip(), promise_caller.clone()); + let range = keccak.range(); + let gate = &range.gate; + + // Assumption: we already have input when calling this function. + // TODO: automatically derive a dummy input from params. + let input = self.input.as_ref().unwrap(); + + let ctx = builder.base.main(0); + // assign subquery results + let subqueries = input.subqueries.assign(ctx).rows; + // Make virtual promise calls. + for subquery in &subqueries { + promise_caller + .call::, VirtualComponentType>( + ctx, + SubqueryResultCall(*subquery), + ) + .unwrap(); + } + + // compute subquery hashes (keccak) + let subquery_hashes = subqueries + .iter() + .map(|row| get_subquery_hash(ctx, &keccak, &row.key, &self.params.enabled_types)) + .collect_vec(); + + let num_subqueries = ctx.load_witness(input.num_subqueries); + range.check_less_than_safe(ctx, num_subqueries, subqueries.len() as u64 + 1); + + let mut poseidon = create_hasher(); + poseidon.initialize_consts(ctx, gate); + // compute resultsRootPoseidon + let results_root_poseidon = + get_results_root(ctx, range, &poseidon, &subqueries, num_subqueries); + + let commit_subquery_hashes = { + // take variable length list of `num_subqueries` subquery hashes and flat hash them all together + // the reason we use variable length is so the hash does not depend on `subquery_hashes.len()` and only on `num_subqueries`. this allows more flexibility in the VerifyCompute circuit + let to_commit = subquery_hashes.into_iter().flat_map(|hash| hash.hi_lo()).collect_vec(); + let len = gate.mul(ctx, num_subqueries, Constant(F::from(MAX_SUBQUERY_OUTPUTS as u64))); + poseidon.hash_var_len_array(ctx, range, &to_commit, len) + }; + + // This component is never called directly. + // The VerifyCompute circuit will read the other public instances and decommit them directly. + let pis = + LogicalPublicInstanceResultsRoot { results_root_poseidon, commit_subquery_hashes }; + + CoreBuilderOutput:: { + public_instances: pis.into_raw(), + virtual_table: vec![], + logical_results: vec![], + } + } +} diff --git a/axiom-query/src/components/results/mod.rs b/axiom-query/src/components/results/mod.rs new file mode 100644 index 00000000..7087c54d --- /dev/null +++ b/axiom-query/src/components/results/mod.rs @@ -0,0 +1,27 @@ +//! # Calculate Subquery Results Root and Subquery Hashes Component Circuit +//! +//! This component circuit is responsible for: +//! - Taking in **ordered** list of **ungrouped** subqueries with results and +//! checking the results are all valid with respect to promise commitments of +//! the individual subquery components, which are **grouped** by subquery type. +//! - Computing the subquery hashes of each subquery in the ordered list. + +use axiom_codec::constants::NUM_SUBQUERY_TYPES; + +/// Circuit and Component Implementation. +pub mod circuit; +/// Compute resultsRoot keccak +pub mod results_root; +/// Subquery hash computation handler +pub mod subquery_hash; +/// Contains the logic of joining virtual tables of different subquery types together into one big table. +/// The virtual tables are of different widths: rather than resizing them directly in-circuit, we only compute +/// the RLC of the resized tables, so that the result is a table with the RLCs of the resized tables, all joined together. +pub mod table; +/// Types +pub mod types; + +#[cfg(test)] +pub mod tests; + +pub const ENABLE_ALL_SUBQUERY_TYPES: [bool; NUM_SUBQUERY_TYPES] = [true; NUM_SUBQUERY_TYPES]; diff --git a/axiom-query/src/components/results/results_root.rs b/axiom-query/src/components/results/results_root.rs new file mode 100644 index 00000000..6eefdbe3 --- /dev/null +++ b/axiom-query/src/components/results/results_root.rs @@ -0,0 +1,137 @@ +use axiom_eth::{ + halo2_base::{ + gates::{GateInstructions, RangeChip, RangeInstructions}, + poseidon::hasher::PoseidonHasher, + safe_types::SafeBool, + utils::log2_ceil, + AssignedValue, Context, + }, + utils::circuit_utils::{log2_ceil as circuit_log2_ceil, unsafe_lt_mask}, +}; +use ethers_core::{types::H256, utils::keccak256}; +use itertools::Itertools; + +use crate::utils::codec::{get_num_fe_from_subquery_key, AssignedSubqueryResult}; +use crate::Field; + +/// The zip of `subquery_hashes` and `results` may be resized to some fixed length ordained +/// by the circuit. The true number of subqueries is given by `num_subqueries`, and we +/// want to compute the results root for those subqueries. +/// +/// ## Definitions +/// - `subqueryResultsRoot`: The Keccak Merkle root of the padded tree (pad by bytes32(0)) with +/// even index leaves given by `subqueryHash := keccak(type . subqueryData)` +/// and odd index leaves given by the result `value` (all encoded in bytes). +/// - `subqueryResultsPoseidonRoot`: see [get_results_root_poseidon]. +/// +/// Note: this is _almost_ the same as the Merkle root of the padded tree with leaves +/// given by `keccak(subqueryHash . value)`. The difference is that in the latter we must pad by +/// `keccak(bytes32(0) . bytes32(0))`. +/// +/// ## Assumptions +/// - `num_subqueries <= results.len()` +/// - `subquery_hashes` and `results` are non-empty of the same length. This length is known +/// at compile time. +// Likely `results.len()` will already be a power of 2, so not worth optimizing anything there +pub fn get_results_root( + ctx: &mut Context, + range: &RangeChip, + poseidon: &PoseidonHasher, + results: &[AssignedSubqueryResult], + num_subqueries: AssignedValue, +) -> AssignedValue { + let gate = range.gate(); + let subquery_mask = unsafe_lt_mask(ctx, gate, num_subqueries, results.len()); + + get_results_root_poseidon(ctx, range, poseidon, results, num_subqueries, &subquery_mask) +} + +/// The subquery data and results vector `subquery_results` may be resized to some fixed length +/// ordained by the circuit. The true number of subqueries is given by `num_subqueries`, and we +/// want to compute the Poseidon results root for those subqueries. +/// +/// ## Definition +/// `subqueryResultsPoseidonRoot`: The Poseidon Merkle root of the padded tree (pad by 0) with +/// leaves given by `poseidon(poseidon(type . fieldSubqueryData), value[..])`. +/// +/// ### Note +/// `value` consists of multiple field elements, so the above means +/// `poseidon([[subqueryHashPoseidon], value[..]].concat())` with +/// `subqueryHashPoseidon := poseidon(type . fieldSubqueryData)` and `fieldSubqueryData` is +/// **variable length**. +/// The length of `fieldSubqueryData` is: +/// * the flattened length of the `FieldSubquery**` struct when the subquery type is not SolidityNestedMapping. This can be gotten as `NUM_FE_ANY[subquery_type]`. +/// * When the subquery type is SolidityNestedMapping, the length is `NUM_FE_SOLIDITY_NESTED_MAPPING_WITHOUT_KEYS + 2 * mapping_depth`. In other words, it's the flattened length of `FieldSubquerySolidityNestedMapping` where we resize based on the variable length of the keys. +/// +/// ## Assumptions +/// - `subquery_results` is non-empty and its length is known at compile time. +/// - `num_subqueries <= results.len()` +/// - `subquery_mask[i] = i < num_subqueries ? 1 : 0` +/// - `subquery_hashes` and `results` are non-empty of the same length. This length is known +/// at compile time. +// Likely `results.len()` will already be a power of 2, so not worth optimizing anything there +pub fn get_results_root_poseidon( + ctx: &mut Context, + range: &RangeChip, + initialized_hasher: &PoseidonHasher, + subquery_results: &[AssignedSubqueryResult], + num_subqueries: AssignedValue, + subquery_mask: &[SafeBool], +) -> AssignedValue { + let gate = range.gate(); + + let tree_depth = log2_ceil(subquery_results.len() as u64); + let depth = circuit_log2_ceil(ctx, gate, num_subqueries, tree_depth + 1); + // `depth_indicator = idx_to_indicator(log2ceil(num_subqueries), log2ceil(subquery_results.len()) + 1)` + let depth_indicator = gate.idx_to_indicator(ctx, depth, tree_depth + 1); + + let const_zero = ctx.load_zero(); + + let mut leaves = Vec::with_capacity(1 << tree_depth); + for (subquery_result, &mask) in subquery_results.iter().zip_eq(subquery_mask) { + let key = &subquery_result.key; + let key_len = get_num_fe_from_subquery_key(ctx, gate, key); + let subquery_hash = initialized_hasher.hash_var_len_array(ctx, range, &key.0, key_len); + let concat = [&[subquery_hash], &subquery_result.value[..]].concat(); + let mut leaf = initialized_hasher.hash_fix_len_array(ctx, gate, &concat); + leaf = gate.mul(ctx, leaf, mask); + leaves.push(leaf); + } + leaves.resize(1 << tree_depth, const_zero); + + let mut layers = Vec::with_capacity(tree_depth + 1); + layers.push(leaves); + for i in 0..tree_depth { + let prev_layer = &layers[i]; + let layer = (0..(prev_layer.len() + 1) / 2) + .map(|j| { + initialized_hasher.hash_fix_len_array( + ctx, + gate, + &[prev_layer[2 * j], prev_layer[2 * j + 1]], + ) + }) + .collect(); + layers.push(layer); + } + + // The correct root is layers[log2ceil(num_subqueries)][0] + let root_candidates = layers.iter().map(|layer| layer[0]).collect_vec(); + gate.select_by_indicator(ctx, root_candidates, depth_indicator.to_vec()) +} + +// empty_root[idx] is the Merkle root of a tree of depth idx with bytes32(0)'s as leaves +fn generate_keccak_empty_roots(len: usize) -> Vec { + let mut empty_roots = Vec::with_capacity(len); + let mut root = H256::zero(); + empty_roots.push(root); + for _ in 1..len { + root = H256(keccak256([root.as_bytes(), root.as_bytes()].concat())); + empty_roots.push(root); + } + empty_roots +} + +lazy_static::lazy_static! { + pub static ref KECCAK_EMPTY_ROOTS: Vec = generate_keccak_empty_roots(32); +} diff --git a/axiom-query/src/components/results/subquery_hash.rs b/axiom-query/src/components/results/subquery_hash.rs new file mode 100644 index 00000000..ff698fb5 --- /dev/null +++ b/axiom-query/src/components/results/subquery_hash.rs @@ -0,0 +1,138 @@ +use std::iter::{self, zip}; + +use axiom_codec::{ + constants::{NUM_SUBQUERY_TYPES, SUBQUERY_TYPE_BYTES}, + encoder::field_elements::{ + BYTES_PER_FE_ANY, FIELD_SOLIDITY_NESTED_MAPPING_DEPTH_IDX, + NUM_FE_SOLIDITY_NESTED_MAPPING_MIN, NUM_FE_SOLIDITY_NESTED_MAPPING_WITHOUT_KEYS, + }, + types::native::SubqueryType, +}; +use axiom_eth::{ + halo2_base::{ + gates::{GateInstructions, RangeChip, RangeInstructions}, + safe_types::{SafeTypeChip, VarLenBytesVec}, + Context, + QuantumCell::{Constant, Existing}, + }, + keccak::{types::KeccakVarLenQuery, KeccakChip}, + utils::uint_to_bytes_be, +}; +use itertools::Itertools; + +use crate::utils::codec::AssignedSubqueryKey; +use crate::Field; + +/// Bytes `encoded_subquery_key` includes the subquery type ([u16]) +pub fn get_subquery_hash( + ctx: &mut Context, + keccak: &KeccakChip, + key: &AssignedSubqueryKey, + enabled_types: &[bool; NUM_SUBQUERY_TYPES], +) -> KeccakVarLenQuery { + let range = keccak.range(); + + let encoded = transform_subquery_key_to_bytes(ctx, range, key, enabled_types); + + let mut min_len = BYTES_PER_FE_ANY + .iter() + .enumerate() + .filter(|&(subquery_type, _)| enabled_types[subquery_type]) + .map(|(subquery_type, bytes_per_fe)| { + if subquery_type == SubqueryType::SolidityNestedMapping as usize { + bytes_per_fe[..NUM_FE_SOLIDITY_NESTED_MAPPING_MIN].iter().sum::() + } else { + bytes_per_fe.iter().sum::() + } + }) + .min() + .unwrap_or(0); + min_len += SUBQUERY_TYPE_BYTES; + + let encoded_key = encoded.bytes().iter().map(|b| *b.as_ref()).collect(); + keccak.keccak_var_len(ctx, encoded_key, *encoded.len(), min_len) +} + +/// Transform from field element encoding of subquery key into byte encoding +pub fn transform_subquery_key_to_bytes( + ctx: &mut Context, + range: &RangeChip, + key: &AssignedSubqueryKey, + enabled_types: &[bool; NUM_SUBQUERY_TYPES], +) -> VarLenBytesVec { + let subquery_type = key.0[0]; + // The key WITHOUT the subquery type + let subquery = &key.0[1..]; + + let gate = range.gate(); + let type_indicator = gate.idx_to_indicator(ctx, subquery_type, NUM_SUBQUERY_TYPES); + + // max_bytes_per_fe[i] is the max number of bytes the i-th field element can represent + let max_bytes_per_fe = (0..subquery.len()) + .map(|i| { + BYTES_PER_FE_ANY + .iter() + .enumerate() + .filter(|&(subtype, _)| enabled_types[subtype]) + .map(|(_, bytes_per_fe)| *bytes_per_fe.get(i).unwrap_or(&0)) + .max() + .unwrap_or(0) + }) + .collect_vec(); + let max_total_bytes = max_bytes_per_fe.iter().sum::(); + + let byte_frags = zip(subquery, max_bytes_per_fe) + .map(|(fe, max_bytes)| uint_to_bytes_be(ctx, range, fe, max_bytes)) + .collect_vec(); + + // now I do the very dumb thing: just create the byte array for each type + let const_zero = SafeTypeChip::unsafe_to_byte(ctx.load_zero()); + let (encoded_bytes_by_type, byte_len_by_type): (Vec<_>, Vec<_>) = BYTES_PER_FE_ANY + .iter() + .enumerate() + .map(|(subtype, &bytes_per_fe)| { + let bytes_per_fe = if enabled_types[subtype] { bytes_per_fe } else { &[] }; + // byte frags are big-endian, so take correct number of bytes from the end + let mut encoded = zip(&byte_frags, bytes_per_fe) + .flat_map(|(frag, &num_bytes)| { + frag[frag.len() - num_bytes..frag.len()].iter().copied() + }) + .collect_vec(); + assert!(encoded.len() <= max_total_bytes); + encoded.resize(max_total_bytes, const_zero); + + let byte_len = if !enabled_types[subtype] { + Constant(F::ZERO) + } else if subtype == SubqueryType::SolidityNestedMapping as usize { + // length depends on mapping_depth + let mapping_depth = subquery[FIELD_SOLIDITY_NESTED_MAPPING_DEPTH_IDX]; + let pre_len = bytes_per_fe[..NUM_FE_SOLIDITY_NESTED_MAPPING_WITHOUT_KEYS] + .iter() + .sum::(); + Existing(gate.mul_add( + ctx, + Constant(F::from(32)), + mapping_depth, + Constant(F::from(pre_len as u64)), + )) + } else { + Constant(F::from(bytes_per_fe.iter().sum::() as u64)) + }; + + (encoded, byte_len) + }) + .unzip(); + + let encoded_subquery = + gate.select_array_by_indicator(ctx, &encoded_bytes_by_type, &type_indicator); + let mut encoded_len = gate.select_by_indicator(ctx, byte_len_by_type, type_indicator); + + let encoded_type = uint_to_bytes_be(ctx, range, &subquery_type, SUBQUERY_TYPE_BYTES); + let encoded: Vec<_> = iter::empty() + .chain(encoded_type) + .chain(encoded_subquery.into_iter().map(SafeTypeChip::unsafe_to_byte)) + .collect(); + encoded_len = gate.add(ctx, encoded_len, Constant(F::from(SUBQUERY_TYPE_BYTES as u64))); + + VarLenBytesVec::new(encoded, encoded_len, max_total_bytes + SUBQUERY_TYPE_BYTES) +} diff --git a/axiom-query/src/components/results/table/join.rs b/axiom-query/src/components/results/table/join.rs new file mode 100644 index 00000000..c4f45ffb --- /dev/null +++ b/axiom-query/src/components/results/table/join.rs @@ -0,0 +1,115 @@ +//! We have virtual tables of different widths for different subquery types. +//! We virtually join them together to get a single table. This is virtual because +//! we actually only compute the RLC of the resulting joined table. + +use axiom_codec::{ + constants::{MAX_SUBQUERY_INPUTS, MAX_SUBQUERY_OUTPUTS}, + types::{field_elements::AnySubqueryResult, native::SubqueryType}, +}; +use axiom_eth::{ + halo2_base::{gates::GateInstructions, utils::ScalarField, AssignedValue}, + rlc::{chip::RlcChip, circuit::builder::RlcContextPair}, +}; + +#[derive(Clone, Debug)] +pub struct GroupedSubqueryResults { + /// This is a **constant** + pub subquery_type: SubqueryType, + /// This is a fixed length vector of (subquery, value) pairs. + /// Assumed that subquery[i].len() = subquery[j].len() for all i, j. + /// Assumed that value[i].len() = value[j].len() for all i, j. + pub results: Vec, Vec>>, +} + +impl GroupedSubqueryResults> { + /// The width of the key and value differs per subquery type, but we want to put them + /// into an ungrouped virtual table. Instead of resizing the key/values directly, we + /// optimize by computing the RLC of the resized key/values since that is all we need + /// to form the dynamic lookup table. + // + // Note: this assumes Solidity Nested Mapping subquery enforces that extra keys + // beyond mapping depth are 0 + pub fn into_rlc( + self, + (ctx_gate, ctx_rlc): RlcContextPair, + gate: &impl GateInstructions, + rlc: &RlcChip, + ) -> Vec<[AssignedValue; 1]> { + if self.results.is_empty() { + return vec![]; + } + let subquery_type = ctx_gate.load_constant(F::from(self.subquery_type as u64)); + let subquery_len = self.results[0].subquery.len(); + assert!(subquery_len <= MAX_SUBQUERY_INPUTS); + let val_len = self.results[0].value.len(); + assert!(val_len <= MAX_SUBQUERY_OUTPUTS); + // right now `value` is always HiLo, but we put this here in case of future generalizations + let val_multiplier = (val_len < MAX_SUBQUERY_OUTPUTS) + .then(|| rlc.rlc_pow_fixed(ctx_gate, gate, MAX_SUBQUERY_OUTPUTS - val_len)); + let subquery_multiplier = rlc.rlc_pow_fixed( + ctx_gate, + gate, + MAX_SUBQUERY_INPUTS + MAX_SUBQUERY_OUTPUTS - subquery_len, + ); + + self.results + .into_iter() + .map(|result| { + let subquery = result.subquery; + let value = result.value; + assert_eq!(subquery.len(), subquery_len); + assert_eq!(value.len(), val_len); + // the following has the same effect as: `RLC(value.resize(MAX_SUBQUERY_OUTPUTS, 0))` + let mut val_rlc = rlc.compute_rlc_fixed_len(ctx_rlc, value.clone()).rlc_val; + if let Some(multiplier) = val_multiplier { + val_rlc = gate.mul(ctx_gate, val_rlc, multiplier); + } + // key = subquery_type . subquery + let key = [vec![subquery_type], subquery].concat(); + // the following has the same effect as: `RLC([key.resize(SUBQUERY_KEY_LEN, 0)), value])` + // where `value` is already resized + // This is because RLC(a . b) = RLC(a) * gamma^|b| + RLC(b) + // if a, b have fixed lengths |a|, |b| respectively + let key_rlc = rlc.compute_rlc_fixed_len(ctx_rlc, key).rlc_val; + [gate.mul_add(ctx_gate, key_rlc, subquery_multiplier, val_rlc)] + }) + .collect() + } +} + +/* +// Unused for now + +/// `inputs` has fixed length known at compile time. +/// The true variable length of `inputs` is given by `var_len`, and +/// it is assumed that `min_len <= var_len <= inputs.len()`. +/// +/// Let `var_input = inputs[..var_len]`. This function has the effect of: +/// ```ignore +/// var_input.resize(0, padded_len) +/// compute_rlc_fixed(var_input) +/// ``` +/// What the function actually does is compute the RLC of `var_input` and multiply by +/// the correct power of the challenge `gamma`. +/// +/// The output is the RLC value. +/// +/// ## Assumptions +/// - `min_len <= var_len <= inputs.len()` +fn compute_rlc_right_pad_to_fixed( + (ctx_gate, ctx_rlc): RlcContextPair, + gate: &impl GateInstructions, + rlc: &RlcChip, + inputs: Vec>, + var_len: AssignedValue, + min_len: usize, + padded_len: usize, +) -> AssignedValue { + assert!(min_len <= inputs.len()); + let rlc_var = rlc.compute_rlc_with_min_len((ctx_gate, ctx_rlc), gate, inputs, var_len, min_len); + let shift_len = gate.sub(ctx_gate, Constant(F::from(padded_len as u64)), var_len); + let multiplier = + rlc.rlc_pow(ctx_gate, gate, shift_len, bit_length((padded_len - min_len) as u64)); + gate.mul(ctx_gate, rlc_var.rlc_val, multiplier) +} +*/ diff --git a/axiom-query/src/components/results/table/mod.rs b/axiom-query/src/components/results/table/mod.rs new file mode 100644 index 00000000..39ab9aae --- /dev/null +++ b/axiom-query/src/components/results/table/mod.rs @@ -0,0 +1,54 @@ +use axiom_codec::types::field_elements::FlattenedSubqueryResult; +use axiom_eth::{ + halo2_base::{AssignedValue, Context}, + rlc::chip::RlcChip, +}; +use serde::{Deserialize, Serialize}; + +use crate::{utils::codec::assign_flattened_subquery_result, Field}; + +pub(super) mod join; + +/// The _ordered_ subqueries with results. +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct SubqueryResultsTable { + pub rows: Vec>, +} + +impl SubqueryResultsTable { + pub fn new(rows: Vec>) -> Self { + Self { rows } + } +} + +pub type AssignedSubqueryResultsTable = SubqueryResultsTable>; + +impl SubqueryResultsTable { + /// Nothing is constrained. Loaded as pure private witnesses. + pub fn assign(&self, ctx: &mut Context) -> AssignedSubqueryResultsTable { + let rows: Vec<_> = + self.rows.iter().map(|row| assign_flattened_subquery_result(ctx, row)).collect(); + SubqueryResultsTable { rows } + } + + pub fn len(&self) -> usize { + self.rows.len() + } + + pub fn is_empty(&self) -> bool { + self.rows.is_empty() + } +} + +impl AssignedSubqueryResultsTable { + pub fn to_rlc(&self, ctx_rlc: &mut Context, rlc: &RlcChip) -> Vec<[AssignedValue; 1]> { + self.rows + .iter() + .map(|row| { + let concat = row.to_fixed_array(); + let trace = rlc.compute_rlc_fixed_len(ctx_rlc, concat); + [trace.rlc_val] + }) + .collect() + } +} diff --git a/axiom-query/src/components/results/tests.rs b/axiom-query/src/components/results/tests.rs new file mode 100644 index 00000000..bb0271da --- /dev/null +++ b/axiom-query/src/components/results/tests.rs @@ -0,0 +1,458 @@ +use std::{collections::HashMap, fs::File, str::FromStr}; + +use anyhow::Result; +use axiom_codec::{ + constants::NUM_SUBQUERY_TYPES, + encoder::field_elements::{NUM_FE_ANY, NUM_FE_SOLIDITY_NESTED_MAPPING_WITHOUT_KEYS}, + special_values::{ + RECEIPT_LOG_IDX_OFFSET, RECEIPT_TOPIC_IDX_OFFSET, TX_FUNCTION_SELECTOR_FIELD_IDX, + }, + types::{ + field_elements::{AnySubqueryResult, FlattenedSubqueryResult}, + native::{ + AccountSubquery, AnySubquery, HeaderSubquery, ReceiptSubquery, + SolidityNestedMappingSubquery, StorageSubquery, Subquery, SubqueryResult, SubqueryType, + TxSubquery, + }, + }, + utils::native::u256_to_h256, +}; +use axiom_eth::{ + block_header::{EXTRA_DATA_INDEX, RECEIPT_ROOT_INDEX, STATE_ROOT_INDEX, TX_ROOT_INDEX}, + halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr}, + keccak::{promise::generate_keccak_shards_from_calls, types::ComponentTypeKeccak}, + utils::build_utils::pinning::{CircuitPinningInstructions, Halo2CircuitPinning}, + utils::{ + component::{ + promise_loader::{ + comp_loader::SingleComponentLoaderParams, + multi::{ComponentTypeList, MultiPromiseLoaderParams}, + single::PromiseLoaderParams, + }, + utils::compute_poseidon, + ComponentCircuit, ComponentPromiseResultsInMerkle, ComponentType, + GroupedPromiseResults, + }, + encode_h256_to_hilo, + }, +}; +use ethers_core::types::{Address, Bytes, H256, U256}; +use hex::FromHex; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::{ + components::{ + dummy_rlc_circuit_params, + results::{ + circuit::SubqueryDependencies, table::SubqueryResultsTable, + types::CircuitInputResultsRootShard, + }, + subqueries::{ + account::{types::ComponentTypeAccountSubquery, STORAGE_ROOT_INDEX}, + block_header::types::ComponentTypeHeaderSubquery, + common::{shard_into_component_promise_results, OutputSubqueryShard}, + receipt::types::ComponentTypeReceiptSubquery, + solidity_mappings::types::ComponentTypeSolidityNestedMappingSubquery, + storage::types::ComponentTypeStorageSubquery, + transaction::types::ComponentTypeTxSubquery, + }, + }, + Field, +}; + +use super::{ + circuit::{ComponentCircuitResultsRoot, CoreParamsResultRoot}, + types::{ + component_type_id_to_subquery_type, LogicOutputResultsRoot, + LogicalPublicInstanceResultsRoot, + }, +}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ComponentCapacities { + pub total: usize, + pub header: usize, + pub account: usize, + pub storage: usize, + pub tx: usize, + pub receipt: usize, + pub solidity_mapping: usize, + pub keccak: usize, +} +impl ComponentCapacities { + pub fn to_str_short(&self) -> String { + format!( + "{}_{}_{}_{}_{}_{}_{}_{}", + self.total, + self.header, + self.account, + self.storage, + self.tx, + self.receipt, + self.solidity_mapping, + self.keccak + ) + } +} + +pub fn get_test_input( + capacity: ComponentCapacities, +) -> Result<(CircuitInputResultsRootShard, LogicOutputResultsRoot, GroupedPromiseResults)> { + let mut extra_data = Vec::from_hex("43727578706f6f6c205050532f6e696365686173682d31").unwrap(); + extra_data.resize(32, 0u8); + let block_number = 9528813; + let mut header_subqueries = vec![ + ( + HeaderSubquery { block_number, field_idx: 1 }, + H256::from_str("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347")?, + ), + ( + HeaderSubquery { block_number, field_idx: 71 }, + H256::from_str("0x2a4a0345b08e0c50805158030213038e9205244c5100b7223068018ef1648009")?, + ), + ( + HeaderSubquery { block_number, field_idx: EXTRA_DATA_INDEX as u32 }, + H256::from_slice(&extra_data), + ), + ( + HeaderSubquery { block_number, field_idx: STATE_ROOT_INDEX as u32 }, + H256::from_str("0x9ce4561dfadb4fb022debc7c013141a36a23d467f4268c0e620a7bba05b174f8")?, + ), + ( + HeaderSubquery { block_number, field_idx: TX_ROOT_INDEX as u32 }, + H256::from_str("0xec4c8ec02281c196e3f882b4061c05f4f0843a0eaf72a3ee4715077220934bbc")?, + ), + ( + HeaderSubquery { block_number, field_idx: RECEIPT_ROOT_INDEX as u32 }, + H256::from_str("0x0dfecab5d97a4da361b92c29157cd6a5089b55facee007eea16005bfa8d82694")?, + ), + ]; + + let addr = Address::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7")?; + let mut acct_subqueries = vec![( + AccountSubquery { block_number, addr, field_idx: STORAGE_ROOT_INDEX as u32 }, + H256::from_str("0x3bddd93ffcce5169c2ce5d08b91f9fda3ed657dbf3b78beffb44dc833f6308a2")?, + )]; + + let mut storage_subqueries = vec![( + StorageSubquery { + block_number, + addr, + slot: U256::from_str( + "0xac33ff75c19e70fe83507db0d683fd3465c996598dc972688b7ace676c89077b", + )?, + }, + u256_to_h256(&U256::from_str("0xb532b80")?), + )]; + + let mut solidity_mapping_subqueries = vec![( + SolidityNestedMappingSubquery { + block_number, + addr, + mapping_slot: U256::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000002", + )?, + mapping_depth: 1, + keys: vec![H256::zero()], + }, // balances https://evm.storage/eth/9528813/0xdac17f958d2ee523a2206206994597c13d831ec7/balances#map + u256_to_h256(&U256::from_str("0xb532b80")?), + )]; + + let mut tx_subqueries = vec![( + TxSubquery { + block_number, + tx_idx: 68, + field_or_calldata_idx: TX_FUNCTION_SELECTOR_FIELD_IDX as u32, + }, // https://etherscan.io/tx/0x9524b67377f7ff228fbe31c7edbfb4ba7bb374ceeac54030793b6727d1dc4505 + u256_to_h256(&U256::from_str("0x013efd8b")?), + )]; + + let mut receipt_subqueries = vec![( + ReceiptSubquery { + block_number, + tx_idx: 68, // https://etherscan.io/tx/0x9524b67377f7ff228fbe31c7edbfb4ba7bb374ceeac54030793b6727d1dc4505#eventlog + field_or_log_idx: RECEIPT_LOG_IDX_OFFSET as u32 + 2, + topic_or_data_or_address_idx: RECEIPT_TOPIC_IDX_OFFSET as u32 + 1, + event_schema: H256::from_str( + "0x7f4091b46c33e918a0f3aa42307641d17bb67029427a5369e54b353984238705", + )?, + }, + H256::from(Address::from_str("0x263FC4d9Eb6da1ED296ea6d189b41e546a188D8A")?), + )]; + header_subqueries.truncate(capacity.header); + acct_subqueries.truncate(capacity.account); + storage_subqueries.truncate(capacity.storage); + solidity_mapping_subqueries.truncate(capacity.solidity_mapping); + tx_subqueries.truncate(capacity.tx); + receipt_subqueries.truncate(capacity.receipt); + + fn append( + results: &mut Vec, + subqueries: &[(impl Into + Clone, H256)], + ) { + for (s, v) in subqueries { + results.push(SubqueryResult { subquery: s.clone().into(), value: v.0.into() }) + } + } + fn resize_with_first(v: &mut Vec, cap: usize) { + if cap > 0 { + v.resize(cap, v[0].clone()); + } else { + v.clear(); + } + } + let mut results = vec![]; + // put them in weird order just to test + append(&mut results, &tx_subqueries); + append(&mut results, &header_subqueries); + append(&mut results, &receipt_subqueries); + append(&mut results, &acct_subqueries); + append(&mut results, &storage_subqueries); + append(&mut results, &solidity_mapping_subqueries); + results.truncate(capacity.total); + let num_subqueries = results.len(); + resize_with_first(&mut results, capacity.total); + let _encoded_subqueries: Vec = + results.iter().map(|r| r.subquery.encode().into()).collect(); + let subquery_hashes: Vec = results.iter().map(|r| r.subquery.keccak()).collect(); + + fn prepare(results: Vec<(A, H256)>) -> OutputSubqueryShard { + let results = results.into_iter().map(|(s, v)| AnySubqueryResult::new(s, v)).collect_vec(); + OutputSubqueryShard { results } + } + resize_with_first(&mut header_subqueries, capacity.header); + resize_with_first(&mut acct_subqueries, capacity.account); + resize_with_first(&mut storage_subqueries, capacity.storage); + resize_with_first(&mut tx_subqueries, capacity.tx); + resize_with_first(&mut receipt_subqueries, capacity.receipt); + resize_with_first(&mut solidity_mapping_subqueries, capacity.solidity_mapping); + + let promise_header = prepare(header_subqueries); + let promise_account = prepare(acct_subqueries); + let promise_storage = prepare(storage_subqueries); + let promise_tx = prepare(tx_subqueries); + let promise_receipt = prepare(receipt_subqueries); + let promise_solidity_mapping = prepare(solidity_mapping_subqueries); + + let mut promise_results = HashMap::new(); + for (type_id, pr) in SubqueryDependencies::::get_component_type_ids().into_iter().zip_eq([ + shard_into_component_promise_results::>( + promise_header.convert_into(), + ), + shard_into_component_promise_results::>( + promise_account.convert_into(), + ), + shard_into_component_promise_results::>( + promise_storage.convert_into(), + ), + shard_into_component_promise_results::>( + promise_tx.convert_into(), + ), + shard_into_component_promise_results::>( + promise_receipt.convert_into(), + ), + shard_into_component_promise_results::>( + promise_solidity_mapping.convert_into(), + ), + ]) { + // filter out empty shards with capacity = 0. + if !pr.shards()[0].1.is_empty() { + promise_results.insert(type_id, pr); + } + } + + Ok(( + CircuitInputResultsRootShard:: { + subqueries: SubqueryResultsTable::::new( + results.clone().into_iter().map(|r| r.try_into().unwrap()).collect_vec(), + ), + num_subqueries: F::from(num_subqueries as u64), + }, + LogicOutputResultsRoot { results, subquery_hashes, num_subqueries }, + promise_results, + )) +} + +pub const fn test_capacity() -> ComponentCapacities { + ComponentCapacities { + total: 32, + header: 32, + account: 8, + storage: 8, + tx: 8, + receipt: 8, + solidity_mapping: 8, + keccak: 500, + } +} + +#[test] +fn test_mock_results_root() -> anyhow::Result<()> { + let k = 18; + let capacity = test_capacity(); + let (input, subquery_results, mut promise_results) = get_test_input(capacity)?; + + let mut enabled_types = [false; NUM_SUBQUERY_TYPES]; + let mut params_per_comp = HashMap::new(); + // reminder: input.promises order is deterministic, from ComponentTypeResultsRoot::subquery_type_ids() + for (component_type_id, results) in &promise_results { + if component_type_id == &ComponentTypeKeccak::::get_type_id() { + continue; + } + let subquery_type = component_type_id_to_subquery_type::(component_type_id).unwrap(); + enabled_types[subquery_type as usize] = true; + + // TODO: Shard capacity shoud come from .get_capactiy. + params_per_comp.insert( + component_type_id.clone(), + SingleComponentLoaderParams::new(0, vec![results.shards()[0].1.len()]), + ); + } + let promise_results_params = MultiPromiseLoaderParams { params_per_component: params_per_comp }; + let keccak_f_capacity = capacity.keccak; + + let circuit_params = dummy_rlc_circuit_params(k as usize); + let mut circuit = ComponentCircuitResultsRoot::new( + CoreParamsResultRoot { enabled_types, capacity: input.subqueries.len() }, + (PromiseLoaderParams::new_for_one_shard(keccak_f_capacity), promise_results_params), + circuit_params, + ); + circuit.feed_input(Box::new(input)).unwrap(); + circuit.calculate_params(); + promise_results.insert( + ComponentTypeKeccak::::get_type_id(), + ComponentPromiseResultsInMerkle::from_single_shard( + generate_keccak_shards_from_calls(&circuit, keccak_f_capacity) + .unwrap() + .into_logical_results(), + ), + ); + circuit.fulfill_promise_results(&promise_results).unwrap(); + let pis = circuit.get_public_instances(); + let LogicalPublicInstanceResultsRoot { results_root_poseidon, commit_subquery_hashes } = + pis.other.clone().try_into().unwrap(); + assert_eq!( + (results_root_poseidon, commit_subquery_hashes), + get_native_results_root_and_subquery_hash_commit(subquery_results.clone()) + ); + let instances: Vec = pis.into(); + MockProver::run(k as u32, &circuit, vec![instances]).unwrap().assert_satisfied(); + circuit.pinning().write("configs/test/results_root.json").unwrap(); + + Ok(()) +} + +/// Returns (poseidon_results_root, subquery_hash_commit) +// `subqueryResultsPoseidonRoot`: The Poseidon Merkle root of the padded tree (pad by 0) with +// leaves given by `poseidon(poseidon(type . fieldSubqueryData), value[..])`. +// +// ### Note +// `value` consists of multiple field elements, so the above means +// `poseidon([[subqueryHashPoseidon], value[..]].concat())` with +// `subqueryHashPoseidon := poseidon(type . fieldSubqueryData)` and `fieldSubqueryData` is +// **variable length**. +fn get_native_results_root_and_subquery_hash_commit(results: LogicOutputResultsRoot) -> (Fr, Fr) { + // note compute_poseidon re-creates poseidon spec, but it doesn't matter for test + let mut leaves = vec![]; + let mut to_commit = vec![]; + for result in results.results.into_iter().take(results.num_subqueries) { + to_commit.extend(encode_h256_to_hilo::(&result.subquery.keccak()).hi_lo()); + // NUM_FE_ANY[subquery_type] is the length of fieldSubqueryData + let unencoded_key_len = + get_num_fe_from_subquery(result.subquery.clone().try_into().unwrap()); + // add 1 for subquery type + let encoded_key_len = unencoded_key_len + 1; + let result = FlattenedSubqueryResult::::try_from(result).unwrap(); + let key_hash = compute_poseidon(&result.key[..encoded_key_len]); + let mut buf = vec![key_hash]; + buf.extend(result.value.0); + leaves.push(compute_poseidon(&buf)); + } + leaves.resize(leaves.len().next_power_of_two(), Fr::zero()); + while leaves.len() > 1 { + leaves = leaves.chunks(2).map(compute_poseidon).collect(); + } + (leaves[0], compute_poseidon(&to_commit)) +} + +fn get_num_fe_from_subquery(subquery: AnySubquery) -> usize { + match subquery { + AnySubquery::SolidityNestedMapping(subquery) => { + NUM_FE_SOLIDITY_NESTED_MAPPING_WITHOUT_KEYS + 2 * subquery.mapping_depth as usize + } + _ => { + let subquery_type = Subquery::from(subquery).subquery_type; + NUM_FE_ANY[subquery_type as usize] + } + } +} + +#[test] +#[ignore = "integration test"] +fn test_mock_results_root_header_only_for_agg() -> anyhow::Result<()> { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + + let k = 18; + let capacity = ComponentCapacities { + total: 3, + header: 3, + account: 0, + storage: 0, + tx: 0, + receipt: 0, + solidity_mapping: 0, + keccak: 200, + }; + let (input, output, mut promise_results) = get_test_input(capacity)?; + + let mut enabled_types = [false; NUM_SUBQUERY_TYPES]; + enabled_types[SubqueryType::Header as usize] = true; + let mut params_per_comp = HashMap::new(); + + params_per_comp.insert( + ComponentTypeHeaderSubquery::::get_type_id(), + SingleComponentLoaderParams::new(0, vec![3]), + ); + let promise_results_params = MultiPromiseLoaderParams { params_per_component: params_per_comp }; + + let keccak_f_capacity = capacity.keccak; + + let circuit_params = dummy_rlc_circuit_params(k as usize); + let mut circuit = ComponentCircuitResultsRoot::new( + CoreParamsResultRoot { enabled_types, capacity: input.subqueries.len() }, + (PromiseLoaderParams::new_for_one_shard(keccak_f_capacity), promise_results_params), + circuit_params, + ); + circuit.feed_input(Box::new(input.clone())).unwrap(); + circuit.calculate_params(); + + let keccak_shard = generate_keccak_shards_from_calls(&circuit, keccak_f_capacity)?; + serde_json::to_writer( + File::create(format!( + "{cargo_manifest_dir}/data/test/results_root_promise_results_keccak_for_agg.json" + ))?, + &keccak_shard, + )?; + promise_results.insert( + ComponentTypeKeccak::::get_type_id(), + ComponentPromiseResultsInMerkle::from_single_shard(keccak_shard.into_logical_results()), + ); + circuit.fulfill_promise_results(&promise_results).unwrap(); + let instances: Vec = circuit.get_public_instances().into(); + MockProver::run(k as u32, &circuit, vec![instances]).unwrap().assert_satisfied(); + + serde_json::to_writer( + File::create(format!("{cargo_manifest_dir}/data/test/input_result_root_for_agg.json"))?, + &input, + )?; + serde_json::to_writer( + File::create(format!("{cargo_manifest_dir}/data/test/output_result_root_for_agg.json"))?, + &output, + )?; + + circuit.pinning().write("configs/test/results_root_for_agg.json").unwrap(); + + Ok(()) +} diff --git a/axiom-query/src/components/results/types.rs b/axiom-query/src/components/results/types.rs new file mode 100644 index 00000000..7f5643ae --- /dev/null +++ b/axiom-query/src/components/results/types.rs @@ -0,0 +1,273 @@ +use std::{any::Any, marker::PhantomData}; + +use anyhow::Result; +use axiom_codec::{ + types::{ + field_elements::{AnySubqueryResult, FlattenedSubqueryResult}, + native::{SubqueryResult, SubqueryType}, + }, + HiLo, +}; +use axiom_eth::{ + halo2_base::{ + gates::{GateInstructions, RangeChip}, + AssignedValue, + }, + impl_flatten_conversion, + rlc::{chip::RlcChip, circuit::builder::RlcContextPair}, + utils::{ + build_utils::dummy::DummyFrom, + component::{ + circuit::CoreBuilderInput, + promise_loader::{ + flatten_witness_to_rlc, + multi::{ComponentTypeList, RlcAdapter}, + }, + types::{Flatten, LogicalEmpty}, + utils::into_key, + ComponentType, ComponentTypeId, LogicalResult, PromiseCallWitness, + TypelessLogicalInput, + }, + encode_h256_to_hilo, + }, +}; +use ethers_core::types::H256; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::{components::results::circuit::SubqueryDependencies, Field, RawField}; + +use super::{ + circuit::CoreParamsResultRoot, + table::{join::GroupedSubqueryResults, SubqueryResultsTable}, +}; + +/// Component type for ResultsRoot component. +pub struct ComponentTypeResultsRoot(PhantomData); + +/// Logic inputs to Data Results and Calculate Subquery Hashes Circuit +/// +/// Length of `data_query` fixed at compile time. True number of subqueries is `num_subqueries`. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CircuitInputResultsRootShard { + /// The **ordered** subqueries to be verified, _with_ claimed result values. + /// This table may have been resized to some fixed length, known at compile time. + /// The actual subqueries will be the first `num_subqueries` rows of the table. + pub subqueries: SubqueryResultsTable, + /// The number of true subqueries in the table. + pub num_subqueries: F, +} + +impl DummyFrom for CircuitInputResultsRootShard { + fn dummy_from(core_params: CoreParamsResultRoot) -> Self { + let subqueries = SubqueryResultsTable { + rows: vec![FlattenedSubqueryResult::default(); core_params.capacity], + }; + Self { subqueries, num_subqueries: F::ZERO } + } +} + +/// Lengths of `results` and `subquery_hashes` are always equal. +/// May include padding - `num_subqueries` is the actual number of subqueries. +#[derive(Clone, Debug, Default, Serialize, Deserialize, Hash)] +#[serde(rename_all = "camelCase")] +pub struct LogicOutputResultsRoot { + pub results: Vec, + pub subquery_hashes: Vec, + pub num_subqueries: usize, +} + +/// Lengths of `results` and `subquery_hashes` must be equal. +/// +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct CircuitOutputResultsRoot { + pub results: SubqueryResultsTable, + pub subquery_hashes: Vec>, + pub num_subqueries: usize, +} + +impl CircuitOutputResultsRoot { + /// Resize itself to `new_len` by repeating the first row. Crash if the table is empty. + pub fn resize_with_first(&mut self, new_len: usize) { + self.results.rows.resize(new_len, self.results.rows[0]); + self.subquery_hashes.resize(new_len, self.subquery_hashes[0]); + } +} + +// ==== Public Instances ==== +// 9999 means that the public instance takes a whole witness. +const NUM_BITS_PER_FE: [usize; 2] = [9999, 9999]; +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct LogicalPublicInstanceResultsRoot { + pub results_root_poseidon: T, + pub commit_subquery_hashes: T, +} + +// All promise calls made by ResultsRoot component are Virtual: they will be managed by MultiPromiseLoader +// The ResultsRoot component should never to called directly, so it has no virtual table as output. +impl ComponentType for ComponentTypeResultsRoot { + type InputValue = LogicalEmpty; + type InputWitness = LogicalEmpty>; + type OutputValue = LogicalEmpty; + type OutputWitness = LogicalEmpty>; + type LogicalInput = LogicalEmpty; + + fn get_type_id() -> ComponentTypeId { + "axiom-query:ComponentTypeResultsRoot".to_string() + } + + fn logical_result_to_virtual_rows_impl( + _ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + unreachable!() + } + fn logical_input_to_virtual_rows_impl(_li: &Self::LogicalInput) -> Vec { + unreachable!() + } +} + +impl LogicOutputResultsRoot { + pub fn new( + results: Vec, + subquery_hashes: Vec, + num_subqueries: usize, + ) -> Self { + assert_eq!(results.len(), subquery_hashes.len()); + Self { results, subquery_hashes, num_subqueries } + } +} + +impl TryFrom for CircuitOutputResultsRoot { + type Error = std::io::Error; + fn try_from(output: LogicOutputResultsRoot) -> Result { + let LogicOutputResultsRoot { results, subquery_hashes, num_subqueries } = output; + let rows = results + .into_iter() + .map(FlattenedSubqueryResult::::try_from) + .collect::, _>>()?; + let results = SubqueryResultsTable { rows }; + let subquery_hashes = subquery_hashes + .into_iter() + .map(|subquery_hash| encode_h256_to_hilo(&subquery_hash)) + .collect(); + Ok(Self { results, subquery_hashes, num_subqueries }) + } +} + +// ============== LogicalPublicInstanceResultsRoot ============== +impl TryFrom> for LogicalPublicInstanceResultsRoot { + type Error = anyhow::Error; + + fn try_from(value: Vec) -> anyhow::Result { + let [results_root_poseidon, commit_subquery_hashes] = + value.try_into().map_err(|_| anyhow::anyhow!("invalid length"))?; + Ok(Self { results_root_poseidon, commit_subquery_hashes }) + } +} +impl LogicalPublicInstanceResultsRoot { + pub fn flatten(&self) -> [T; 2] { + [self.results_root_poseidon, self.commit_subquery_hashes] + } +} +impl_flatten_conversion!(LogicalPublicInstanceResultsRoot, NUM_BITS_PER_FE); + +// ======== Types for component implementation ========== + +/// RLC adapter for MultiPromiseLoader of results root component. +pub struct RlcAdapterResultsRoot(PhantomData); +impl RlcAdapter for RlcAdapterResultsRoot { + fn to_rlc( + ctx_pair: RlcContextPair, + gate: &impl GateInstructions, + rlc: &RlcChip, + component_type_id: &ComponentTypeId, + io_pairs: &[(Flatten>, Flatten>)], + ) -> Vec> { + let subquery_type = component_type_id_to_subquery_type::(component_type_id).unwrap(); + + let subqueries = GroupedSubqueryResults { + subquery_type, + results: io_pairs + .iter() + .map(|(i, o)| AnySubqueryResult { + subquery: i.fields.clone(), + value: o.fields.clone(), + }) + .collect_vec(), + }; + subqueries.into_rlc(ctx_pair, gate, rlc).concat() + } +} + +/// Virtual component type for MultiPromiseLoader +pub struct VirtualComponentType(PhantomData); + +impl ComponentType for VirtualComponentType { + type InputValue = FlattenedSubqueryResult; + type InputWitness = FlattenedSubqueryResult>; + type OutputValue = LogicalEmpty; + type OutputWitness = LogicalEmpty>; + type LogicalInput = LogicalEmpty; + + fn get_type_id() -> ComponentTypeId { + "axiom-query:VirtualComponentTypeResultsRoot".to_string() + } + + fn logical_result_to_virtual_rows_impl( + _ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + unreachable!() + } + fn logical_input_to_virtual_rows_impl(_li: &Self::LogicalInput) -> Vec { + unreachable!() + } +} + +#[derive(Clone, Copy, Debug)] +pub struct SubqueryResultCall(pub FlattenedSubqueryResult>); + +impl PromiseCallWitness for SubqueryResultCall { + fn get_component_type_id(&self) -> ComponentTypeId { + VirtualComponentType::::get_type_id() + } + fn get_capacity(&self) -> usize { + // virtual call so no consumption + 0 + } + fn to_rlc( + &self, + (_, rlc_ctx): RlcContextPair, + _range_chip: &RangeChip, + rlc_chip: &RlcChip, + ) -> AssignedValue { + flatten_witness_to_rlc(rlc_ctx, rlc_chip, &self.0.into()) + } + fn to_typeless_logical_input(&self) -> TypelessLogicalInput { + let f_a: Flatten> = self.0.into(); + let f_v: Flatten = f_a.into(); + let l_v: FlattenedSubqueryResult = f_v.try_into().unwrap(); + into_key(l_v) + } + fn get_mock_output(&self) -> Flatten { + let output_val: as ComponentType>::OutputValue = + Default::default(); + output_val.into() + } + fn as_any(&self) -> &dyn Any { + self + } +} + +// ======== Boilerplate conversions ======== + +/// Mapping from component type id to subquery type. +pub fn component_type_id_to_subquery_type( + type_id: &ComponentTypeId, +) -> Option { + // This cannot be static because of + let type_ids = SubqueryDependencies::::get_component_type_ids(); + type_ids + .iter() + .position(|id| id == type_id) + .map(|i| SubqueryType::try_from(i as u16 + 1).unwrap()) +} diff --git a/axiom-query/src/components/subqueries/account/circuit.rs b/axiom-query/src/components/subqueries/account/circuit.rs new file mode 100644 index 00000000..785947ab --- /dev/null +++ b/axiom-query/src/components/subqueries/account/circuit.rs @@ -0,0 +1,298 @@ +use std::iter::zip; + +use axiom_eth::{ + block_header::STATE_ROOT_INDEX, + halo2_base::{ + gates::{flex_gate::threads::parallelize_core, GateInstructions, RangeInstructions}, + safe_types::SafeTypeChip, + AssignedValue, Context, + QuantumCell::Constant, + }, + halo2_proofs::plonk::ConstraintSystem, + keccak::{types::ComponentTypeKeccak, KeccakChip}, + mpt::MPTChip, + rlc::circuit::builder::RlcCircuitBuilder, + rlc::circuit::builder::RlcContextPair, + rlp::RlpChip, + storage::{ + EthAccountWitness, EthStorageChip, ACCOUNT_STATE_FIELDS_MAX_BYTES, + ACCOUNT_STATE_FIELD_IS_VAR_LEN, NUM_ACCOUNT_STATE_FIELDS, + }, + utils::{ + build_utils::aggregation::CircuitMetadata, + bytes_be_to_uint, + circuit_utils::bytes::{pack_bytes_to_hilo, unsafe_mpt_root_to_hi_lo}, + component::{ + circuit::{ + ComponentBuilder, ComponentCircuitImpl, CoreBuilder, CoreBuilderOutput, + CoreBuilderOutputParams, CoreBuilderParams, + }, + promise_collector::PromiseCaller, + promise_loader::{combo::PromiseBuilderCombo, single::PromiseLoader}, + types::LogicalEmpty, + utils::create_hasher, + LogicalResult, + }, + constrain_vec_equal, encode_h256_to_hilo, + hilo::HiLo, + unsafe_bytes_to_assigned, + }, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::{ + components::subqueries::{ + block_header::types::{ComponentTypeHeaderSubquery, FieldHeaderSubqueryCall}, + common::{extract_logical_results, extract_virtual_table}, + }, + utils::codec::{ + AssignedAccountSubquery, AssignedAccountSubqueryResult, AssignedHeaderSubquery, + }, + Field, +}; + +use super::{ + types::{CircuitInputAccountShard, CircuitInputAccountSubquery, ComponentTypeAccountSubquery}, + KECCAK_RLP_EMPTY_STRING, STORAGE_ROOT_INDEX, +}; + +pub struct CoreBuilderAccountSubquery { + input: Option>, + params: CoreParamsAccountSubquery, + payload: Option<(KeccakChip, Vec>)>, +} + +/// Specify the output format of AccountSubquery component. +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct CoreParamsAccountSubquery { + /// The maximum number of subqueries of this type allowed in a single circuit. + pub capacity: usize, + /// The maximum depth of the state MPT trie supported by this circuit. + /// The depth is defined as the maximum length of an account proof, where the account proof always ends in a terminal leaf node. + /// + /// In production this will be set to 14 based on the MPT analysis from https://hackmd.io/@axiom/BJBledudT + pub max_trie_depth: usize, +} +impl CoreBuilderParams for CoreParamsAccountSubquery { + fn get_output_params(&self) -> CoreBuilderOutputParams { + CoreBuilderOutputParams::new(vec![self.capacity]) + } +} + +type CKeccak = ComponentTypeKeccak; +type CHeader = ComponentTypeHeaderSubquery; +pub type PromiseLoaderAccountSubquery = + PromiseBuilderCombo>, PromiseLoader>>; +pub type ComponentCircuitAccountSubquery = + ComponentCircuitImpl, PromiseLoaderAccountSubquery>; + +impl CircuitMetadata for CoreBuilderAccountSubquery { + const HAS_ACCUMULATOR: bool = false; + fn num_instance(&self) -> Vec { + unreachable!() + } +} + +impl ComponentBuilder for CoreBuilderAccountSubquery { + type Params = CoreParamsAccountSubquery; + fn new(params: Self::Params) -> Self { + Self { input: None, params, payload: None } + } + fn get_params(&self) -> Self::Params { + self.params.clone() + } + fn clear_witnesses(&mut self) { + self.payload = None; + } + fn calculate_params(&mut self) -> Self::Params { + self.params.clone() + } + fn configure_with_params(_: &mut ConstraintSystem, _: Self::Params) {} +} + +impl CoreBuilder for CoreBuilderAccountSubquery { + type CompType = ComponentTypeAccountSubquery; + type PublicInstanceValue = LogicalEmpty; + type PublicInstanceWitness = LogicalEmpty>; + type CoreInput = CircuitInputAccountShard; + + fn feed_input(&mut self, input: Self::CoreInput) -> anyhow::Result<()> { + for r in &input.requests { + if r.proof.acct_pf.max_depth != self.params.max_trie_depth { + anyhow::bail!("AccountSubquery: request MPT max depth {} does not match configured max depth {}", r.proof.acct_pf.max_depth, self.params.max_trie_depth); + } + } + self.input = Some(input); + Ok(()) + } + /// Includes computing the component commitment to the logical output (the subquery results). + /// **In addition** performs _promise calls_ to the Header Component to verify + /// all `(block_number, state_root)` pairs as additional "enriched" header subqueries. + /// These are checked against the supplied promise commitment using dynamic lookups + /// (behind the scenes) by `promise_caller`. + fn virtual_assign_phase0( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_caller: PromiseCaller, + ) -> CoreBuilderOutput { + // preamble: to be removed + let keccak = + KeccakChip::new_with_promise_collector(builder.range_chip(), promise_caller.clone()); + let range_chip = keccak.range(); + let rlp = RlpChip::new(range_chip, None); + let mut poseidon = create_hasher(); + poseidon.initialize_consts(builder.base.main(0), keccak.gate()); + + // Assumption: we already have input when calling this function. + // TODO: automatically derive a dummy input from params. + let input = self.input.as_ref().unwrap(); + + let mpt = MPTChip::new(rlp, &keccak); + let chip = EthStorageChip::new(&mpt, None); + let base_builder = &mut builder.base; + // actual logic + let payload = + parallelize_core(base_builder.pool(0), input.requests.clone(), |ctx, subquery| { + handle_single_account_subquery_phase0(ctx, &chip, &subquery) + }); + + let vt = extract_virtual_table(payload.iter().map(|p| p.output)); + let lr: Vec> = + extract_logical_results(payload.iter().map(|p| p.output)); + + let ctx = base_builder.main(0); + // promise calls to header component: + // - for each block number in a subquery, we must make a promise call to check the state root of that block + let header_state_root_idx = ctx.load_constant(F::from(STATE_ROOT_INDEX as u64)); + for p in payload.iter() { + let block_number = p.output.subquery.block_number; + let state_root = p.state_root; + let header_subquery = + AssignedHeaderSubquery { block_number, field_idx: header_state_root_idx }; + let promise_state_root = promise_caller + .call::, ComponentTypeHeaderSubquery>( + ctx, + FieldHeaderSubqueryCall(header_subquery), + ) + .unwrap(); + constrain_vec_equal(ctx, &state_root.hi_lo(), &promise_state_root.hi_lo()); + } + self.payload = Some((keccak, payload)); + CoreBuilderOutput { public_instances: vec![], virtual_table: vt, logical_results: lr } + } + + fn virtual_assign_phase1(&mut self, builder: &mut RlcCircuitBuilder) { + let (keccak, payload) = self.payload.take().unwrap(); + // preamble + let range_chip = keccak.range(); + let rlc_chip = builder.rlc_chip(&range_chip.gate); + let rlp = RlpChip::new(range_chip, Some(&rlc_chip)); + let mpt = MPTChip::new(rlp, &keccak); + let chip = EthStorageChip::new(&mpt, None); + + // actual logic + builder.parallelize_phase1(payload, |(ctx_gate, ctx_rlc), payload| { + handle_single_account_subquery_phase1((ctx_gate, ctx_rlc), &chip, payload) + }); + } +} + +pub struct PayloadAccountSubquery { + pub account_witness: EthAccountWitness, + pub state_root: HiLo>, + pub output: AssignedAccountSubqueryResult, +} + +/// Assigns `subquery` to virtual cells and then handles the subquery to get result. +/// **Assumes** that the stateRoot is verified. Returns the assigned private witnesses of +/// `(block_number, state_root)`, to be looked up against Header Component promise. +pub fn handle_single_account_subquery_phase0( + ctx: &mut Context, + chip: &EthStorageChip, + subquery: &CircuitInputAccountSubquery, +) -> PayloadAccountSubquery { + let gate = chip.gate(); + let range = chip.range(); + let safe = SafeTypeChip::new(range); + // assign address as SafeBytes20 and also convert to single field element + let unsafe_address = unsafe_bytes_to_assigned(ctx, subquery.proof.addr.as_bytes()); + let address = safe.raw_bytes_to(ctx, unsafe_address); + // transmute SafeBytes20 to FixLenBytesVec + let addr = SafeTypeChip::unsafe_to_fix_len_bytes_vec(address.value().to_vec(), 20); + // convert bytes (160 bits) to single field element + let addr = bytes_be_to_uint(ctx, gate, addr.bytes(), 20); + // assign MPT proof + let mpt_proof = subquery.proof.acct_pf.clone().assign(ctx); + // convert state root to HiLo form to save for later. `parse_account_proof` will constrain these witnesses to be bytes + let state_root = unsafe_mpt_root_to_hi_lo(ctx, gate, &mpt_proof); + // Check the account MPT proof + let account_witness = chip.parse_account_proof_phase0(ctx, address, mpt_proof); + // get the value for subquery + let field_idx = ctx.load_witness(F::from(subquery.field_idx as u64)); + range.check_less_than_safe(ctx, field_idx, NUM_ACCOUNT_STATE_FIELDS as u64); + + // Left pad value types to 32 bytes and convert to HiLo + let mut account_fixed = zip(ACCOUNT_STATE_FIELDS_MAX_BYTES, ACCOUNT_STATE_FIELD_IS_VAR_LEN) + .zip(&account_witness.array_witness().field_witness) + .map(|((max_bytes, is_var_len), w)| { + let inputs = w.field_cells.clone(); + // if var len, then its either nonce or balance, which are value types + let fixed_bytes = if is_var_len { + let len = w.field_len; + let var_len_bytes = + SafeTypeChip::unsafe_to_var_len_bytes_vec(inputs, len, max_bytes); + assert!(var_len_bytes.max_len() <= 32); + var_len_bytes.left_pad_to_fixed(ctx, gate) + } else { + let len = inputs.len(); + SafeTypeChip::unsafe_to_fix_len_bytes_vec(inputs, len) + }; + let fixed_bytes = fixed_bytes.into_bytes(); + // known to be <=32 bytes + pack_bytes_to_hilo(ctx, gate, &fixed_bytes).hi_lo() + }) + .collect_vec(); + + let account_is_empty = account_witness.mpt_witness().slot_is_empty; + // If account does not exist, then return: + // - nonce = 0 + // - balance = 0 + // - storageRoot = null root hash = keccak(rlp("")) = keccak(0x80) = 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421 + // - codeHash = 0 (see Test Case 2 of https://eips.ethereum.org/EIPS/eip-1052) + for (i, account_field) in account_fixed.iter_mut().enumerate() { + if i == STORAGE_ROOT_INDEX { + let null_root = encode_h256_to_hilo(&KECCAK_RLP_EMPTY_STRING).hi_lo(); + for (limb, null_limb) in account_field.iter_mut().zip(null_root) { + *limb = gate.select(ctx, Constant(null_limb), *limb, account_is_empty); + } + } else { + for limb in account_field.iter_mut() { + *limb = gate.mul_not(ctx, account_is_empty, *limb); + } + } + } + + let indicator = gate.idx_to_indicator(ctx, field_idx, account_fixed.len()); + let value = gate.select_array_by_indicator(ctx, &account_fixed, &indicator); + let value = HiLo::from_hi_lo(value.try_into().unwrap()); + + let block_number = ctx.load_witness(F::from(subquery.block_number)); + + PayloadAccountSubquery { + account_witness, + state_root, + output: AssignedAccountSubqueryResult { + subquery: AssignedAccountSubquery { block_number, addr, field_idx }, + value, + }, + } +} + +pub fn handle_single_account_subquery_phase1( + ctx: RlcContextPair, + chip: &EthStorageChip, + payload: PayloadAccountSubquery, +) { + chip.parse_account_proof_phase1(ctx, payload.account_witness); +} diff --git a/axiom-query/src/components/subqueries/account/mod.rs b/axiom-query/src/components/subqueries/account/mod.rs new file mode 100644 index 00000000..7bfd1cc5 --- /dev/null +++ b/axiom-query/src/components/subqueries/account/mod.rs @@ -0,0 +1,40 @@ +//! # Account Subqueries Circuit +//! +//! | Account State Field | Max bytes | +//! |-------------------------|-------------| +//! | nonce | ≤8 | +//! | balance | ≤12 | +//! | storageRoot | 32 | +//! | codeHash | 32 | +//! +//! Account subquery +//! - `blockNumber` (uint32) +//! - `addr` (address = bytes20) +//! - `fieldIdx` (uint32) +//! + +use std::str::FromStr; + +use ethers_core::types::H256; + +/// Circuit and Component Implementation. +pub mod circuit; +/// Types +pub mod types; + +#[cfg(test)] +pub mod tests; + +pub const STORAGE_ROOT_INDEX: usize = 2; + +lazy_static::lazy_static! { + /// keccak(rlp("")) = keccak(0x80) + pub static ref KECCAK_RLP_EMPTY_STRING: H256 = H256::from_str("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(); +} + +#[cfg(test)] +#[test] +fn test_null_mpt_root() { + use ethers_core::utils::keccak256; + assert_eq!(KECCAK_RLP_EMPTY_STRING.as_bytes(), &keccak256(vec![0x80])); +} diff --git a/axiom-query/src/components/subqueries/account/tests.rs b/axiom-query/src/components/subqueries/account/tests.rs new file mode 100644 index 00000000..f066b756 --- /dev/null +++ b/axiom-query/src/components/subqueries/account/tests.rs @@ -0,0 +1,218 @@ +use std::{marker::PhantomData, str::FromStr}; + +use axiom_codec::types::{field_elements::AnySubqueryResult, native::HeaderSubquery}; +use axiom_eth::{ + block_header::STATE_ROOT_INDEX, + halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr}, + keccak::{promise::generate_keccak_shards_from_calls, types::ComponentTypeKeccak}, + providers::{setup_provider, storage::json_to_mpt_input}, + utils::component::{ + promise_loader::single::PromiseLoaderParams, ComponentCircuit, + ComponentPromiseResultsInMerkle, ComponentType, + }, +}; +use ethers_core::types::{Address, Chain, H256}; +use ethers_providers::Middleware; +use futures::future::join_all; +use itertools::Itertools; +use rand::{rngs::StdRng, Rng}; +use rand_core::SeedableRng; +use tokio; + +use crate::components::{ + dummy_rlc_circuit_params, + subqueries::{ + block_header::{ + tests::get_latest_block_number, + types::{ComponentTypeHeaderSubquery, OutputHeaderShard}, + }, + common::shard_into_component_promise_results, + }, +}; + +use super::{ + circuit::{ComponentCircuitAccountSubquery, CoreParamsAccountSubquery}, + types::{CircuitInputAccountShard, CircuitInputAccountSubquery}, +}; + +pub const ACCOUNT_PROOF_MAX_DEPTH: usize = 13; + +async fn test_mock_account_subqueries( + k: u32, + network: Chain, + subqueries: Vec<(u64, &str, usize)>, // (blockNum, addr, fieldIdx) + keccak_f_capacity: usize, +) -> ComponentCircuitAccountSubquery { + let _ = env_logger::builder().is_test(true).try_init(); + + let _provider = setup_provider(network); + let provider = &_provider; + let requests = + join_all(subqueries.into_iter().map(|(block_num, addr, field_idx)| async move { + let addr = Address::from_str(addr).unwrap(); + let block = provider.get_block(block_num).await.unwrap().unwrap(); + let proof = provider.get_proof(addr, vec![], Some(block_num.into())).await.unwrap(); + let mut proof = json_to_mpt_input(proof, ACCOUNT_PROOF_MAX_DEPTH, 0); + proof.acct_pf.root_hash = block.state_root; + CircuitInputAccountSubquery { + block_number: block_num, + field_idx: field_idx as u32, + proof, + } + })) + .await; + + let mut promise_header = OutputHeaderShard { + results: requests + .iter() + .map(|r| AnySubqueryResult { + subquery: HeaderSubquery { + block_number: r.block_number as u32, + field_idx: STATE_ROOT_INDEX as u32, + }, + value: r.proof.acct_pf.root_hash, + }) + .collect(), + }; + // shard_into_component_promise_results::>(promise_header); + // add in an extra one just to test lookup table + promise_header.results.push(AnySubqueryResult { + subquery: HeaderSubquery { block_number: 0x9165ed, field_idx: 4 }, + value: H256::from_str("0xec4c8ec02281c196e3f882b4061c05f4f0843a0eaf72a3ee4715077220934bbc") + .unwrap(), + }); + + let header_capacity = promise_header.len(); + + let circuit_params = dummy_rlc_circuit_params(k as usize); + let mut circuit = ComponentCircuitAccountSubquery::new( + CoreParamsAccountSubquery { + capacity: requests.len(), + max_trie_depth: ACCOUNT_PROOF_MAX_DEPTH, + }, + ( + PromiseLoaderParams::new_for_one_shard(keccak_f_capacity), + PromiseLoaderParams::new_for_one_shard(header_capacity), + ), + circuit_params, + ); + + let input = CircuitInputAccountShard:: { requests, _phantom: PhantomData }; + circuit.feed_input(Box::new(input)).unwrap(); + circuit.calculate_params(); + + let promises = [ + ( + ComponentTypeKeccak::::get_type_id(), + ComponentPromiseResultsInMerkle::from_single_shard( + generate_keccak_shards_from_calls(&circuit, keccak_f_capacity) + .unwrap() + .into_logical_results(), + ), + ), + ( + ComponentTypeHeaderSubquery::::get_type_id(), + shard_into_component_promise_results::>( + promise_header.into(), + ), + ), + ] + .into_iter() + .collect(); + circuit.fulfill_promise_results(&promises).unwrap(); + + let instances: Vec = circuit.get_public_instances().into(); + MockProver::run(k, &circuit, vec![instances]).unwrap().assert_satisfied(); + circuit +} + +#[tokio::test] +async fn test_mock_account_subqueries_simple() { + let k = 18; + let subqueries = vec![ + (17143006, "0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B", 0), + (17143000, "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", 1), + (15000000, "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", 2), + (15411056, "0x1c479675ad559DC151F6Ec7ed3FbF8ceE79582B6", 3), + (18320885, "0x0000a26b00c1F0DF003000390027140000fAa719", 0), // Opensea: fees 3 + (18320885, "0x00005EA00Ac477B1030CE78506496e8C2dE24bf5", 0), // Seadrop + (18320885, "0x000000008924D42d98026C656545c3c1fb3ad31C", 0), // Seadrop factory + ]; + let _circuit = test_mock_account_subqueries(k, Chain::Mainnet, subqueries, 200).await; + // TODO: validate field values +} + +// this test takes a while +#[tokio::test] +async fn test_mock_account_subqueries_vanity() { + let k = 19; + let latest = 18320885; + let mut rng = StdRng::seed_from_u64(0); + let subqueries = (0..32) + .flat_map(|_| { + vec![ + ( + rng.gen_range(15303574..latest), + "0x0000a26b00c1F0DF003000390027140000fAa719", + rng.gen_range(0..4), + ), // Opensea: fees 3 + ( + rng.gen_range(15527904..latest), + "0x00005EA00Ac477B1030CE78506496e8C2dE24bf5", + rng.gen_range(0..4), + ), // Seadrop + ( + rng.gen_range(16836342..latest), + "0x000000008924D42d98026C656545c3c1fb3ad31C", + rng.gen_range(0..4), + ), // Seadrop factory + ] + }) + .collect_vec(); + let _circuit = test_mock_account_subqueries(k, Chain::Mainnet, subqueries, 2800).await; +} + +#[tokio::test] +async fn test_mock_account_subqueries_genesis() { + let network = Chain::Mainnet; + // address existing in block 0 + let address = "0x756F45E3FA69347A9A973A725E3C98bC4db0b5a0"; + let mut rng = StdRng::seed_from_u64(1); + let latest = get_latest_block_number(network).await; + let mut subqueries: Vec<_> = + (0..7).map(|_| (rng.gen_range(0..latest), address, rng.gen_range(0..4))).collect(); + subqueries.push((0, address, 0)); + let _circuit = test_mock_account_subqueries(18, network, subqueries, 300).await; +} + +#[tokio::test] +async fn test_mock_eoa_account_subqueries() { + let network = Chain::Mainnet; + let subqueries: Vec<_> = vec![ + (18320885, "0x5cC0d3B4926D5430946Ea1b60eA2B27974485921", 0), + (18320885, "0x5cC0d3B4926D5430946Ea1b60eA2B27974485921", 1), + (18320885, "0x5cC0d3B4926D5430946Ea1b60eA2B27974485921", 2), + (18320885, "0x5cC0d3B4926D5430946Ea1b60eA2B27974485921", 3), + ]; + let _circuit = test_mock_account_subqueries(18, network, subqueries, 50).await; +} + +#[tokio::test] +async fn test_mock_empty_account_subqueries() { + let network = Chain::Mainnet; + let subqueries: Vec<_> = vec![ + (18320885, "0x000000008924D42d98026C656545c3c1fb3ad31B", 0), // empty account + (18320885, "0x000000008924D42d98026C656545c3c1fb3ad31B", 1), + (18320885, "0x000000008924D42d98026C656545c3c1fb3ad31B", 2), + (18320885, "0x000000008924D42d98026C656545c3c1fb3ad31B", 3), + ]; + let _circuit = test_mock_account_subqueries(18, network, subqueries, 50).await; +} + +#[tokio::test] +async fn test_mock_empty_account_subqueries2() { + let network = Chain::Goerli; + // non-inclusion ends in extension node + let subqueries: Vec<_> = vec![(9173678, "0x8dde5d4a8384f403f888e1419672d94c570440c9", 0)]; + let _circuit = test_mock_account_subqueries(18, network, subqueries, 50).await; +} diff --git a/axiom-query/src/components/subqueries/account/types.rs b/axiom-query/src/components/subqueries/account/types.rs new file mode 100644 index 00000000..49e1e509 --- /dev/null +++ b/axiom-query/src/components/subqueries/account/types.rs @@ -0,0 +1,129 @@ +//! Types are separated into: +//! - Circuit metadata that along with the circuit type determines the circuit configuration completely. +//! - Human readable _logical_ input and output to the circuit. These include private inputs and outputs that are only commited to in the public output. +//! - The in-circuit formatted versions of logical inputs and outputs. These include formatting in terms of field elements and accounting for all lengths needing to be fixed at compile time. +//! - We then provide conversion functions from human-readable to circuit formats. +//! - This circuit has no public instances (IO) other than the circuit's own component commitment and the promise commitments from any component calls. +use std::marker::PhantomData; + +use axiom_codec::{ + types::{field_elements::FieldAccountSubquery, native::AccountSubquery}, + HiLo, +}; +use axiom_eth::{ + halo2_base::AssignedValue, + impl_fix_len_call_witness, + providers::storage::json_to_mpt_input, + storage::circuit::EthStorageInput, + utils::{ + build_utils::dummy::DummyFrom, + component::{circuit::CoreBuilderInput, ComponentType, ComponentTypeId, LogicalResult}, + }, +}; +use ethers_core::types::{EIP1186ProofResponse, H256}; +use serde::{Deserialize, Serialize}; + +use crate::{ + components::subqueries::common::OutputSubqueryShard, utils::codec::AssignedAccountSubquery, + Field, +}; + +use super::circuit::CoreParamsAccountSubquery; + +/// Identifier for the component type of this component circuit +pub struct ComponentTypeAccountSubquery(PhantomData); + +/// Human readable. +/// The output value of any account subquery is always `bytes32` right now. +pub type OutputAccountShard = OutputSubqueryShard; + +/// Circuit input for a shard of Account subqueries. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CircuitInputAccountShard { + /// Enriched subquery requests + pub requests: Vec, + pub _phantom: PhantomData, +} + +/// Circuit input for a single Account subquery. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CircuitInputAccountSubquery { + /// The block number to access the account state at. + pub block_number: u64, + /// Account proof formatted as MPT input. `proof.storage_pfs` will be empty. + /// It will contain the correct state root of the block. + pub proof: EthStorageInput, + pub field_idx: u32, +} + +impl DummyFrom for CircuitInputAccountShard { + fn dummy_from(core_params: CoreParamsAccountSubquery) -> Self { + let CoreParamsAccountSubquery { capacity, max_trie_depth } = core_params; + let request = { + let mut pf: EIP1186ProofResponse = + serde_json::from_str(GENESIS_ADDRESS_0_ACCOUNT_PROOF).unwrap(); + pf.storage_proof.clear(); + let proof = json_to_mpt_input(pf, max_trie_depth, 0); + CircuitInputAccountSubquery { block_number: 0, field_idx: 0, proof } + }; + Self { requests: vec![request; capacity], _phantom: PhantomData } + } +} + +/// The output value of any account subquery is always `bytes32` right now. +/// Vector has been resized to the capacity. +pub type CircuitOutputAccountShard = OutputSubqueryShard, HiLo>; + +impl_fix_len_call_witness!( + FieldAccountSubqueryCall, + FieldAccountSubquery, + ComponentTypeAccountSubquery +); + +// ===== The account component has no public instances other than the component commitment and promise commitments from external component calls ===== + +impl ComponentType for ComponentTypeAccountSubquery { + type InputValue = FieldAccountSubquery; + type InputWitness = AssignedAccountSubquery; + type OutputValue = HiLo; + type OutputWitness = HiLo>; + type LogicalInput = FieldAccountSubquery; + + fn get_type_id() -> ComponentTypeId { + "axiom-query:ComponentTypeAccountSubquery".to_string() + } + + fn logical_result_to_virtual_rows_impl( + ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + vec![(ins.input, ins.output)] + } + fn logical_input_to_virtual_rows_impl(li: &Self::LogicalInput) -> Vec { + vec![*li] + } +} + +impl From for CircuitOutputAccountShard { + fn from(output: OutputAccountShard) -> Self { + output.convert_into() + } +} + +pub const GENESIS_ADDRESS_0_ACCOUNT_PROOF: &str = r#"{"address":"0x0000000000000000000000000000000000000000","balance":"0x0","codeHash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0","storageHash":"0x0000000000000000000000000000000000000000000000000000000000000000","accountProof":["0xf90211a090dcaf88c40c7bbc95a912cbdde67c175767b31173df9ee4b0d733bfdd511c43a0babe369f6b12092f49181ae04ca173fb68d1a5456f18d20fa32cba73954052bda0473ecf8a7e36a829e75039a3b055e51b8332cbf03324ab4af2066bbd6fbf0021a0bbda34753d7aa6c38e603f360244e8f59611921d9e1f128372fec0d586d4f9e0a04e44caecff45c9891f74f6a2156735886eedf6f1a733628ebc802ec79d844648a0a5f3f2f7542148c973977c8a1e154c4300fec92f755f7846f1b734d3ab1d90e7a0e823850f50bf72baae9d1733a36a444ab65d0a6faaba404f0583ce0ca4dad92da0f7a00cbe7d4b30b11faea3ae61b7f1f2b315b61d9f6bd68bfe587ad0eeceb721a07117ef9fc932f1a88e908eaead8565c19b5645dc9e5b1b6e841c5edbdfd71681a069eb2de283f32c11f859d7bcf93da23990d3e662935ed4d6b39ce3673ec84472a0203d26456312bbc4da5cd293b75b840fc5045e493d6f904d180823ec22bfed8ea09287b5c21f2254af4e64fca76acc5cd87399c7f1ede818db4326c98ce2dc2208a06fc2d754e304c48ce6a517753c62b1a9c1d5925b89707486d7fc08919e0a94eca07b1c54f15e299bd58bdfef9741538c7828b5d7d11a489f9c20d052b3471df475a051f9dd3739a927c89e357580a4c97b40234aa01ed3d5e0390dc982a7975880a0a089d613f26159af43616fd9455bb461f4869bfede26f2130835ed067a8b967bfb80","0xf90211a06f499aafd2fcc95db6ac85b9ec36ce16b3747180d51b7ba72babdbceaef0cac8a034ffbe94cc9f4ac7e43bbd0ab875ce079e5d131f72f33974c09525bad37da4b4a026ac19ac1e99055b84ef53fad0ff4bf76a54af485b399dac5d91e55320941c16a0a33d103a92ff6f95c081309f83f474a009048614d5d40e14067dbae0cf9ed084a046a0e834a4f3482cb37f70f1f188d7c749c33fb8b94854b16dcebe840fc9390aa0a5a914013e15472dc3ae08f774e9d5ac3127419a2c81bec98963f40dde42ebaaa0b5740bdfa8ecf2b4d0b560f72474846788a3e19f9e0894c6bd2eb46255d222e9a04aa4e4ebe1930364ae283e8f1fa4de7ef1867a3f7fb89c23e068b807464eac14a0f84e5e71db73c15fc0bfa5566fae5e687e8eed398ef68e0d8229a7bc2eb333fda0551d35fa9c76d23bbbc1feb30a16e6ee1087c96aa2c31a8be297c4904c37373ba0f25b1be3ea53f222e17985dde60b04716bc342232874af3ad0af1652165138f2a0e50848e903b54f966851f4cbac1deb5b1d1beb42b4223379bb911f68001747f8a021d90bccf615ff6349cc5fdf8604ee52789c0e977fe12c2401b1cc229a9e7e47a0ade009f37dd2907895900d853eefbcf26af8f1665c8802804584241d825a6b49a09fe500ded938f686589ab2f42caad51341441980ea20e9fcb69e62b774c9990fa087888bb118be98fa5dfd57a76d0b59af08d7977fe87bad7c0d68ef82f2c9a92880","0xf901b1a0ec652a529bfb6f625879b961b8f7986b8294cfb1082d24b2c27f9a5b3fbccece80a088a3bacf48a0d00e3b36c3274ca2ab8d9d8f54c90e03724b3f6f5137c5a350c1a0a3c84954aad8408ed44eed138573a4db917e19d49e6cb716c14c7dedcb7a0051a069d3ae295c988b5e52f9d86b3aa85e9167a2d59a5ad47b6d1f8faaae9cd3aee4a0252dbbed1d3b713b43b6b8745d1d60605bbc4474746bfffe16375acbf42c0ec080a0a886f03399a8e32312b65d59b2a5d97ba7bb078aa5dab7aeb08d1fbd721a0944a0e9b89be70399650793c37b4aca1779e5adf4d8a07cea63dab9a9f5ef6b7dc66fa0b352a156bda0e42ce192bc430f8513e827b0aaa70002a21fef3a4df416be93e9a00665ba82ae23119a4a244be15e42e23589490995236c43bac11b5628613c337ba0b45176ce952dda9f523f244d921805f005c11b2027f53d12dda0e069278cf908a0eefa94d2ecf8946494c634277eac048823f35f7820d354c6e9352176c9b44e4da046443df5febce492f17eed4f98f2ad755fec20cd9eede857dc951757ef85b51aa0fc14ff2dbb3675d852fb37d7796eb1f303282d3466aef865a17da813d22bfc028080","0xf8718080a06f69700f636d81db1793bcee2562dcf0a4a06f2525fb2f55f5c00aab81b4b86880a00f13a02c0878c787e7f9fdcfbb3b3169b42c2a0595c7afecf86dbb46fbcb567b80808080a05c80e25e034b9cc0f079b1226a97c22434851b86c6b55be77eae89b096462afd80808080808080"],"storageProof":[{"key":"0x0000000000000000000000000000000000000000000000000000000000000000","proof":[],"value":"0x0"}]}"#; + +#[cfg(test)] +mod test { + use axiom_eth::providers::setup_provider; + use ethers_core::types::{Chain, H256}; + use ethers_providers::Middleware; + + use super::GENESIS_ADDRESS_0_ACCOUNT_PROOF; + + #[tokio::test] + async fn test_dummy_account_proof() { + let provider = setup_provider(Chain::Mainnet); + let address = "0x0000000000000000000000000000000000000000"; + let proof = provider.get_proof(address, vec![H256::zero()], Some(0.into())).await.unwrap(); + assert_eq!(GENESIS_ADDRESS_0_ACCOUNT_PROOF, serde_json::to_string(&proof).unwrap()); + } +} diff --git a/axiom-query/src/components/subqueries/block_header/circuit.rs b/axiom-query/src/components/subqueries/block_header/circuit.rs new file mode 100644 index 00000000..7fe68301 --- /dev/null +++ b/axiom-query/src/components/subqueries/block_header/circuit.rs @@ -0,0 +1,372 @@ +use std::iter::zip; + +use axiom_codec::{ + constants::FIELD_IDX_BITS, + special_values::{ + HEADER_EXTRA_DATA_LEN_FIELD_IDX, HEADER_HASH_FIELD_IDX, HEADER_HEADER_SIZE_FIELD_IDX, + HEADER_LOGS_BLOOM_FIELD_IDX_OFFSET, + }, + HiLo, +}; +use axiom_eth::{ + block_header::{ + get_block_header_rlp_max_lens_from_extra, EthBlockHeaderChip, EthBlockHeaderWitness, + BLOCK_HEADER_FIELD_IS_VAR_LEN, EXTRA_DATA_INDEX, NUM_BLOCK_HEADER_FIELDS, + }, + halo2_base::{ + gates::{flex_gate::threads::parallelize_core, GateInstructions, RangeInstructions}, + safe_types::{SafeBool, SafeTypeChip}, + utils::bit_length, + AssignedValue, Context, + QuantumCell::Constant, + }, + halo2_proofs::plonk::ConstraintSystem, + keccak::{types::ComponentTypeKeccak, KeccakChip}, + rlc::circuit::builder::RlcCircuitBuilder, + rlc::circuit::builder::RlcContextPair, + rlp::RlpChip, + utils::{ + build_utils::aggregation::CircuitMetadata, + circuit_utils::extract_array_chunk, + component::{ + circuit::{ + ComponentBuilder, ComponentCircuitImpl, CoreBuilder, CoreBuilderOutput, + CoreBuilderOutputParams, CoreBuilderParams, + }, + promise_collector::PromiseCaller, + promise_loader::single::PromiseLoader, + types::FixLenLogical, + utils::create_hasher, + LogicalResult, + }, + }, + utils::{ + circuit_utils::{ + bytes::{pack_bytes_to_hilo, select_hi_lo}, + is_equal_usize, is_in_range, min_with_usize, unsafe_lt_mask, + }, + load_h256_to_safe_bytes32, unsafe_bytes_to_assigned, + }, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::{ + components::subqueries::common::{extract_logical_results, extract_virtual_table}, + utils::codec::{AssignedHeaderSubquery, AssignedHeaderSubqueryResult}, + Field, +}; + +use super::{ + mmr_verify::{assign_mmr, verify_mmr_proof, AssignedMmr}, + types::{ + CircuitInputHeaderShard, CircuitInputHeaderSubquery, ComponentTypeHeaderSubquery, + LogicalPublicInstanceHeader, + }, +}; + +pub struct CoreBuilderHeaderSubquery { + input: Option>, + params: CoreParamsHeaderSubquery, + payload: Option<(KeccakChip, Vec>)>, +} + +/// Specify the output format of HeaderSubquery component. +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct CoreParamsHeaderSubquery { + pub max_extra_data_bytes: usize, + pub capacity: usize, +} +impl CoreBuilderParams for CoreParamsHeaderSubquery { + fn get_output_params(&self) -> CoreBuilderOutputParams { + CoreBuilderOutputParams::new(vec![self.capacity]) + } +} + +/// For header circuit to read promise results. +pub type PromiseLoaderHeaderSubquery = PromiseLoader>; +pub type ComponentCircuitHeaderSubquery = + ComponentCircuitImpl, PromiseLoaderHeaderSubquery>; + +impl CircuitMetadata for CoreBuilderHeaderSubquery { + const HAS_ACCUMULATOR: bool = false; + fn num_instance(&self) -> Vec { + unreachable!() + } +} + +impl ComponentBuilder for CoreBuilderHeaderSubquery { + type Params = CoreParamsHeaderSubquery; + + fn new(params: Self::Params) -> Self { + Self { input: None, params, payload: None } + } + fn get_params(&self) -> Self::Params { + self.params.clone() + } + fn clear_witnesses(&mut self) { + self.payload = None; + } + fn calculate_params(&mut self) -> Self::Params { + self.params.clone() + } + fn configure_with_params(_: &mut ConstraintSystem, _: Self::Params) {} +} +impl CoreBuilder for CoreBuilderHeaderSubquery { + type CompType = ComponentTypeHeaderSubquery; + type PublicInstanceValue = LogicalPublicInstanceHeader; + type PublicInstanceWitness = LogicalPublicInstanceHeader>; + type CoreInput = CircuitInputHeaderShard; + fn feed_input(&mut self, input: Self::CoreInput) -> anyhow::Result<()> { + let (header_rlp_max_bytes, _) = + get_block_header_rlp_max_lens_from_extra(self.params.max_extra_data_bytes); + for request in &input.requests { + if request.header_rlp.len() != header_rlp_max_bytes { + anyhow::bail!("Header RLP length not resized correctly."); + } + } + self.input = Some(input); + Ok(()) + } + // No public instances are assigned inside this function. That is done automatically. + fn virtual_assign_phase0( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_caller: PromiseCaller, + ) -> CoreBuilderOutput { + // preamble: to be removed + let keccak = + KeccakChip::new_with_promise_collector(builder.range_chip(), promise_caller.clone()); + let range_chip = keccak.range(); + let rlp = RlpChip::new(range_chip, None); + let mut poseidon = create_hasher(); + poseidon.initialize_consts(builder.base.main(0), keccak.gate()); + + // Assumption: we already have input when calling this function. + // TODO: automatically derive a dummy input from params. + let input = self.input.as_ref().unwrap(); + let base_builder = &mut builder.base; + // assign MMR and compute its keccakPacked + let ctx = base_builder.main(0); + let assigned_mmr = assign_mmr(ctx, range_chip, input.mmr); + let mmr_keccak = assigned_mmr.keccak(ctx, &keccak); + // handle subqueries + let pool = base_builder.pool(0); + let chip = EthBlockHeaderChip::new(rlp, self.params.max_extra_data_bytes); + let payload = parallelize_core(pool, input.requests.clone(), |ctx, subquery| { + handle_single_header_subquery_phase0(ctx, &chip, &keccak, &subquery, &assigned_mmr) + }); + + let vt = extract_virtual_table(payload.iter().map(|p| p.output)); + let lr: Vec> = + extract_logical_results(payload.iter().map(|p| p.output)); + + let logical_pis = + LogicalPublicInstanceHeader { mmr_keccak: HiLo::from_hi_lo(mmr_keccak.hi_lo()) }; + self.payload = Some((keccak, payload)); + + CoreBuilderOutput { + public_instances: logical_pis.into_raw(), + virtual_table: vt, + logical_results: lr, + } + } + + // There is no additional logic necessary for component commitments. + fn virtual_assign_phase1(&mut self, builder: &mut RlcCircuitBuilder) { + let (keccak, payload) = self.payload.take().unwrap(); + // preamble + let range_chip = keccak.range(); + let rlc_chip = builder.rlc_chip(&range_chip.gate); + let rlp = RlpChip::new(range_chip, Some(&rlc_chip)); + + // actual logic + let chip = EthBlockHeaderChip::new(rlp, self.params.max_extra_data_bytes); + builder.parallelize_phase1(payload, |(ctx_gate, ctx_rlc), payload| { + handle_single_header_subquery_phase1((ctx_gate, ctx_rlc), &chip, payload) + }); + } +} + +/// Non-value types are right padded with zeros. +/// "Value" type in the sense of [Solidity](https://docs.soliditylang.org/en/latest/types.html). Essentially everything that's not a variable length array. +/// We should conform to how Value types are left vs right padded with zeros in EVM memory: https://ethdebug.github.io/solidity-data-representation/#table-of-direct-types +/// There is currently no type in block header that is `bytesN` where `N < 32`. If there were, we would want to *right pad* those with zeros. +/// `bytes32` is neither left nor right padded. +/// +/// Currently `logsBloom` (fixed len 256 bytes) and `extraData` (var len bytes) are not left padded. +pub const BLOCK_HEADER_FIELD_SHOULD_LEFT_PAD: [bool; NUM_BLOCK_HEADER_FIELDS] = [ + true, true, true, true, true, true, false, true, true, true, true, true, false, true, true, + true, true, true, true, true, +]; + +pub struct PayloadHeaderSubquery { + pub header_witness: EthBlockHeaderWitness, + pub output: AssignedHeaderSubqueryResult, +} + +/// Assigns `subquery` to virtual cells and then handles the subquery to get result. +pub fn handle_single_header_subquery_phase0( + ctx: &mut Context, + chip: &EthBlockHeaderChip, + keccak: &KeccakChip, + subquery: &CircuitInputHeaderSubquery, + assigned_mmr: &AssignedMmr, +) -> PayloadHeaderSubquery { + let gate = chip.gate(); + let range = chip.range(); + let safe = SafeTypeChip::new(range); + let header_rlp = unsafe_bytes_to_assigned(ctx, &subquery.header_rlp); + // parse the header RLP + let header_witness = chip.decompose_block_header_phase0(ctx, keccak, &header_rlp); + + // verify MMR proof for this block + let block_number = header_witness.get_number_value(ctx, gate); + let block_hash = header_witness.block_hash.output_bytes.clone(); + let mmr_proof = (subquery.mmr_proof.iter()) + .map(|&node| load_h256_to_safe_bytes32(ctx, &safe, node)) + .collect(); + verify_mmr_proof(ctx, keccak, assigned_mmr, block_number, block_hash, mmr_proof, None); + + let field_idx = ctx.load_witness(F::from(subquery.field_idx as u64)); + range.range_check(ctx, field_idx, FIELD_IDX_BITS); + // if `field_idx` < `HEADER_HASH_IDX`, then it is an actual header field + let threshold = Constant(F::from(HEADER_HASH_FIELD_IDX as u64)); + let is_idx_in_header = range.is_less_than(ctx, field_idx, threshold, FIELD_IDX_BITS); + let header_idx = gate.mul(ctx, field_idx, is_idx_in_header); + + let (_, header_fields_max_bytes) = + get_block_header_rlp_max_lens_from_extra(chip.max_extra_data_bytes); + // Left pad value types to 32 bytes and convert to HiLo + let header_fixed = zip(BLOCK_HEADER_FIELD_IS_VAR_LEN, BLOCK_HEADER_FIELD_SHOULD_LEFT_PAD) + .zip_eq(&header_witness.rlp_witness.field_witness) + .enumerate() + .map(|(i, ((is_var_len, left_pad), w))| { + let inputs = w.field_cells.clone(); + let fixed_bytes = if is_var_len && left_pad { + let len = w.field_len; + let var_len_bytes = SafeTypeChip::unsafe_to_var_len_bytes_vec( + inputs, + len, + header_fields_max_bytes[i], + ); + assert!(var_len_bytes.max_len() <= 32); + var_len_bytes.left_pad_to_fixed(ctx, gate) + } else { + let len = inputs.len(); + // currently the only var len field that is not value type is `extraData` + SafeTypeChip::unsafe_to_fix_len_bytes_vec(inputs, len) + }; + let mut fixed_bytes = fixed_bytes.into_bytes(); + if fixed_bytes.len() > 32 { + assert!(!left_pad); + fixed_bytes.truncate(32); + } + // constrain `extraData` is 0s after length + if i == EXTRA_DATA_INDEX { + let mut len = w.field_len; + if chip.max_extra_data_bytes > 32 { + let max_bits = bit_length(chip.max_extra_data_bytes as u64); + len = min_with_usize(ctx, range, len, 32, max_bits); + } + let mask = unsafe_lt_mask(ctx, gate, len, 32); + for (byte, mask) in fixed_bytes.iter_mut().zip_eq(mask) { + *byte = SafeTypeChip::unsafe_to_byte(gate.mul(ctx, *byte, mask)); + } + } + + // Slightly more optimal to pack to 20 bytes for `beneficiary` but HiLo is cleaner, so we'll sacrifice the optimization + pack_bytes_to_hilo(ctx, gate, &fixed_bytes).hi_lo() + }) + .collect_vec(); + let header_indicator = gate.idx_to_indicator(ctx, header_idx, header_fixed.len()); + let value = gate.select_array_by_indicator(ctx, &header_fixed, &header_indicator); + let mut value = HiLo::from_hi_lo(value.try_into().unwrap()); + // time to handle special cases: + let [return_hash, return_size, return_extra_data_len] = + [HEADER_HASH_FIELD_IDX, HEADER_HEADER_SIZE_FIELD_IDX, HEADER_EXTRA_DATA_LEN_FIELD_IDX] + .map(|const_idx| is_equal_usize(ctx, gate, field_idx, const_idx)); + // return block hash + let block_hash = HiLo::from_hi_lo(header_witness.get_block_hash_hi_lo()); + value = select_hi_lo(ctx, gate, &block_hash, &value, return_hash); + // return block size in bytes + let block_size = HiLo::from_hi_lo([ctx.load_zero(), header_witness.rlp_witness.rlp_len]); + value = select_hi_lo(ctx, gate, &block_size, &value, return_size); + // return extra data length in bytes + let extra_data = header_witness.get_extra_data(); + let extra_data_len = HiLo::from_hi_lo([ctx.load_zero(), extra_data.field_len]); + value = select_hi_lo(ctx, gate, &extra_data_len, &value, return_extra_data_len); + + let (logs_bloom_buf, return_logs_bloom) = handle_logs_bloom( + ctx, + range, + &header_witness.get_logs_bloom().field_cells, + field_idx, + HEADER_LOGS_BLOOM_FIELD_IDX_OFFSET, + ); + value = select_hi_lo(ctx, gate, &logs_bloom_buf, &value, return_logs_bloom); + + // constrain that `field_idx` is valid: either + // - `field_idx` is less than true length of block header list + // - this means you cannot request a field such as `withdrawalsRoot` if the block is before EIP-4895 + // - or `field_idx` is one of the special return cases `header_idx` is less than true length of block header list + let is_valid_header_idx = + range.is_less_than(ctx, header_idx, header_witness.get_list_len(), FIELD_IDX_BITS); + // This sum is guaranteed to be 0 or 1: + let is_special_case = + gate.sum(ctx, [return_hash, return_size, return_extra_data_len, return_logs_bloom]); + let is_valid = gate.select(ctx, is_valid_header_idx, is_special_case, is_idx_in_header); + gate.assert_is_const(ctx, &is_valid, &F::ONE); + + PayloadHeaderSubquery { + header_witness, + output: AssignedHeaderSubqueryResult { + subquery: AssignedHeaderSubquery { block_number, field_idx }, + value, + }, + } +} + +pub fn handle_single_header_subquery_phase1( + ctx: RlcContextPair, + chip: &EthBlockHeaderChip, + payload: PayloadHeaderSubquery, +) { + chip.decompose_block_header_phase1(ctx, payload.header_witness); +} + +/// Returns `HiLo(logs_bloom_bytes[logs_bloom_idx..logs_bloom_idx + 32]), is_in_range` +/// where `logs_bloom_idx = field_idx - logs_bloom_field_idx_offset` and +/// `is_in_range = (0..8).contains(logs_bloom_idx)` +pub(crate) fn handle_logs_bloom( + ctx: &mut Context, + range: &impl RangeInstructions, + logs_bloom_bytes: &[AssignedValue], + field_idx: AssignedValue, + logs_bloom_field_idx_offset: usize, +) -> (HiLo>, SafeBool) { + let offset = logs_bloom_field_idx_offset; + let is_offset = is_in_range(ctx, range, field_idx, offset..offset + 8, FIELD_IDX_BITS); + let gate = range.gate(); + let mut shift = gate.sub(ctx, field_idx, Constant(F::from(offset as u64))); + shift = gate.mul(ctx, shift, *is_offset.as_ref()); + let buffer = extract_array_chunk(ctx, gate, logs_bloom_bytes, shift, 32); + let buffer = SafeTypeChip::unsafe_to_fix_len_bytes_vec(buffer, 32); + (pack_bytes_to_hilo(ctx, gate, buffer.bytes()), is_offset) +} + +#[cfg(test)] +mod test { + use axiom_eth::block_header::{EXTRA_DATA_INDEX, LOGS_BLOOM_INDEX}; + + use super::BLOCK_HEADER_FIELD_SHOULD_LEFT_PAD; + + #[test] + fn test_block_header_value_types() { + for (i, &is_value) in BLOCK_HEADER_FIELD_SHOULD_LEFT_PAD.iter().enumerate() { + if !is_value { + assert!(i == LOGS_BLOOM_INDEX || i == EXTRA_DATA_INDEX); + } + } + } +} diff --git a/axiom-query/src/components/subqueries/block_header/mmr_verify.rs b/axiom-query/src/components/subqueries/block_header/mmr_verify.rs new file mode 100644 index 00000000..f066d79f --- /dev/null +++ b/axiom-query/src/components/subqueries/block_header/mmr_verify.rs @@ -0,0 +1,166 @@ +use axiom_eth::{ + halo2_base::{ + gates::{GateInstructions, RangeChip, RangeInstructions}, + safe_types::{SafeBool, SafeBytes32, SafeTypeChip}, + utils::ScalarField, + AssignedValue, Context, + QuantumCell::{Constant, Existing}, + }, + keccak::{types::KeccakVarLenQuery, KeccakChip}, + utils::{is_zero_vec, load_h256_to_safe_bytes32}, +}; +use ethers_core::types::H256; +use itertools::Itertools; + +use crate::Field; + +use super::MMR_MAX_NUM_PEAKS; + +/// `mmr` is Merkle Mountain Range in *increasing* order of peak size. +/// +/// After construction it is guaranteed that: +/// * `mmr_num_blocks` is the length of the original list that `mmr` is a commitment to. +/// * `mmr_bits` is the same length as `mmr`. `mmr_bits[i]` is a bit that is 1 if `mmr[i]` is a non-empty peak, and 0 otherwise. In other words, `mmr_bits` is the little-endian bit representation of `mmr_num_blocks`. +#[derive(Clone, Debug)] +pub struct AssignedMmr { + pub mmr: [SafeBytes32; MMR_MAX_NUM_PEAKS], + pub mmr_bits: [SafeBool; MMR_MAX_NUM_PEAKS], + pub mmr_num_blocks: AssignedValue, +} + +pub fn assign_mmr( + ctx: &mut Context, + range: &RangeChip, + mmr: [H256; MMR_MAX_NUM_PEAKS], +) -> AssignedMmr { + let safe = SafeTypeChip::new(range); + let gate = range.gate(); + let mmr = mmr.map(|peak| load_h256_to_safe_bytes32(ctx, &safe, peak)); + let mmr_bits = mmr + .iter() + .map(|peak| { + let no_peak = is_zero_vec(ctx, gate, peak.value()); + SafeTypeChip::unsafe_to_bool(gate.not(ctx, no_peak)) + }) + .collect_vec(); + let mmr_num_blocks = gate.inner_product( + ctx, + mmr_bits.iter().map(|bit| *bit.as_ref()), + gate.pow_of_two().iter().take(mmr_bits.len()).map(|x| Constant(*x)), + ); + let mmr_bits = mmr_bits.try_into().unwrap(); + AssignedMmr { mmr, mmr_bits, mmr_num_blocks } +} + +pub type AssignedMmrKeccak = KeccakVarLenQuery; + +impl AssignedMmr { + pub fn keccak( + &self, + ctx: &mut Context, + keccak_chip: &KeccakChip, + ) -> AssignedMmrKeccak { + let gate = keccak_chip.gate(); + // mmr_num_peaks = bit_length(mmr_num_blocks) = MMR_MAX_NUM_PEAKS - num_leading_zeros(mmr_num_blocks) + let mut is_leading = Constant(F::ONE); + let mut num_leading_zeros = ctx.load_zero(); + for bit in self.mmr_bits.iter().rev() { + // is_zero = 1 - bit + // is_leading = is_leading * (is_zero) + is_leading = Existing(gate.mul_not(ctx, *bit.as_ref(), is_leading)); + num_leading_zeros = gate.add(ctx, num_leading_zeros, is_leading); + } + let max_num_peaks = F::from(MMR_MAX_NUM_PEAKS as u64); + let num_peaks = gate.sub(ctx, Constant(max_num_peaks), num_leading_zeros); + let mmr_bytes = gate.mul(ctx, num_peaks, Constant(F::from(32u64))); + keccak_chip.keccak_var_len( + ctx, + self.mmr.iter().flat_map(|bytes| bytes.value().iter().copied()).collect(), + mmr_bytes, + 0, + ) + } +} + +/// `mmr` is a Merkle Mountan Range (MMR) of block hashes (bytes32). +/// It is a commitment to block hashes for blocks [0, mmr_num_blocks). +/// Given a `merkle_proof` of a block hash at index `list_id`, +/// we verify the merkle proof into the MMR. +/// +/// If `not_empty` is None, then we definitely enforce the Merkle proof check. +/// If `not_empty` is Some, then we conditionally enforce the Merkle proof check +/// depending on the boolean flag. +pub fn verify_mmr_proof( + ctx: &mut Context, + keccak: &KeccakChip, + assigned_mmr: &AssignedMmr, + list_id: AssignedValue, // the index in underlying list + leaf: SafeBytes32, // the leaf node at `list_id` in underlying list + merkle_proof: Vec>, + not_empty: Option>, // actually do the proof check +) { + let AssignedMmr { mmr, mmr_bits, mmr_num_blocks } = assigned_mmr; + assert!(!mmr.is_empty()); + let range = keccak.range(); + let gate = range.gate(); + assert_eq!(mmr.len(), mmr_bits.len()); + let index_bits = range.gate().num_to_bits(ctx, list_id, mmr.len()); + range.check_less_than(ctx, list_id, *mmr_num_blocks, mmr.len()); + // count how many leading (big-endian) bits `mmr_bits` and `index_bits` have in common + let mut agree = Constant(F::ONE); + let mut num_leading_agree = ctx.load_zero(); + for (a, b) in mmr_bits.iter().rev().zip(index_bits.iter().rev()) { + let is_equal = bit_is_equal(ctx, gate, *a.as_ref(), *b); + agree = Existing(gate.mul(ctx, agree, is_equal)); + num_leading_agree = gate.add(ctx, num_leading_agree, agree); + } + // if num_leading_agree = mmr.len() that means peak_id = mmr_list_len is outside of this MMR + let max_peak_id = F::from(mmr.len() as u64 - 1); + let peak_id = gate.sub(ctx, Constant(max_peak_id), num_leading_agree); + + // we merkle prove `leaf` into `mmr[peak_id]` using `index_bits[..peak_id]` as the "side" + assert_eq!(merkle_proof.len() + 1, mmr.len()); // max depth of a peak is mmr.len() - 1 + let mut intermediate_hashes = Vec::with_capacity(mmr.len()); + intermediate_hashes.push(leaf); + // last index_bit is never used: if it were 1 then leading bit of mmr_bits would also have to be 1 + for (side, node) in index_bits.into_iter().zip(merkle_proof) { + let cur = intermediate_hashes.last().unwrap(); + // Possible optimization: if merkle_proof consists of unassigned witnesses, they can be assigned while `select`ing here. We avoid this low-level optimization for code clarity for now. + let concat = (cur.value().iter().chain(node.value())) + .zip_eq(node.value().iter().chain(cur.value())) + .map(|(a, b)| gate.select(ctx, *b, *a, side)) + .collect_vec(); + let hash = keccak.keccak_fixed_len(ctx, concat).output_bytes; + intermediate_hashes.push(hash); + } + let peak_indicator = gate.idx_to_indicator(ctx, peak_id, mmr.len()); + // get mmr[peak_id] + debug_assert_eq!(mmr[0].as_ref().len(), 32); + // H256 as bytes: + let peak = gate.select_array_by_indicator(ctx, mmr, &peak_indicator); + // H256 as bytes: + let proof_peak = gate.select_array_by_indicator(ctx, &intermediate_hashes, &peak_indicator); + // If Some, conditional selector on whether to check merkle proof validity + let not_empty: Option> = not_empty.map(|x| x.into()); + for (mut a, mut b) in peak.into_iter().zip_eq(proof_peak) { + if let Some(not_empty) = not_empty { + a = gate.mul(ctx, a, not_empty); + b = gate.mul(ctx, b, not_empty); + } + ctx.constrain_equal(&a, &b); + } +} + +/// Assumes `a, b` are both bits. +/// +/// Returns `a == b` as a bit. +pub fn bit_is_equal( + ctx: &mut Context, + gate: &impl GateInstructions, + a: AssignedValue, + b: AssignedValue, +) -> AssignedValue { + // (a == b) = 1 - (a - b)^2 + let diff = gate.sub(ctx, a, b); + gate.sub_mul(ctx, Constant(F::ONE), diff, diff) +} diff --git a/axiom-query/src/components/subqueries/block_header/mod.rs b/axiom-query/src/components/subqueries/block_header/mod.rs new file mode 100644 index 00000000..c038efaf --- /dev/null +++ b/axiom-query/src/components/subqueries/block_header/mod.rs @@ -0,0 +1,46 @@ +//! # Block Header Subqueries Circuit +//! +//! | Block Header Field | Max bytes | +//! |---------------------------|---------------| +//! | parentHash | 32 | +//! | ommersHash | 32 | +//! | beneficiary | 20 | +//! | stateRoot | 32 | +//! | transactionsRoot | 32 | +//! | receiptsRoot | 32 | +//! | logsBloom | 256 | +//! | difficulty | ≤7 | +//! | number | ≤4 | +//! | gasLimit | ≤4 | +//! | gasUsed | ≤4 | +//! | timestamp | ≤4 | +//! | extraData | ≤32 (mainnet) | +//! | mixHash | 32 | +//! | nonce | 8 | +//! | basefee (post-1559) | ≤32 or 0 | +//! | withdrawalsRoot (post-4895) | 32 or 0 | +//! +//! Header subquery +//! - `blockNumber` (uint32) +//! - `fieldIdx` (uint32) +//! - If the `fieldIdx` corresponds to `logsBloom`, +//! the `result` will be only the first 32 bytes. +//! We will add a special `LOGS_BLOOM_FIELD_IDX` so that if +//! `fieldIdx = LOGS_BLOOM_FIELD_IDX + logsBloomIdx`, +//! the result will be bytes `[32 * logsBloomIdx, 32 * logsBloomIdx + 32)` +//! for `logsBloomIdx` in `[0, 8)`. +//! +//! **Note:** We will always truncate `extraData` to 32 bytes +//! (for Goerli `extraData` can be longer, but we ignore the extra bytes). + +/// Circuit and Component Implementation. +pub mod circuit; +/// Verify all block hashes against a given Merkle Mountain Range. Used in [circuit] +pub mod mmr_verify; +/// Types +pub mod types; + +#[cfg(test)] +pub mod tests; + +pub const MMR_MAX_NUM_PEAKS: usize = 32; // assuming block number stays in u32, < 2^32 diff --git a/axiom-query/src/components/subqueries/block_header/tests.rs b/axiom-query/src/components/subqueries/block_header/tests.rs new file mode 100644 index 00000000..fbee9cbb --- /dev/null +++ b/axiom-query/src/components/subqueries/block_header/tests.rs @@ -0,0 +1,150 @@ +use std::{collections::HashMap, fs::File, marker::PhantomData}; + +use axiom_eth::{ + block_header::{ + get_block_header_extra_bytes_from_chain_id, get_block_header_rlp_max_lens_from_extra, + EXTRA_DATA_INDEX, + }, + halo2_base::halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr}, + halo2_proofs::plonk::Circuit, + keccak::{promise::generate_keccak_shards_from_calls, types::ComponentTypeKeccak}, + providers::{block::get_block_rlp_from_num, setup_provider}, + utils::{ + build_utils::pinning::{CircuitPinningInstructions, Halo2CircuitPinning}, + component::{ + promise_loader::{ + comp_loader::SingleComponentLoaderParams, single::PromiseLoaderParams, + }, + ComponentCircuit, ComponentPromiseResultsInMerkle, ComponentType, + }, + }, +}; +use ethers_core::types::{Chain, H256}; +use ethers_providers::Middleware; +use serde_json::{Result, Value}; +use test_log::test; + +use crate::components::{ + dummy_rlc_circuit_params, + subqueries::block_header::circuit::{ComponentCircuitHeaderSubquery, CoreParamsHeaderSubquery}, +}; + +use super::{ + types::{CircuitInputHeaderShard, CircuitInputHeaderSubquery}, + MMR_MAX_NUM_PEAKS, +}; + +/// Return (params, input, promise results) +fn get_test_input() -> Result<(CoreParamsHeaderSubquery, CircuitInputHeaderShard)> { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + let v: Value = serde_json::from_reader( + File::open(format!("{cargo_manifest_dir}/data/test/input_mmr_proof_for_header.json")) + .unwrap(), + )?; + let historical_mmr: Vec = serde_json::from_value(v["historicalMmr"].clone())?; + // let block_hash: H256 = serde_json::from_value(v["blockHash"].clone())?; + // let logs_bloom: Bytes = serde_json::from_value(v["logsBloom"].clone())?; + // dbg!(ðers_core::utils::hex::encode(&logs_bloom[32..64])); + let mmr_proof: Vec = serde_json::from_value(v["mmrProof"].clone())?; + let mmr = [vec![H256::zero(); 10], historical_mmr].concat(); + + let chain_id = 1; + let block_number = 9528813; + + let max_extra_data_bytes = get_block_header_extra_bytes_from_chain_id(chain_id); + let (header_rlp_max_bytes, _) = get_block_header_rlp_max_lens_from_extra(max_extra_data_bytes); + let mut mmr_proof_fixed = [H256::zero(); MMR_MAX_NUM_PEAKS - 1]; + mmr_proof_fixed[..mmr_proof.len()].copy_from_slice(&mmr_proof); + + let provider = setup_provider(Chain::Mainnet); + let mut header_rlp = get_block_rlp_from_num(&provider, block_number); + header_rlp.resize(header_rlp_max_bytes, 0); + + let requests = vec![ + CircuitInputHeaderSubquery { + header_rlp: header_rlp.clone(), + mmr_proof: mmr_proof_fixed, + field_idx: 1, + }, + CircuitInputHeaderSubquery { + header_rlp: header_rlp.clone(), + mmr_proof: mmr_proof_fixed, + field_idx: 71, + }, + CircuitInputHeaderSubquery { + header_rlp, + mmr_proof: mmr_proof_fixed, + field_idx: EXTRA_DATA_INDEX as u32, + }, + ]; + let mut mmr_fixed = [H256::zero(); MMR_MAX_NUM_PEAKS]; + mmr_fixed[..mmr.len()].copy_from_slice(&mmr); + + Ok(( + CoreParamsHeaderSubquery { max_extra_data_bytes, capacity: requests.len() }, + CircuitInputHeaderShard:: { mmr: mmr_fixed, requests, _phantom: PhantomData }, + )) +} + +#[test] +fn test_mock_header_subquery() -> anyhow::Result<()> { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + let k = 18; + let (core_builder_params, input) = get_test_input().unwrap(); + let circuit_params = dummy_rlc_circuit_params(k as usize); + let keccak_capacity = 200; + let mut circuit = ComponentCircuitHeaderSubquery::::new( + core_builder_params, + PromiseLoaderParams { + comp_loader_params: SingleComponentLoaderParams::new(3, vec![keccak_capacity]), + }, + circuit_params, + ); + circuit.feed_input(Box::new(input.clone())).unwrap(); + + let mut promise_results = HashMap::new(); + let promise_keccak = generate_keccak_shards_from_calls(&circuit, keccak_capacity)?; + serde_json::to_writer( + File::create(format!( + "{cargo_manifest_dir}/data/test/header_promise_results_keccak_for_agg.json" + ))?, + &promise_keccak, + )?; + promise_results.insert( + ComponentTypeKeccak::::get_type_id(), + ComponentPromiseResultsInMerkle::from_single_shard(promise_keccak.into_logical_results()), + ); + circuit.calculate_params(); + circuit.fulfill_promise_results(&promise_results)?; + + let instances: Vec = circuit.get_public_instances().into(); + MockProver::run(k as u32, &circuit, vec![instances]).unwrap().assert_satisfied(); + + let comp_params = circuit.params(); + + serde_json::to_writer_pretty( + File::create(format!( + "{cargo_manifest_dir}/configs/test/header_subquery_core_params.json" + ))?, + &comp_params.0, + )?; + + serde_json::to_writer_pretty( + File::create(format!( + "{cargo_manifest_dir}/configs/test/header_subquery_loader_params.json" + ))?, + &comp_params.1, + )?; + + serde_json::to_writer_pretty( + File::create(format!("{cargo_manifest_dir}/data/test/input_header_for_agg.json"))?, + &input, + )?; + circuit.pinning().write("configs/test/header_subquery.json")?; + Ok(()) +} + +pub async fn get_latest_block_number(network: Chain) -> u64 { + let provider = setup_provider(network); + provider.get_block_number().await.unwrap().as_u64() +} diff --git a/axiom-query/src/components/subqueries/block_header/types.rs b/axiom-query/src/components/subqueries/block_header/types.rs new file mode 100644 index 00000000..20603969 --- /dev/null +++ b/axiom-query/src/components/subqueries/block_header/types.rs @@ -0,0 +1,163 @@ +//! Types are separated into: +//! - Circuit metadata that along with the circuit type determines the circuit configuration completely. +//! - Human readable _logical_ input and output to the circuit. These include private inputs and outputs that are only commited to in the public output. +//! - The in-circuit formatted versions of logical inputs and outputs. These include formatting in terms of field elements and accounting for all lengths needing to be fixed at compile time. +//! - We then provide conversion functions from human-readable to circuit formats. +//! - A struct for the public instances (IO) of the circuit, excluding the circuit's own component commitment and the promise commitments from any component calls. +//! - We then specify [TryFrom] and [From] implementations to describe how to "flatten" the public instance struct into a 1d array of field elements. +use std::marker::PhantomData; + +use axiom_codec::{ + types::{field_elements::FieldHeaderSubquery, native::HeaderSubquery}, + HiLo, +}; +use axiom_eth::{ + block_header::{get_block_header_rlp_max_lens_from_extra, GENESIS_BLOCK_RLP}, + halo2_base::AssignedValue, + impl_fix_len_call_witness, + utils::{ + build_utils::dummy::DummyFrom, + component::{ + circuit::CoreBuilderInput, + types::{FixLenLogical, Flatten}, + ComponentType, ComponentTypeId, LogicalResult, + }, + }, +}; +use ethers_core::types::H256; +use serde::{Deserialize, Serialize}; + +use crate::Field; +use crate::{ + components::subqueries::common::OutputSubqueryShard, utils::codec::AssignedHeaderSubquery, +}; + +use super::{circuit::CoreParamsHeaderSubquery, MMR_MAX_NUM_PEAKS}; + +/// Identifier for the component type of this component circuit +pub struct ComponentTypeHeaderSubquery(PhantomData); + +/// Human readable +pub type OutputHeaderShard = OutputSubqueryShard; + +/// Circuit input for a shard of Header subqueries. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CircuitInputHeaderShard { + // pub(crate) chain_id: u64, + /// Merkle Mountain Range of block hashes for blocks `[0, mmr_num_blocks)`, in *increasing* order of peak size. Resized with `H256::zero()` to a fixed max length, known at compile time. + pub mmr: [H256; MMR_MAX_NUM_PEAKS], + /// Enriched subquery requests + pub requests: Vec, + pub _phantom: PhantomData, +} + +/// Circuit input for a single Header subquery. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CircuitInputHeaderSubquery { + /// The full RLP encoded block header, resized to a specified `header_rlp_max_bytes`. + /// Length is known at compile time. + pub header_rlp: Vec, + /// `mmr_proof` is a Merkle proof of this header's `block_hash` into `mmr`. + pub mmr_proof: [H256; MMR_MAX_NUM_PEAKS - 1], + pub field_idx: u32, +} + +impl DummyFrom for CircuitInputHeaderShard { + fn dummy_from(core_params: CoreParamsHeaderSubquery) -> Self { + let CoreParamsHeaderSubquery { max_extra_data_bytes, capacity } = core_params; + + let (header_rlp_max_bytes, _) = + get_block_header_rlp_max_lens_from_extra(max_extra_data_bytes); + + let mut header_rlp = GENESIS_BLOCK_RLP.to_vec(); + header_rlp.resize(header_rlp_max_bytes, 0); + let input_subquery = CircuitInputHeaderSubquery { + header_rlp, + mmr_proof: [H256::zero(); MMR_MAX_NUM_PEAKS - 1], + field_idx: 0, + }; + + CircuitInputHeaderShard { + mmr: [H256::zero(); MMR_MAX_NUM_PEAKS], + requests: vec![input_subquery; capacity], + _phantom: PhantomData, + } + } +} + +/// The output value of any header subquery is always `bytes32` right now. +/// Vector has been resized to the capacity. +pub type CircuitOutputHeaderShard = OutputSubqueryShard, HiLo>; + +impl_fix_len_call_witness!( + FieldHeaderSubqueryCall, + FieldHeaderSubquery, + ComponentTypeHeaderSubquery +); + +/// Size in bits of public instances, excluding component commitments +const BITS_PER_PUBLIC_INSTANCE: [usize; 2] = [128, 128]; +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LogicalPublicInstanceHeader { + pub mmr_keccak: HiLo, +} + +impl ComponentType for ComponentTypeHeaderSubquery { + type InputValue = FieldHeaderSubquery; + type InputWitness = AssignedHeaderSubquery; + type OutputValue = HiLo; + type OutputWitness = HiLo>; + type LogicalInput = FieldHeaderSubquery; + + fn get_type_id() -> ComponentTypeId { + "axiom-query:ComponentTypeHeaderSubquery".to_string() + } + + fn logical_result_to_virtual_rows_impl( + ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + vec![(ins.input, ins.output)] + } + fn logical_input_to_virtual_rows_impl(li: &Self::LogicalInput) -> Vec { + vec![*li] + } +} + +impl From for CircuitOutputHeaderShard { + fn from(output: OutputHeaderShard) -> Self { + output.convert_into() + } +} + +// ============== LogicalPublicInstanceHeader ============== +impl TryFrom> for LogicalPublicInstanceHeader { + type Error = anyhow::Error; + + fn try_from(value: Vec) -> Result { + if value.len() != BITS_PER_PUBLIC_INSTANCE.len() { + return Err(anyhow::anyhow!("incorrect length")); + } + Ok(Self { mmr_keccak: HiLo::from_hi_lo([value[0], value[1]]) }) + } +} + +impl TryFrom> for LogicalPublicInstanceHeader { + type Error = anyhow::Error; + + fn try_from(value: Flatten) -> Result { + if value.field_size != BITS_PER_PUBLIC_INSTANCE { + return Err(anyhow::anyhow!("invalid field size")); + } + value.fields.try_into() + } +} +impl From> for Flatten { + fn from(val: LogicalPublicInstanceHeader) -> Self { + Flatten { fields: val.mmr_keccak.hi_lo().to_vec(), field_size: &BITS_PER_PUBLIC_INSTANCE } + } +} +impl FixLenLogical for LogicalPublicInstanceHeader { + fn get_field_size() -> &'static [usize] { + &BITS_PER_PUBLIC_INSTANCE + } +} diff --git a/axiom-query/src/components/subqueries/common.rs b/axiom-query/src/components/subqueries/common.rs new file mode 100644 index 00000000..0a439dba --- /dev/null +++ b/axiom-query/src/components/subqueries/common.rs @@ -0,0 +1,96 @@ +use axiom_codec::types::field_elements::AnySubqueryResult; +use axiom_eth::{ + halo2_base::AssignedValue, + utils::component::{ + types::{FixLenLogical, Flatten}, + utils::get_logical_value, + ComponentPromiseResultsInMerkle, ComponentType, FlattenVirtualTable, LogicalResult, + }, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::Field; + +/// Generic type for output of a subquery shard circuit +#[derive(Clone, Default, Debug, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OutputSubqueryShard { + /// Vector is assumed to be resized to the capacity. + pub results: Vec>, +} + +impl OutputSubqueryShard { + pub fn len(&self) -> usize { + self.results.len() + } + + pub fn is_empty(&self) -> bool { + self.results.is_empty() + } + + pub fn into_flattened_pairs(self) -> Vec<(Flatten, Flatten)> + where + I: Into>, + O: Into>, + T: Copy, + { + self.results.into_iter().map(|r| (r.subquery.into(), r.value.into())).collect() + } + + // cannot do blanket From implementation because it conflicts with Rust std T: From impl + pub fn convert_into(self) -> OutputSubqueryShard + where + I: Into, + O: Into, + { + OutputSubqueryShard { + results: self + .results + .into_iter() + .map(|r| AnySubqueryResult { subquery: r.subquery.into(), value: r.value.into() }) + .collect(), + } + } +} + +/// Helper function to convert OutputSubqueryShard into ComponentPromiseResults +pub fn shard_into_component_promise_results>( + shard: OutputSubqueryShard, +) -> ComponentPromiseResultsInMerkle { + ComponentPromiseResultsInMerkle::from_single_shard( + shard + .results + .into_iter() + .map(|r| LogicalResult::::new(r.subquery, r.value)) + .collect_vec(), + ) +} + +pub(crate) fn extract_virtual_table< + F: Field, + S: Into>>, + T: Into>>, +>( + outputs: impl Iterator>, +) -> FlattenVirtualTable> { + outputs.map(|output| (output.subquery.into(), output.value.into())).collect() +} + +pub(crate) fn extract_logical_results< + F: Field, + S: FixLenLogical>, + FS: FixLenLogical, + T: ComponentType, +>( + outputs: impl Iterator>, +) -> Vec> { + outputs + .map(|output| { + LogicalResult::::new( + get_logical_value(&output.subquery), + get_logical_value(&output.value), + ) + }) + .collect() +} diff --git a/axiom-query/src/components/subqueries/mod.rs b/axiom-query/src/components/subqueries/mod.rs new file mode 100644 index 00000000..884aafd5 --- /dev/null +++ b/axiom-query/src/components/subqueries/mod.rs @@ -0,0 +1,8 @@ +pub mod account; +pub mod block_header; +/// Types common to all subqueries +pub mod common; +pub mod receipt; +pub mod solidity_mappings; +pub mod storage; +pub mod transaction; diff --git a/axiom-query/src/components/subqueries/receipt/circuit.rs b/axiom-query/src/components/subqueries/receipt/circuit.rs new file mode 100644 index 00000000..1e4e05c9 --- /dev/null +++ b/axiom-query/src/components/subqueries/receipt/circuit.rs @@ -0,0 +1,568 @@ +use std::iter::zip; + +use axiom_codec::{ + constants::FIELD_IDX_BITS, + encoder::field_elements::SUBQUERY_OUTPUT_BYTES, + special_values::{ + RECEIPT_ADDRESS_IDX, RECEIPT_BLOCK_NUMBER_FIELD_IDX, RECEIPT_DATA_IDX_OFFSET, + RECEIPT_LOGS_BLOOM_IDX_OFFSET, RECEIPT_LOG_IDX_OFFSET, RECEIPT_TX_INDEX_FIELD_IDX, + RECEIPT_TX_TYPE_FIELD_IDX, + }, + HiLo, +}; +use axiom_eth::{ + block_header::RECEIPT_ROOT_INDEX, + halo2_base::{ + gates::{flex_gate::threads::parallelize_core, GateInstructions, RangeInstructions}, + safe_types::{SafeBool, SafeByte, SafeTypeChip, VarLenBytesVec}, + utils::bit_length, + AssignedValue, Context, + QuantumCell::Constant, + }, + halo2_proofs::plonk::ConstraintSystem, + keccak::{types::ComponentTypeKeccak, KeccakChip}, + mpt::MPTChip, + receipt::{ + EthReceiptChip, EthReceiptChipParams, EthReceiptLogFieldWitness, EthReceiptLogWitness, + EthReceiptWitness, RECEIPT_NUM_FIELDS, + }, + rlc::circuit::builder::RlcCircuitBuilder, + rlc::circuit::builder::RlcContextPair, + rlp::RlpChip, + utils::{ + build_utils::aggregation::CircuitMetadata, + circuit_utils::{ + bytes::{pack_bytes_to_hilo, select_hi_lo_by_indicator, unsafe_mpt_root_to_hi_lo}, + extract_array_chunk_and_constrain_trailing_zeros, is_equal_usize, is_gte_usize, + is_lt_usize, min_with_usize, unsafe_constrain_trailing_zeros, + }, + component::{ + circuit::{ + ComponentBuilder, ComponentCircuitImpl, CoreBuilder, CoreBuilderOutput, + CoreBuilderOutputParams, CoreBuilderParams, + }, + promise_collector::PromiseCaller, + promise_loader::{combo::PromiseBuilderCombo, single::PromiseLoader}, + types::LogicalEmpty, + utils::create_hasher, + LogicalResult, + }, + constrain_vec_equal, is_zero_vec, unsafe_bytes_to_assigned, + }, +}; +use itertools::{zip_eq, Itertools}; +use serde::{Deserialize, Serialize}; + +use crate::{ + components::subqueries::{ + block_header::{ + circuit::handle_logs_bloom, + types::{ComponentTypeHeaderSubquery, FieldHeaderSubqueryCall}, + }, + common::{extract_logical_results, extract_virtual_table}, + }, + utils::codec::{ + AssignedHeaderSubquery, AssignedReceiptSubquery, AssignedReceiptSubqueryResult, + }, + Field, +}; + +use super::{ + types::{CircuitInputReceiptShard, CircuitInputReceiptSubquery, ComponentTypeReceiptSubquery}, + DUMMY_LOG, +}; + +/// The fieldIdx for cumulativeGas +const CUMULATIVE_GAS_FIELD_IDX: usize = 2; + +pub struct CoreBuilderReceiptSubquery { + input: Option>, + params: CoreParamsReceiptSubquery, + payload: Option<(KeccakChip, Vec>)>, +} + +/// Specify the output format of ReceiptSubquery component. +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct CoreParamsReceiptSubquery { + pub chip_params: EthReceiptChipParams, + /// The maximum number of subqueries of this type allowed in a single circuit. + pub capacity: usize, + /// The maximum depth of the receipt MPT trie supported by this circuit. + /// The depth is defined as the maximum length of a Merkle proof, where the proof always ends in a terminal node (if the proof ends in a branch, we extract the leaf and add it as a separate node). + /// + /// In practice this can always be set to 6, because + /// transaction index is within u16, so rlp(txIndex) is at most 3 bytes => 6 nibbles. + pub max_trie_depth: usize, +} +impl CoreBuilderParams for CoreParamsReceiptSubquery { + fn get_output_params(&self) -> CoreBuilderOutputParams { + CoreBuilderOutputParams::new(vec![self.capacity]) + } +} + +type CKeccak = ComponentTypeKeccak; +type CHeader = ComponentTypeHeaderSubquery; +/// Used for loading receipt subquery promise results. +pub type PromiseLoaderReceiptSubquery = + PromiseBuilderCombo>, PromiseLoader>>; +pub type ComponentCircuitReceiptSubquery = + ComponentCircuitImpl, PromiseLoaderReceiptSubquery>; + +impl CircuitMetadata for CoreBuilderReceiptSubquery { + const HAS_ACCUMULATOR: bool = false; + fn num_instance(&self) -> Vec { + unreachable!() + } +} + +impl ComponentBuilder for CoreBuilderReceiptSubquery { + type Params = CoreParamsReceiptSubquery; + + fn new(params: Self::Params) -> Self { + Self { input: None, params, payload: None } + } + fn get_params(&self) -> Self::Params { + self.params.clone() + } + fn clear_witnesses(&mut self) { + self.payload = None; + } + fn calculate_params(&mut self) -> Self::Params { + self.params.clone() + } + fn configure_with_params(_: &mut ConstraintSystem, _: Self::Params) {} +} + +impl CoreBuilder for CoreBuilderReceiptSubquery { + type CompType = ComponentTypeReceiptSubquery; + type PublicInstanceValue = LogicalEmpty; + type PublicInstanceWitness = LogicalEmpty>; + type CoreInput = CircuitInputReceiptShard; + + fn feed_input(&mut self, input: Self::CoreInput) -> anyhow::Result<()> { + self.input = Some(input); + Ok(()) + } + /// Includes computing the component commitment to the logical output (the subquery results). + /// **In addition** performs _promise calls_ to the Header Component to verify + /// all `(block_number, receipts_root)` pairs as additional "enriched" header subqueries. + /// These are checked against the supplied promise commitment using dynamic lookups + /// (behind the scenes) by `promise_caller`. + fn virtual_assign_phase0( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_caller: PromiseCaller, + ) -> CoreBuilderOutput { + // preamble: to be removed + let keccak = + KeccakChip::new_with_promise_collector(builder.range_chip(), promise_caller.clone()); + let range_chip = keccak.range(); + let rlp = RlpChip::new(range_chip, None); + let mut poseidon = create_hasher(); + poseidon.initialize_consts(builder.base.main(0), keccak.gate()); + + // Assumption: we already have input when calling this function. + // TODO: automatically derive a dummy input from params. + let input = self.input.as_ref().unwrap(); + + let mpt = MPTChip::new(rlp, &keccak); + let chip = EthReceiptChip::new(&mpt, self.params.chip_params); + let base_builder = &mut builder.base; + // actual logic + let payload = + parallelize_core(base_builder.pool(0), input.requests.clone(), |ctx, subquery| { + handle_single_receipt_subquery_phase0(ctx, &chip, &subquery) + }); + + let vt = extract_virtual_table(payload.iter().map(|p| p.output)); + let lr: Vec> = + extract_logical_results(payload.iter().map(|p| p.output)); + + let ctx = base_builder.main(0); + // promise calls to header component: + // - for each block number in a subquery, we must make a promise call to check the transaction root of that block + let header_rc_root_idx = ctx.load_constant(F::from(RECEIPT_ROOT_INDEX as u64)); + for p in payload.iter() { + let block_number = p.output.subquery.block_number; + let rc_root = p.rc_root; + let header_subquery = + AssignedHeaderSubquery { block_number, field_idx: header_rc_root_idx }; + let promise_rc_root = promise_caller + .call::, ComponentTypeHeaderSubquery>( + ctx, + FieldHeaderSubqueryCall(header_subquery), + ) + .unwrap(); + constrain_vec_equal(ctx, &rc_root.hi_lo(), &promise_rc_root.hi_lo()); + } + self.payload = Some((keccak, payload)); + CoreBuilderOutput { public_instances: vec![], virtual_table: vt, logical_results: lr } + } + + fn virtual_assign_phase1(&mut self, builder: &mut RlcCircuitBuilder) { + let (keccak, payload) = self.payload.take().unwrap(); + // preamble + let range_chip = keccak.range(); + let rlc_chip = builder.rlc_chip(&range_chip.gate); + let rlp = RlpChip::new(range_chip, Some(&rlc_chip)); + let mpt = MPTChip::new(rlp, &keccak); + let chip = EthReceiptChip::new(&mpt, self.params.chip_params); + + // actual logic + builder.parallelize_phase1(payload, |(ctx_gate, ctx_rlc), payload| { + handle_single_receipt_subquery_phase1((ctx_gate, ctx_rlc), &chip, payload) + }); + } +} + +pub struct PayloadReceiptSubquery { + pub rc_witness: EthReceiptWitness, + pub parsed_log_witness: EthReceiptLogFieldWitness, + pub rc_root: HiLo>, + pub output: AssignedReceiptSubqueryResult, +} + +/// Assigns `subquery` to virtual cells and then handles the subquery to get result. +/// **Assumes** that the receiptsRoot is verified. Returns the assigned private witnesses of +/// `(block_number, receiptsRoot)`, to be looked up against Header Component promise. +pub fn handle_single_receipt_subquery_phase0( + ctx: &mut Context, + chip: &EthReceiptChip, + subquery: &CircuitInputReceiptSubquery, +) -> PayloadReceiptSubquery { + assert_eq!(chip.params.topic_num_bounds, (0, 4), "Always support all topics"); + let gate = chip.gate(); + let range = chip.range(); + // assign rc proof + let rc_proof = subquery.proof.clone().assign(ctx); + // convert receiptsRoot from bytes to HiLo for later. `parse_receipt_proof` will constrain these witnesses to be bytes + let rc_root = unsafe_mpt_root_to_hi_lo(ctx, gate, &rc_proof.proof); + // Check the receipt MPT proof + let rc_witness = chip.parse_receipt_proof_phase0(ctx, rc_proof); + gate.assert_is_const(ctx, &rc_witness.mpt_witness().slot_is_empty, &F::ZERO); // ensure slot is not empty + + let field_or_log_idx = ctx.load_witness(F::from(subquery.field_or_log_idx as u64)); + range.range_check(ctx, field_or_log_idx, FIELD_IDX_BITS); + let log_threshold = Constant(F::from(RECEIPT_LOG_IDX_OFFSET as u64)); + // if `field_idx` < `RECEIPT_NUM_FIELDS`, then it is an actual tx rlp list item. Note even though `field_idx` has both postState and status, we **do not** allow `field_idx = 4` for logs. That is what `is_log_idx` is for. + let is_idx_in_list = + is_lt_usize(ctx, range, field_or_log_idx, RECEIPT_NUM_FIELDS, FIELD_IDX_BITS); + // The cumulativeGas field is always in a receipt, regardless of EIP-658, whereas we only allow fieldIdx = 0 (status) if the block is after EIP-658 or allow fieldIdx = 1 (postState) if the block is before EIP-658 + let field_idx = gate.select( + ctx, + field_or_log_idx, + Constant(F::from(CUMULATIVE_GAS_FIELD_IDX as u64)), + is_idx_in_list, + ); + // if `field_idx` >= `RECEIPT_LOG_IDX_OFFSET`, then we want a log + // must be log_idx if field_or_log_idx >= RECEIPT_LOG_IDX_OFFSET + let is_log_idx = + is_gte_usize(ctx, range, field_or_log_idx, RECEIPT_LOG_IDX_OFFSET, FIELD_IDX_BITS); + // log_idx = field_or_log_idx - RECEIPT_LOG_IDX_OFFSET, wrapping sub + let mut log_idx = gate.sub(ctx, field_or_log_idx, log_threshold); + log_idx = gate.mul(ctx, log_idx, is_log_idx); + let num_logs = rc_witness.logs.list_len.expect("logs are var len"); + let is_valid_log_idx = range.is_less_than(ctx, log_idx, num_logs, FIELD_IDX_BITS); + let is_log_idx = SafeTypeChip::unsafe_to_bool(gate.and(ctx, is_log_idx, is_valid_log_idx)); + let log_idx = gate.mul(ctx, log_idx, is_log_idx); + + let tx_type = rc_witness.receipt_type; + + let rc_field_bytes = + extract_truncated_field(ctx, range, &rc_witness, field_idx, SUBQUERY_OUTPUT_BYTES); + + let logs_bloom_bytes = &rc_witness.value().field_witness[2].field_cells; + let (logs_bloom_value, is_logs_bloom_idx) = handle_logs_bloom( + ctx, + range, + logs_bloom_bytes, + field_or_log_idx, + RECEIPT_LOGS_BLOOM_IDX_OFFSET, + ); + + // === begin process logs === + // tda = topic_or_data_or_address; too much to type + let tda_idx = ctx.load_witness(F::from(subquery.topic_or_data_or_address_idx as u64)); + range.range_check(ctx, tda_idx, FIELD_IDX_BITS); + let is_topic = is_lt_usize(ctx, range, tda_idx, 4, FIELD_IDX_BITS); + let mut is_topic = gate.and(ctx, is_topic, is_log_idx); + let data_threshold = Constant(F::from(RECEIPT_DATA_IDX_OFFSET as u64)); + let is_data_idx = is_gte_usize(ctx, range, tda_idx, RECEIPT_DATA_IDX_OFFSET, FIELD_IDX_BITS); + let mut is_data_idx = gate.and(ctx, is_data_idx, is_log_idx); + let topic_idx = gate.mul(ctx, tda_idx, is_topic); + let data_idx = gate.sub(ctx, tda_idx, data_threshold); + let data_idx = gate.mul(ctx, data_idx, is_data_idx); + + let log = chip.extract_receipt_log(ctx, &rc_witness, log_idx); + let log_witness = conditional_parse_log_phase0(ctx, chip, log, is_log_idx); + // Get 32 bytes from data + let (data_bytes, is_valid_data) = + extract_data_section(ctx, range, &log_witness, data_idx, SUBQUERY_OUTPUT_BYTES); + is_data_idx = gate.and(ctx, is_data_idx, is_valid_data); + // Get the address + let addr = log_witness.address().to_vec(); + // Select the topic + let topics_bytes = log_witness.topics_bytes(); + assert_eq!(topics_bytes.len(), 4); + let topic_indicator = gate.idx_to_indicator(ctx, topic_idx, 4); + let topic = gate.select_array_by_indicator(ctx, &topics_bytes, &topic_indicator); + let is_valid_topic = + range.is_less_than(ctx, topic_idx, log_witness.num_topics(), FIELD_IDX_BITS); + is_topic = gate.and(ctx, is_topic, is_valid_topic); + // ---- event schema ---- + // if event_schema != bytes32(0) and `is_log_idx`, then we constrain `topic[0] == event_schema` + let event_schema = unsafe_bytes_to_assigned(ctx, subquery.event_schema.as_bytes()); + let no_constrain_event = is_zero_vec(ctx, gate, &event_schema); + let event_diff = + zip_eq(&topics_bytes[0], &event_schema).map(|(&a, &b)| gate.sub(ctx, a, b)).collect_vec(); + let mut event_eq = is_zero_vec(ctx, gate, &event_diff); + event_eq = gate.and(ctx, event_eq, is_log_idx); + let valid_event = gate.or(ctx, no_constrain_event, event_eq); + gate.assert_is_const(ctx, &valid_event, &F::ONE); + // ==== end process logs ==== + + let [is_tx_type, is_block_num, is_tx_idx] = + [RECEIPT_TX_TYPE_FIELD_IDX, RECEIPT_BLOCK_NUMBER_FIELD_IDX, RECEIPT_TX_INDEX_FIELD_IDX] + .map(|x| is_equal_usize(ctx, gate, field_or_log_idx, x)); + let is_addr = is_equal_usize(ctx, gate, tda_idx, RECEIPT_ADDRESS_IDX); + let is_addr = gate.and(ctx, is_addr, is_log_idx); + + let safe = SafeTypeChip::new(range); + let value_indicator = vec![ + is_idx_in_list.into(), + is_tx_type.into(), + is_block_num.into(), + is_tx_idx.into(), + is_logs_bloom_idx.into(), + is_topic, + is_addr, + is_data_idx, + ]; + // it must be exactly one of the above cases + let idx_check = gate.sum(ctx, value_indicator.clone()); + gate.assert_is_const(ctx, &idx_check, &F::ONE); + + let block_number = ctx.load_witness(F::from(subquery.block_number)); + let tx_idx = rc_witness.tx_idx; + let field_hilo = prep_field(ctx, gate, rc_field_bytes, field_idx); + let const_zero = ctx.load_zero(); + let from_lo = |lo| HiLo::from_hi_lo([const_zero, lo]); + + // unsafe because rlp has already constrained these to be bytes + let topic = SafeTypeChip::unsafe_to_fix_len_bytes_vec(topic, 32); + let addr = SafeTypeChip::unsafe_to_fix_len_bytes_vec(addr, 20); + let topic_hilo = pack_bytes_to_hilo(ctx, gate, topic.bytes()); + let addr_hilo = pack_bytes_to_hilo(ctx, gate, addr.bytes()); + let data_hilo = pack_bytes_to_hilo(ctx, gate, &data_bytes); + let hilos = vec![ + field_hilo, + from_lo(tx_type), + from_lo(block_number), + from_lo(tx_idx), + logs_bloom_value, + topic_hilo, + addr_hilo, + data_hilo, + ]; + let value = select_hi_lo_by_indicator(ctx, gate, &hilos, value_indicator); + // dbg!(value.hi_lo().map(|v| *v.value())); + let event_schema = safe.raw_to_fix_len_bytes_vec(ctx, event_schema, 32); + let event_schema = pack_bytes_to_hilo(ctx, gate, event_schema.bytes()); + PayloadReceiptSubquery { + rc_witness, + rc_root, + parsed_log_witness: log_witness, + output: AssignedReceiptSubqueryResult { + subquery: AssignedReceiptSubquery { + block_number, + tx_idx, + field_or_log_idx, + topic_or_data_or_address_idx: tda_idx, + event_schema, + }, + value, + }, + } +} + +pub fn handle_single_receipt_subquery_phase1( + (ctx_gate, ctx_rlc): RlcContextPair, + chip: &EthReceiptChip, + payload: PayloadReceiptSubquery, +) { + chip.parse_receipt_proof_phase1((ctx_gate, ctx_rlc), payload.rc_witness); + conditional_parse_log_phase1((ctx_gate, ctx_rlc), chip, payload.parsed_log_witness); +} + +/// Extracts the field at `field_idx` from the given rlp list decomposition of a transaction. +/// The field is truncated to the first `truncated_byte_len` bytes. +/// +/// We do not use `EthReceiptChip::extract_field` because without the truncation the +/// select operation can be very expensive if the `data` field is very long. +/// +/// We **ignore** `field_idx = 4` (logs) because it is handled separately. +pub fn extract_truncated_field( + ctx: &mut Context, + range: &impl RangeInstructions, + witness: &EthReceiptWitness, + field_idx: AssignedValue, + truncated_byte_len: usize, +) -> VarLenBytesVec { + let gate = range.gate(); + let rc_values = &witness.value().field_witness; + assert_eq!(rc_values.len(), RECEIPT_NUM_FIELDS); + let rc_values = &rc_values[..RECEIPT_NUM_FIELDS - 1]; + // | ReceiptField | `fieldIdx` | + // |------------------------|-------| + // | Status | 0 | + // | PostState | 1 | + // | CumulativeGas | 2 | + // | LogsBloom | 3 | + // | Logs | 4 | + // while the actual list index is: + // + // | `listIdx` | State Field | Type | Bytes | RLP Size (Bytes) | RLP Size (Bits) | + // | --------- | -------------- | --------- | -------- | -------- | -------- | + // | 0 | PostState | bytes32 | 32 | 33 | 264 | + // | 0 | Status | uint64 | $\leq 1$ | $\leq 33$ | $\leq 264$ | + // | 1 | Cumulative Gas | uint256 | $\leq 32$ | $\leq 33$ | $\leq 264$ | + // | 2 | Log Blooms | Bytes | 256 | 259 | 2072 | + // | 3 | Logs | List of Logs | variable | variable | variable | + // + // Before EIP-658, receipts hold the PostState hash (the intermediate state root hash) instead of the Status. + let get_status = gate.is_zero(ctx, field_idx); + let offset = gate.not(ctx, get_status); + let list_idx = gate.sub(ctx, field_idx, offset); + let indicator = gate.idx_to_indicator(ctx, list_idx, RECEIPT_NUM_FIELDS - 1); + let const_zero = ctx.load_zero(); + let mut field_bytes = (0..truncated_byte_len) + .map(|i| { + let entries = rc_values.iter().map(|w| *w.field_cells.get(i).unwrap_or(&const_zero)); + gate.select_by_indicator(ctx, entries, indicator.clone()) + }) + .collect_vec(); + let lens = rc_values.iter().map(|w| w.field_len); + let mut len = gate.select_by_indicator(ctx, lens, indicator); + // len = min(len, truncated_byte_len) + let max_bytes = rc_values.iter().map(|w| w.field_cells.len()).max().unwrap(); + let max_bits = bit_length(max_bytes as u64); + len = min_with_usize(ctx, range, len, truncated_byte_len, max_bits); + + unsafe_constrain_trailing_zeros(ctx, gate, &mut field_bytes, len); + + // constrain that postState is 32 bytes and status is less than 32 bytes + let is_post_state_or_status = gate.is_zero(ctx, list_idx); + let is_small = range.is_less_than_safe(ctx, len, 32); + // if is_post_state_or_status, then is_small and get_status must match + let diff = gate.sub(ctx, is_small, get_status); + let status_check = gate.mul(ctx, is_post_state_or_status, diff); + ctx.constrain_equal(&status_check, &const_zero); + + SafeTypeChip::unsafe_to_var_len_bytes_vec(field_bytes, len, truncated_byte_len) +} + +fn prep_field( + ctx: &mut Context, + gate: &impl GateInstructions, + field_bytes: VarLenBytesVec, + field_idx: AssignedValue, +) -> HiLo> { + let left_pad_indicator = [true, false, true, false, false].map(F::from).map(Constant); + let field_fixed = field_bytes.left_pad_to_fixed(ctx, gate); + let left_pad = gate.select_from_idx(ctx, left_pad_indicator, field_idx); + let value = zip(field_bytes.bytes(), field_fixed.bytes()) + .map(|(var, fixed)| gate.select(ctx, *fixed, *var, left_pad)) + .collect_vec(); + let value = SafeTypeChip::unsafe_to_fix_len_bytes_vec(value, SUBQUERY_OUTPUT_BYTES); + pack_bytes_to_hilo(ctx, gate, value.bytes()) +} + +/// Extracts a chunk of `log_data[data_idx * chunk_size.. (data_idx + 1) * chunk_size]` +/// and constrains trailing zeros. +/// Returns a flag indicating whether `data_idx * chunk_size < data_len`. +/// +/// Note: select operation can be very expensive if the `data` field is very long. +pub fn extract_data_section( + ctx: &mut Context, + range: &impl RangeInstructions, + witness: &EthReceiptLogFieldWitness, + data_idx: AssignedValue, + chunk_size: usize, +) -> (Vec>, SafeBool) { + let (chunk, is_valid) = extract_array_chunk_and_constrain_trailing_zeros( + ctx, + range, + witness.data_bytes(), + witness.data_len(), + data_idx, + chunk_size, + FIELD_IDX_BITS, + ); + let chunk = chunk.into_iter().map(SafeTypeChip::unsafe_to_byte).collect(); + (chunk, is_valid) +} + +/// The `witness` might not have a valid log. +/// When `parse_log_flag` is false, we parse a dummy log. +/// +/// # Assumptions +/// - When `parse_log_flag` is true, `witness` has a valid log. +pub fn conditional_parse_log_phase0( + ctx: &mut Context, + chip: &EthReceiptChip, + mut witness: EthReceiptLogWitness, + parse_log_flag: SafeBool, +) -> EthReceiptLogFieldWitness { + let gate = chip.gate(); + let log = &mut witness.log_bytes; + // we zip here because the RLP will parse based on the prefix so it should not matter what dummy values are beyond `DUMMY_LOG.len()` + for (byte, dummy_byte) in log.iter_mut().zip(DUMMY_LOG) { + let dummy_byte = F::from(dummy_byte as u64); + *byte = gate.select(ctx, *byte, Constant(dummy_byte), parse_log_flag); + } + parse_log_phase0(ctx, chip, witness) +} + +pub fn conditional_parse_log_phase1( + (ctx_gate, ctx_rlc): RlcContextPair, + chip: &EthReceiptChip, + witness: EthReceiptLogFieldWitness, +) { + parse_log_phase1((ctx_gate, ctx_rlc), chip, witness); +} + +/// ### Log Fields +/// +/// | State Field | Type | Bytes | RLP Size (Bytes) | RLP Size (Bits) | +/// | -------- | -------- | -------- | -------- | -------- | +/// | Address | address hash | 20 | 21 | 168 | +/// | List of 0 to 4 Topics | bytes32 | 32 | 33 | 264 | +/// | data | Bytes | variable | variable | variable | +pub fn parse_log_phase0( + ctx_gate: &mut Context, + chip: &EthReceiptChip, + witness: EthReceiptLogWitness, +) -> EthReceiptLogFieldWitness { + let (_, max_topics) = chip.params.topic_num_bounds; // in practice this will always be 4 + let max_data_byte_len = chip.params.max_data_byte_len; + let field_lengths = [20, max_topics * 33 + 3, max_data_byte_len]; + let log_list = + chip.rlp().decompose_rlp_array_phase0(ctx_gate, witness.log_bytes, &field_lengths, false); + let topics = log_list.field_witness[1].clone(); + let topics_list = chip.rlp().decompose_rlp_array_phase0( + ctx_gate, + topics.encoded_item, + &vec![32; max_topics], + true, + ); + EthReceiptLogFieldWitness { log_list, topics_list } +} + +pub fn parse_log_phase1( + (ctx_gate, ctx_rlc): RlcContextPair, + chip: &EthReceiptChip, + witness: EthReceiptLogFieldWitness, +) { + chip.rlp().decompose_rlp_array_phase1((ctx_gate, ctx_rlc), witness.log_list, false); + chip.rlp().decompose_rlp_array_phase1((ctx_gate, ctx_rlc), witness.topics_list, true); +} diff --git a/axiom-query/src/components/subqueries/receipt/mod.rs b/axiom-query/src/components/subqueries/receipt/mod.rs new file mode 100644 index 00000000..af7591ee --- /dev/null +++ b/axiom-query/src/components/subqueries/receipt/mod.rs @@ -0,0 +1,38 @@ +/*! +# Receipt Subqueries Circuit + +Receipt +* Raw EVM receipt layout reference: +* `blockNumber` (uint32) +* `txIdx` (uint16) +* `fieldOrLogIdx` (uint32) + * If in `[0, 4)` -- refers to the field. + * If the `fieldIdx = 3` corresponds to `logsBloom`, the `result` will be only the first 32 bytes. We will add a special `LOGS_BLOOM_FIELD_IDX` (70) so that if `fieldIdx = LOGS_BLOOM_FIELD_IDX + logsBloomIdx`, the result will be bytes `[32 * logsBloomIdx, 32 * logsBloomIdx + 32)` for `logsBloomIdx` in `[0, 8)`. + * The `fieldIdx` is defined as an enum: + | ReceiptField | `fieldIdx` | + |------------------------|-------| + | Status | 0 | + | PostState | 1 | + | CumulativeGas | 2 | + | LogsBloom | 3 | + | Logs | 4 | + * If in `[100, infty)` -- represents `100 + logIdx` + * As with Transaction, we will have a special `RECEIPT_TX_TYPE_FIELD_IDX` (51) for transaction type, `RECEIPT_BLOCK_NUMBER_FIELD_IDX` (52) for block number, and `RECEIPT_TX_IDX_FIELD_IDX` (53) for transaction index. + * [Nice to have **(not yet supported)**] We will later add a special `RECEIPT_TX_HASH_FIELD_IDX` (54) for the transaction hash. +* `topicOrDataOrAddressIdx` (uint32) + * If in `[0, 4)` -- refers to the topic. + * If equal to `50` -- refers to the address. + * If in `[100, infty)` -- represents `100 + dataIdx`, where for a given value of `dataIdx` we return bytes `[32 * dataIdx, 32 * dataIdx + 32)` of the data. This byte alignment is to return chunks of the ABI encoding. +* `eventSchema` (bytes32) -- Either `bytes32(0x0)` in which case it is a no-op, or the query **must** have `fieldOrLogIdx` in `[100, infty)` and constrains `topic[0]` of the log to equal `eventSchema`. + +*/ + +/// Circuit and Component Implementation. +pub mod circuit; +/// Types +pub mod types; + +#[cfg(test)] +pub mod tests; + +const DUMMY_LOG: [u8; 4] = [0xc3, 0x80, 0xc0, 0x80]; diff --git a/axiom-query/src/components/subqueries/receipt/tests.rs b/axiom-query/src/components/subqueries/receipt/tests.rs new file mode 100644 index 00000000..3a246b94 --- /dev/null +++ b/axiom-query/src/components/subqueries/receipt/tests.rs @@ -0,0 +1,368 @@ +use std::{marker::PhantomData, str::FromStr}; + +use anyhow::anyhow; +use axiom_codec::{ + special_values::{ + RECEIPT_BLOCK_NUMBER_FIELD_IDX, RECEIPT_DATA_IDX_OFFSET, RECEIPT_LOGS_BLOOM_IDX_OFFSET, + RECEIPT_LOG_IDX_OFFSET, RECEIPT_TOPIC_IDX_OFFSET, RECEIPT_TX_INDEX_FIELD_IDX, + RECEIPT_TX_TYPE_FIELD_IDX, + }, + types::{ + field_elements::AnySubqueryResult, + native::{HeaderSubquery, ReceiptSubquery}, + }, +}; +use axiom_eth::{ + block_header::RECEIPT_ROOT_INDEX, + halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr}, + keccak::{promise::generate_keccak_shards_from_calls, types::ComponentTypeKeccak}, + mpt::MPTInput, + providers::{get_provider_uri, setup_provider}, + receipt::{calc_max_val_len, EthReceiptChipParams, EthReceiptInput}, + utils::component::{ + promise_loader::single::PromiseLoaderParams, ComponentCircuit, + ComponentPromiseResultsInMerkle, ComponentType, + }, +}; +use cita_trie::Trie; +use ethers_core::types::{Chain, TransactionReceipt, H256}; +use ethers_providers::Middleware; +use futures::future::join_all; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use tokio; + +use crate::components::{ + dummy_rlc_circuit_params, + subqueries::{ + block_header::types::{ComponentTypeHeaderSubquery, OutputHeaderShard}, + common::shard_into_component_promise_results, + transaction::types::get_tx_key_from_index, + }, +}; + +use super::{ + circuit::{ComponentCircuitReceiptSubquery, CoreParamsReceiptSubquery}, + types::{ + construct_rc_tries_from_full_blocks, BlockWithReceipts, CircuitInputReceiptShard, + CircuitInputReceiptSubquery, + }, +}; + +/// transaction index is within u16, so rlp(txIndex) is at most 3 bytes => 6 nibbles +pub const RECEIPT_PROOF_MAX_DEPTH: usize = 6; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct Params { + block_number: String, +} + +#[derive(Serialize)] +struct Request { + id: u8, + jsonrpc: String, + method: String, + params: Vec, +} + +#[derive(Deserialize)] +struct Response { + // id: u8, + // jsonrpc: String, + result: ResponseBody, +} +#[derive(Deserialize)] +struct ResponseBody { + receipts: Vec, +} + +async fn test_mock_receipt_subqueries( + k: u32, + network: Chain, + subqueries: Vec<(&str, usize, usize, &str)>, // txHash, field_or_log_idx, topic_or_data_or_address_idx, event_schema + max_data_byte_len: usize, + max_log_num: usize, +) -> ComponentCircuitReceiptSubquery { + let _ = env_logger::builder().is_test(true).try_init(); + + let _provider = setup_provider(network); + let provider = &_provider; + let requests = join_all(subqueries.into_iter().map( + |(tx_hash, field_idx, tda_idx, event_schema)| async move { + let tx_hash = H256::from_str(tx_hash).unwrap(); + let event_schema = H256::from_str(event_schema).unwrap(); + let tx = provider.get_transaction(tx_hash).await.unwrap().unwrap(); + // let rc = provider.get_transaction_receipt(tx_hash).await.unwrap().unwrap(); + // dbg!(rc.logs_bloom); + let block_number = tx.block_number.unwrap().as_u32(); + let tx_idx = tx.transaction_index.unwrap().as_u32() as u16; + ReceiptSubquery { + block_number, + tx_idx, + field_or_log_idx: field_idx as u32, + topic_or_data_or_address_idx: tda_idx as u32, + event_schema, + } + }, + )) + .await; + + // assuming this is Alchemy for now + let _provider_uri = get_provider_uri(network); + let provider_uri = _provider_uri.as_str(); // async moves are weird + let _client = reqwest::Client::new(); + let client = &_client; + let block_nums = requests.iter().map(|r| r.block_number as u64).sorted().dedup().collect_vec(); + let blocks = join_all(block_nums.iter().map(|&block_num| async move { + let block = provider.get_block(block_num).await.unwrap().unwrap(); + let req = Request { + id: 1, + jsonrpc: "2.0".to_string(), + method: "alchemy_getTransactionReceipts".to_string(), + params: vec![Params { block_number: format!("0x{block_num:x}") }], + }; + // println!("{}", serde_json::to_string(&req).unwrap()); + let res = client + .post(provider_uri) + .json(&req) + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + BlockWithReceipts { + number: block_num.into(), + receipts_root: block.receipts_root, + receipts: res.result.receipts, + } + })) + .await; + + let chip_params = EthReceiptChipParams { + max_data_byte_len, + max_log_num, + topic_num_bounds: (0, 4), + network: None, + }; + let receipt_rlp_max_byte_len = + calc_max_val_len(max_data_byte_len, max_log_num, chip_params.topic_num_bounds); + + let rc_tries = construct_rc_tries_from_full_blocks(blocks.clone()).unwrap(); + let mut requests_in_circuit = Vec::with_capacity(requests.len()); + for subquery in requests { + let block_number = subquery.block_number as u64; + let tx_idx = subquery.tx_idx as usize; + let tx_key = get_tx_key_from_index(tx_idx); + let db = rc_tries + .get(&block_number) + .ok_or_else(|| { + anyhow!("Subquery block number {block_number} not in provided full blocks") + }) + .unwrap(); + let trie = &db.trie; + let rc_rlps = &db.rc_rlps; + let proof = trie.get_proof(&tx_key).unwrap(); + let value = rc_rlps + .get(tx_idx) + .ok_or_else(|| anyhow!("Receipt index {tx_idx} not in block {block_number}")) + .unwrap(); + let mpt_proof = MPTInput { + path: (&tx_key).into(), + value: value.to_vec(), + root_hash: db.root, + proof, + slot_is_empty: false, + value_max_byte_len: receipt_rlp_max_byte_len, + max_depth: RECEIPT_PROOF_MAX_DEPTH, + max_key_byte_len: 3, + key_byte_len: Some(tx_key.len()), + }; + let rc_proof = EthReceiptInput { idx: tx_idx, proof: mpt_proof }; + requests_in_circuit.push(CircuitInputReceiptSubquery { + block_number, + proof: rc_proof, + field_or_log_idx: subquery.field_or_log_idx, + topic_or_data_or_address_idx: subquery.topic_or_data_or_address_idx, + event_schema: subquery.event_schema, + }); + } + + let promise_header = OutputHeaderShard { + results: blocks + .iter() + .map(|block| AnySubqueryResult { + subquery: HeaderSubquery { + block_number: block.number.as_u32(), + field_idx: RECEIPT_ROOT_INDEX as u32, + }, + value: block.receipts_root, + }) + .collect(), + }; + let keccak_f_capacity = 200; + let header_capacity = promise_header.len(); + + let circuit_params = dummy_rlc_circuit_params(k as usize); + let mut circuit = ComponentCircuitReceiptSubquery::new( + CoreParamsReceiptSubquery { + chip_params, + capacity: requests_in_circuit.len(), + max_trie_depth: RECEIPT_PROOF_MAX_DEPTH, + }, + ( + PromiseLoaderParams::new_for_one_shard(keccak_f_capacity), + PromiseLoaderParams::new_for_one_shard(header_capacity), + ), + circuit_params, + ); + + let input = + CircuitInputReceiptShard:: { requests: requests_in_circuit, _phantom: PhantomData }; + circuit.feed_input(Box::new(input)).unwrap(); + + circuit.calculate_params(); + let promises = [ + ( + ComponentTypeKeccak::::get_type_id(), + ComponentPromiseResultsInMerkle::from_single_shard( + generate_keccak_shards_from_calls(&circuit, keccak_f_capacity) + .unwrap() + .into_logical_results(), + ), + ), + ( + ComponentTypeHeaderSubquery::::get_type_id(), + shard_into_component_promise_results::>( + promise_header.into(), + ), + ), + ] + .into_iter() + .collect(); + circuit.fulfill_promise_results(&promises).unwrap(); + let instances: Vec = circuit.get_public_instances().into(); + MockProver::run(k, &circuit, vec![instances]).unwrap().assert_satisfied(); + circuit +} + +#[tokio::test] +async fn test_mock_receipt_subqueries_simple() { + let k = 18; + let zero = "0x0000000000000000000000000000000000000000000000000000000000000000"; + let subqueries = vec![ + ("0xa85fb48c6cd0b6013c91a3ea93ef73cd3c39845eb258f1d82ef4210c223594f4", 0, 0, zero), // status = fail + ( + "0xc830e27d1bbfc0ea7f9a86f3debb5d6c6105a6585a44589734b12cb678f843c4", + RECEIPT_LOGS_BLOOM_IDX_OFFSET + 1, + 0, + zero, + ), + ( + "0xc830e27d1bbfc0ea7f9a86f3debb5d6c6105a6585a44589734b12cb678f843c4", + RECEIPT_LOG_IDX_OFFSET, + RECEIPT_TOPIC_IDX_OFFSET + 1, + "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c", // Deposit (index_topic_1 address dst, uint256 wad) + ), + ( + "0xc830e27d1bbfc0ea7f9a86f3debb5d6c6105a6585a44589734b12cb678f843c4", + RECEIPT_LOG_IDX_OFFSET + 1, + RECEIPT_TOPIC_IDX_OFFSET + 1, + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", // Transfer (index_topic_1 address src, index_topic_2 address dst, uint256 wad) + ), + ( + "0xc830e27d1bbfc0ea7f9a86f3debb5d6c6105a6585a44589734b12cb678f843c4", + RECEIPT_LOG_IDX_OFFSET + 1, + RECEIPT_DATA_IDX_OFFSET, + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + ), + ]; + test_mock_receipt_subqueries(k, Chain::Mainnet, subqueries, 200, 10).await; +} + +#[tokio::test] +async fn test_mock_receipt_subqueries_pre_eip658() { + let k = 18; + let zero = "0x0000000000000000000000000000000000000000000000000000000000000000"; + let subqueries = vec![ + ("0x5be4ebff5bb19b012d8db932e629eda0af068e392758719353db5e0895147f8e", 1, 0, zero), // postState + ("0x5be4ebff5bb19b012d8db932e629eda0af068e392758719353db5e0895147f8e", 2, 0, zero), // cumulativeGas + ("0x5be4ebff5bb19b012d8db932e629eda0af068e392758719353db5e0895147f8e", 3, 0, zero), // logsBloom + ( + "0x5be4ebff5bb19b012d8db932e629eda0af068e392758719353db5e0895147f8e", + RECEIPT_BLOCK_NUMBER_FIELD_IDX, + 0, + zero, + ), + ( + "0x5be4ebff5bb19b012d8db932e629eda0af068e392758719353db5e0895147f8e", + RECEIPT_TX_TYPE_FIELD_IDX, + 0, + zero, + ), + ( + "0x5be4ebff5bb19b012d8db932e629eda0af068e392758719353db5e0895147f8e", + RECEIPT_TX_INDEX_FIELD_IDX, + 0, + zero, + ), + ( + "0x5be4ebff5bb19b012d8db932e629eda0af068e392758719353db5e0895147f8e", + RECEIPT_LOGS_BLOOM_IDX_OFFSET + 7, + 0, + zero, + ), + ( + "0x5be4ebff5bb19b012d8db932e629eda0af068e392758719353db5e0895147f8e", + RECEIPT_LOG_IDX_OFFSET, + 0, + zero, + ), + ( + "0x5be4ebff5bb19b012d8db932e629eda0af068e392758719353db5e0895147f8e", + RECEIPT_LOG_IDX_OFFSET, + 50, + zero, + ), + ( + "0x5be4ebff5bb19b012d8db932e629eda0af068e392758719353db5e0895147f8e", + RECEIPT_LOG_IDX_OFFSET, + 100 + 63, + zero, + ), + ( + "0x5be4ebff5bb19b012d8db932e629eda0af068e392758719353db5e0895147f8e", + RECEIPT_LOG_IDX_OFFSET, + 100 + 30, + "0x92ca3a80853e6663fa31fa10b99225f18d4902939b4c53a9caae9043f6efd004", + ), + ]; + test_mock_receipt_subqueries(k, Chain::Mainnet, subqueries, 2048, 2).await; +} + +#[cfg(feature = "keygen")] +#[tokio::test] +#[ignore] +async fn test_generate_receipt_shard_pk() { + use axiom_eth::halo2_base::utils::{fs::read_params, halo2::ProvingKeyGenerator}; + + use crate::{global_constants::RECEIPT_TOPIC_BOUNDS, keygen::shard::ShardIntentReceipt}; + + let core_params = CoreParamsReceiptSubquery { + chip_params: EthReceiptChipParams { + max_data_byte_len: 256, + max_log_num: 10, + topic_num_bounds: RECEIPT_TOPIC_BOUNDS, + network: None, + }, + capacity: 4, + max_trie_depth: RECEIPT_PROOF_MAX_DEPTH, + }; + let loader_params = + (PromiseLoaderParams::new_for_one_shard(200), PromiseLoaderParams::new_for_one_shard(8)); + let k = 18; + let intent = ShardIntentReceipt { core_params, loader_params, k, lookup_bits: 8 }; + let kzg_params = read_params(k); + intent.create_pk_and_pinning(&kzg_params); +} diff --git a/axiom-query/src/components/subqueries/receipt/types.rs b/axiom-query/src/components/subqueries/receipt/types.rs new file mode 100644 index 00000000..98f6d6a3 --- /dev/null +++ b/axiom-query/src/components/subqueries/receipt/types.rs @@ -0,0 +1,195 @@ +//! Types are separated into: +//! - Circuit metadata that along with the circuit type determines the circuit configuration completely. +//! - Human readable _logical_ input and output to the circuit. These include private inputs and outputs that are only commited to in the public output. +//! - The in-circuit formatted versions of logical inputs and outputs. These include formatting in terms of field elements and accounting for all lengths needing to be fixed at compile time. +//! - We then provide conversion functions from human-readable to circuit formats. +//! - This circuit has no public instances (IO) other than the circuit's own component commitment and the promise commitments from any component calls. +use std::{collections::HashMap, marker::PhantomData, sync::Arc}; + +use anyhow::Result; +use axiom_codec::{ + types::{field_elements::FieldReceiptSubquery, native::ReceiptSubquery}, + HiLo, +}; +use axiom_eth::{ + halo2_base::AssignedValue, + impl_fix_len_call_witness, + mpt::MPTInput, + providers::receipt::rlp_bytes, + receipt::{calc_max_val_len as rc_calc_max_val_len, EthReceiptInput}, + utils::{ + build_utils::dummy::DummyFrom, + component::{circuit::CoreBuilderInput, ComponentType, ComponentTypeId, LogicalResult}, + }, +}; +use cita_trie::{MemoryDB, PatriciaTrie, Trie}; +use ethers_core::types::{TransactionReceipt, H256, U64}; +use hasher::HasherKeccak; +use serde::{Deserialize, Serialize}; + +use crate::{ + components::subqueries::{ + common::OutputSubqueryShard, transaction::types::get_tx_key_from_index, + }, + utils::codec::AssignedReceiptSubquery, + Field, +}; + +use super::circuit::CoreParamsReceiptSubquery; + +/// Identifier for the component type of this component circuit +pub struct ComponentTypeReceiptSubquery(PhantomData); + +/// Human readable. +/// The output value of any transaction subquery is always `bytes32` right now. +pub type OutputReceiptShard = OutputSubqueryShard; + +/// Circuit input for a shard of Receipt subqueries. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CircuitInputReceiptShard { + /// Enriched subquery requests + pub requests: Vec, + pub _phantom: PhantomData, +} + +/// Circuit input for a single Receipt subquery. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CircuitInputReceiptSubquery { + /// The block number to access the storage state at. + pub block_number: u64, + /// Transaction proof formatted as [axiom_eth::mpt::MPTInput]. Contains the transaction index. + pub proof: EthReceiptInput, + /// Special index to specify what subquery value to extract from the transaction. + pub field_or_log_idx: u32, + pub topic_or_data_or_address_idx: u32, + pub event_schema: H256, +} + +impl DummyFrom for CircuitInputReceiptShard { + fn dummy_from(core_params: CoreParamsReceiptSubquery) -> Self { + let CoreParamsReceiptSubquery { chip_params, capacity, max_trie_depth } = core_params; + let mut trie = + PatriciaTrie::new(Arc::new(MemoryDB::new(true)), Arc::new(HasherKeccak::new())); + let rc = TransactionReceipt { status: Some(0x1.into()), ..Default::default() }; + let rc_rlp = rlp_bytes(rc); + trie.insert(vec![0x80], rc_rlp.clone()).unwrap(); + let mpt_input = MPTInput { + path: (&[0x80]).into(), + value: rc_rlp, + root_hash: Default::default(), + proof: trie.get_proof(&[0x80]).unwrap(), + value_max_byte_len: rc_calc_max_val_len( + chip_params.max_data_byte_len, + chip_params.max_log_num, + chip_params.topic_num_bounds, + ), + max_depth: max_trie_depth, + slot_is_empty: false, + max_key_byte_len: 3, + key_byte_len: Some(1), + }; + let rc_pf = EthReceiptInput { idx: 0, proof: mpt_input }; + let dummy_subquery = CircuitInputReceiptSubquery { + block_number: 0, + proof: rc_pf, + field_or_log_idx: 0, + topic_or_data_or_address_idx: 0, + event_schema: H256::zero(), + }; + Self { requests: vec![dummy_subquery; capacity], _phantom: PhantomData } + } +} + +/// The output value of any storage subquery is always `bytes32` right now. +/// Vector has been resized to the capacity. +pub type CircuitOutputReceiptShard = OutputSubqueryShard, HiLo>; + +impl_fix_len_call_witness!( + FieldReceiptSubqueryCall, + FieldReceiptSubquery, + ComponentTypeReceiptSubquery +); + +// ===== The storage component has no public instances other than the component commitment and promise commitments from external component calls ===== + +impl ComponentType for ComponentTypeReceiptSubquery { + type InputValue = FieldReceiptSubquery; + type InputWitness = AssignedReceiptSubquery; + type OutputValue = HiLo; + type OutputWitness = HiLo>; + type LogicalInput = FieldReceiptSubquery; + + fn get_type_id() -> ComponentTypeId { + "axiom-query:ComponentTypeReceiptSubquery".to_string() + } + + fn logical_result_to_virtual_rows_impl( + ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + vec![(ins.input, ins.output)] + } + fn logical_input_to_virtual_rows_impl(li: &Self::LogicalInput) -> Vec { + vec![*li] + } +} + +impl From for CircuitOutputReceiptShard { + fn from(output: OutputReceiptShard) -> Self { + output.convert_into() + } +} + +// ===== Block with Receipts ===== +/// A block with all receipts. We require the receiptsRoot to be provided for a safety check. +/// Deserialization should still work on an object with extra fields. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BlockWithReceipts { + /// Block number + pub number: U64, + /// Receipts root hash + pub receipts_root: H256, + /// All receipts in the block + pub receipts: Vec, +} + +pub struct BlockReceiptsDb { + pub trie: PatriciaTrie, + pub root: H256, + pub rc_rlps: Vec>, +} + +impl BlockReceiptsDb { + pub fn new( + trie: PatriciaTrie, + root: H256, + rc_rlps: Vec>, + ) -> Self { + Self { trie, root, rc_rlps } + } +} + +pub fn construct_rc_tries_from_full_blocks( + blocks: Vec, +) -> Result> { + let mut tries = HashMap::new(); + for block in blocks { + let mut trie = + PatriciaTrie::new(Arc::new(MemoryDB::new(true)), Arc::new(HasherKeccak::new())); + let mut rc_rlps = Vec::with_capacity(block.receipts.len()); + for (idx, rc) in block.receipts.into_iter().enumerate() { + let tx_key = get_tx_key_from_index(idx); + let rc_rlp = rlp_bytes(rc); + rc_rlps.push(rc_rlp.clone()); + trie.insert(tx_key, rc_rlp)?; + } + // safety check: + let root = trie.root()?; + if root != block.receipts_root.as_bytes() { + anyhow::bail!("Transactions trie incorrectly constructed"); + } + let root = block.receipts_root; + tries.insert(block.number.as_u64(), BlockReceiptsDb::new(trie, root, rc_rlps)); + } + Ok(tries) +} diff --git a/axiom-query/src/components/subqueries/solidity_mappings/circuit.rs b/axiom-query/src/components/subqueries/solidity_mappings/circuit.rs new file mode 100644 index 00000000..1f02538f --- /dev/null +++ b/axiom-query/src/components/subqueries/solidity_mappings/circuit.rs @@ -0,0 +1,240 @@ +use axiom_codec::{ + constants::MAX_SOLIDITY_MAPPING_KEYS, + types::field_elements::FieldSolidityNestedMappingSubquery, HiLo, +}; +use axiom_eth::{ + halo2_base::{ + gates::flex_gate::threads::parallelize_core, safe_types::SafeBytes32, AssignedValue, + Context, + }, + halo2_proofs::plonk::ConstraintSystem, + keccak::{types::ComponentTypeKeccak, KeccakChip}, + mpt::MPTChip, + rlc::circuit::builder::RlcCircuitBuilder, + rlc::circuit::builder::RlcContextPair, + rlp::RlpChip, + solidity::{ + types::{NestedMappingWitness, SolidityType}, + SolidityChip, + }, + utils::{ + build_utils::aggregation::CircuitMetadata, + circuit_utils::bytes::safe_bytes32_to_hi_lo, + component::{ + circuit::{ + ComponentBuilder, ComponentCircuitImpl, CoreBuilder, CoreBuilderOutput, + CoreBuilderOutputParams, CoreBuilderParams, + }, + promise_collector::PromiseCaller, + promise_loader::{combo::PromiseBuilderCombo, single::PromiseLoader}, + types::LogicalEmpty, + utils::{create_hasher, get_logical_value}, + LogicalResult, + }, + uint_to_bytes_be, + }, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + components::subqueries::storage::types::{ + ComponentTypeStorageSubquery, FieldStorageSubqueryCall, + }, + utils::codec::{AssignedSolidityNestedMappingSubquery, AssignedStorageSubquery}, + Field, +}; + +use super::types::{ + CircuitInputSolidityNestedMappingShard, ComponentTypeSolidityNestedMappingSubquery, +}; + +pub struct CoreBuilderSolidityNestedMappingSubquery { + input: Option>, + params: CoreParamsSolidityNestedMappingSubquery, + payload: Option<(KeccakChip, Vec>)>, +} + +/// Specify the output format of SolidityNestedMappingSubquery component. +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct CoreParamsSolidityNestedMappingSubquery { + pub capacity: usize, +} +impl CoreBuilderParams for CoreParamsSolidityNestedMappingSubquery { + fn get_output_params(&self) -> CoreBuilderOutputParams { + CoreBuilderOutputParams::new(vec![self.capacity]) + } +} + +type CKeccak = ComponentTypeKeccak; +type CStorage = ComponentTypeStorageSubquery; +/// Used for loading solidity nested mapping promise results. +pub type PromiseLoaderSolidityNestedMappingSubquery = + PromiseBuilderCombo>, PromiseLoader>>; +pub type ComponentCircuitSolidityNestedMappingSubquery = ComponentCircuitImpl< + F, + CoreBuilderSolidityNestedMappingSubquery, + PromiseLoaderSolidityNestedMappingSubquery, +>; + +impl CircuitMetadata for CoreBuilderSolidityNestedMappingSubquery { + const HAS_ACCUMULATOR: bool = false; + fn num_instance(&self) -> Vec { + unreachable!() + } +} + +impl ComponentBuilder for CoreBuilderSolidityNestedMappingSubquery { + type Params = CoreParamsSolidityNestedMappingSubquery; + + fn new(params: Self::Params) -> Self { + Self { input: None, params, payload: None } + } + fn get_params(&self) -> Self::Params { + self.params.clone() + } + fn clear_witnesses(&mut self) { + self.payload = None; + } + fn calculate_params(&mut self) -> Self::Params { + self.params.clone() + } + fn configure_with_params(_: &mut ConstraintSystem, _: Self::Params) {} +} +impl CoreBuilder for CoreBuilderSolidityNestedMappingSubquery { + type CompType = ComponentTypeSolidityNestedMappingSubquery; + type PublicInstanceValue = LogicalEmpty; + type PublicInstanceWitness = LogicalEmpty>; + type CoreInput = CircuitInputSolidityNestedMappingShard; + + fn feed_input(&mut self, input: Self::CoreInput) -> anyhow::Result<()> { + self.input = Some(input); + Ok(()) + } + fn virtual_assign_phase0( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_caller: PromiseCaller, + ) -> CoreBuilderOutput { + // preamble: to be removed + let keccak = + KeccakChip::new_with_promise_collector(builder.range_chip(), promise_caller.clone()); + let range_chip = keccak.range(); + let rlp = RlpChip::new(range_chip, None); + let mut poseidon = create_hasher(); + poseidon.initialize_consts(builder.base.main(0), keccak.gate()); + + // Assumption: we already have input when calling this function. + // TODO: automatically derive a dummy input from params. + let input = self.input.as_ref().unwrap(); + + let mpt = MPTChip::new(rlp, &keccak); + let chip = SolidityChip::new(&mpt, MAX_SOLIDITY_MAPPING_KEYS, 32); + let pool = &mut builder.base.pool(0); + let payload = parallelize_core(pool, input.requests.clone(), |ctx, subquery| { + handle_single_solidity_nested_mapping_subquery_phase0(ctx, &chip, &subquery) + }); + + let ctx = pool.main(); + let mut vt = Vec::with_capacity(payload.len()); + let mut lr = Vec::with_capacity(payload.len()); + // promise calls to header component: + for p in payload.iter() { + let block_number = p.subquery.block_number; + let addr = p.subquery.addr; + let slot = p.value_slot; + let storage_subquery = AssignedStorageSubquery { block_number, addr, slot }; + // promise call to get the value at the value_slot + let value = promise_caller + .call::, ComponentTypeStorageSubquery>( + ctx, + FieldStorageSubqueryCall(storage_subquery), + ) + .unwrap(); + vt.push((p.subquery.into(), value.into())); + lr.push(LogicalResult::::new( + get_logical_value(&p.subquery), + get_logical_value(&value), + )); + } + self.payload = Some((keccak, payload)); + CoreBuilderOutput { public_instances: vec![], virtual_table: vt, logical_results: lr } + } + + fn virtual_assign_phase1(&mut self, builder: &mut RlcCircuitBuilder) { + let (keccak, payload) = self.payload.take().unwrap(); + // preamble + let range_chip = keccak.range(); + let rlc_chip = builder.rlc_chip(&range_chip.gate); + let rlp = RlpChip::new(range_chip, Some(&rlc_chip)); + let mpt = MPTChip::new(rlp, &keccak); + let chip = SolidityChip::new(&mpt, MAX_SOLIDITY_MAPPING_KEYS, 32); + + // actual logic + builder.parallelize_phase1(payload, |(ctx_gate, ctx_rlc), payload| { + handle_single_solidity_nested_mapping_subquery_phase1( + (ctx_gate, ctx_rlc), + &chip, + payload, + ) + }); + } +} + +pub struct PayloadSolidityNestedMappingSubquery { + pub mapping_witness: NestedMappingWitness, + pub subquery: AssignedSolidityNestedMappingSubquery, + /// Storage slot with the actual value of the mapping + pub value_slot: HiLo>, +} + +/// Assigns `subquery` to virtual cells and then handles the subquery. +/// Calculates the correct raw EVM storage slot corresponding to the nested mapping. +/// We do not return the `value` here. Instead we use the `value` gotten by making a promise +/// call to the Storage Subqueries Component circuit at the returned `slot`. +pub fn handle_single_solidity_nested_mapping_subquery_phase0( + ctx: &mut Context, + chip: &SolidityChip, + subquery: &FieldSolidityNestedMappingSubquery, +) -> PayloadSolidityNestedMappingSubquery { + let gate = chip.gate(); + let range = chip.range(); + // assign `mapping_slot` as HiLo + let mapping_slot = subquery.mapping_slot.assign(ctx); + // convert to `SafeBytes32` + let mapping_slot_bytes = SafeBytes32::try_from( + mapping_slot.hi_lo().map(|u| uint_to_bytes_be(ctx, range, &u, 16)).concat(), + ) + .unwrap(); + let keys_hilo = subquery.keys.map(|key| key.assign(ctx)); + let keys = keys_hilo.map(|k| { + SolidityType::Value( + SafeBytes32::try_from(k.hi_lo().map(|u| uint_to_bytes_be(ctx, range, &u, 16)).concat()) + .unwrap(), + ) + }); + let mapping_depth = ctx.load_witness(subquery.mapping_depth); + let mapping_witness = + chip.slot_for_nested_mapping_phase0(ctx, mapping_slot_bytes, keys, mapping_depth); + let value_slot = safe_bytes32_to_hi_lo(ctx, gate, &mapping_witness.slot); + + // Assign the rest of the subquery as witnesses + let addr = ctx.load_witness(subquery.addr); + let block_number = ctx.load_witness(subquery.block_number); + let subquery = AssignedSolidityNestedMappingSubquery { + block_number, + addr, + mapping_slot, + mapping_depth, + keys: keys_hilo, + }; + + PayloadSolidityNestedMappingSubquery { mapping_witness, subquery, value_slot } +} + +pub fn handle_single_solidity_nested_mapping_subquery_phase1( + ctx: RlcContextPair, + chip: &SolidityChip, + payload: PayloadSolidityNestedMappingSubquery, +) { + chip.slot_for_nested_mapping_phase1(ctx, payload.mapping_witness); +} diff --git a/axiom-query/src/components/subqueries/solidity_mappings/mod.rs b/axiom-query/src/components/subqueries/solidity_mappings/mod.rs new file mode 100644 index 00000000..46c67d10 --- /dev/null +++ b/axiom-query/src/components/subqueries/solidity_mappings/mod.rs @@ -0,0 +1,16 @@ +//! # Solidity Nested Mapping Subqueries Circuit +//! +//! SolidityNestedMapping +//! - `blockNumber` (uint32) +//! - `addr` (address = bytes20) +//! - `mappingSlot` (uint256) +//! - `mappingDepth` (uint8) -- in `(0, 4]` +//! - `keys` \[key0, key1, key2, key3\] (bytes32\[4\]) + +/// Circuit and Component Implementation. +pub mod circuit; +/// Types +pub mod types; + +#[cfg(test)] +pub mod tests; diff --git a/axiom-query/src/components/subqueries/solidity_mappings/tests.rs b/axiom-query/src/components/subqueries/solidity_mappings/tests.rs new file mode 100644 index 00000000..430a63fd --- /dev/null +++ b/axiom-query/src/components/subqueries/solidity_mappings/tests.rs @@ -0,0 +1,158 @@ +use std::str::FromStr; + +use axiom_codec::{ + types::{ + field_elements::AnySubqueryResult, + native::{SolidityNestedMappingSubquery, StorageSubquery}, + }, + utils::native::u256_to_h256, +}; +use axiom_eth::{ + halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr}, + keccak::{promise::generate_keccak_shards_from_calls, types::ComponentTypeKeccak}, + providers::setup_provider, + utils::component::{ + promise_loader::single::PromiseLoaderParams, ComponentCircuit, + ComponentPromiseResultsInMerkle, ComponentType, + }, +}; +use ethers_core::{ + types::{Address, Chain, H256, U256}, + utils::keccak256, +}; +use ethers_providers::Middleware; +use futures::future::join_all; +use tokio; + +use crate::components::{ + dummy_rlc_circuit_params, + subqueries::{ + common::shard_into_component_promise_results, + storage::types::{ComponentTypeStorageSubquery, OutputStorageShard}, + }, +}; + +use super::{ + circuit::{ + ComponentCircuitSolidityNestedMappingSubquery, CoreParamsSolidityNestedMappingSubquery, + }, + types::CircuitInputSolidityNestedMappingShard, +}; + +async fn test_mock_storage_subqueries( + k: u32, + network: Chain, + subqueries: Vec<(u64, &str, &str, Vec)>, // (blockNum, addr, mappingSlot, mappingKeys) +) -> ComponentCircuitSolidityNestedMappingSubquery { + let _ = env_logger::builder().is_test(true).try_init(); + + let _provider = setup_provider(network); + let provider = &_provider; + let (requests, storage_results): (Vec<_>, Vec<_>) = + join_all(subqueries.into_iter().map(|(block_num, addr, mapping_slot, keys)| async move { + let addr = Address::from_str(addr).unwrap(); + let mapping_slot = U256::from_str(mapping_slot).unwrap(); + let mut slot = u256_to_h256(&mapping_slot); + for key in keys.iter() { + slot = H256(keccak256(&[key.as_bytes(), slot.as_bytes()].concat())); + } + let proof = provider.get_proof(addr, vec![slot], Some(block_num.into())).await.unwrap(); + let depth = keys.len(); + let value = u256_to_h256(&proof.storage_proof[0].value); + ( + SolidityNestedMappingSubquery { + block_number: block_num as u32, + addr, + mapping_slot, + mapping_depth: depth as u8, + keys, + }, + AnySubqueryResult { + subquery: StorageSubquery { + block_number: block_num as u32, + addr, + slot: U256::from_big_endian(&slot.0), + }, + value, + }, + ) + })) + .await + .into_iter() + .unzip(); + + let promise_storage = OutputStorageShard { results: storage_results }; + let storage_capacity = promise_storage.results.len(); + let keccak_f_capacity = 200; + + let circuit_params = dummy_rlc_circuit_params(k as usize); + let mut circuit = ComponentCircuitSolidityNestedMappingSubquery::new( + CoreParamsSolidityNestedMappingSubquery { capacity: requests.len() }, + ( + PromiseLoaderParams::new_for_one_shard(keccak_f_capacity), + PromiseLoaderParams::new_for_one_shard(storage_capacity), + ), + circuit_params, + ); + + let input = CircuitInputSolidityNestedMappingShard:: { + requests: requests.into_iter().map(|r| r.into()).collect(), + }; + circuit.feed_input(Box::new(input)).unwrap(); + circuit.calculate_params(); + + let promises = [ + ( + ComponentTypeKeccak::::get_type_id(), + ComponentPromiseResultsInMerkle::from_single_shard( + generate_keccak_shards_from_calls(&circuit, keccak_f_capacity) + .unwrap() + .into_logical_results(), + ), + ), + ( + ComponentTypeStorageSubquery::::get_type_id(), + shard_into_component_promise_results::>( + promise_storage.into(), + ), + ), + ] + .into_iter() + .collect(); + circuit.fulfill_promise_results(&promises).unwrap(); + + let instances: Vec = circuit.get_public_instances().into(); + MockProver::run(k, &circuit, vec![instances]).unwrap().assert_satisfied(); + circuit +} + +#[tokio::test] +async fn test_mock_solidity_nested_mapping_uni_v3_factory() { + let k = 18; + let usdc_eth_500 = vec![ + Address::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap().into(), + Address::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap().into(), + H256::from_low_u64_be(500), + ]; + let eth_usdc_500 = vec![ + Address::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap().into(), + Address::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap().into(), + H256::from_low_u64_be(500), + ]; + + let subqueries = vec![ + ( + 17143006, + "0x1F98431c8aD98523631AE4a59f267346ea31F984", + "0x05", /*getPool*/ + usdc_eth_500, + ), + ( + 18331196, + "0x1F98431c8aD98523631AE4a59f267346ea31F984", + "0x05", /*getPool*/ + eth_usdc_500, + ), + ]; + test_mock_storage_subqueries(k, Chain::Mainnet, subqueries).await; +} diff --git a/axiom-query/src/components/subqueries/solidity_mappings/types.rs b/axiom-query/src/components/subqueries/solidity_mappings/types.rs new file mode 100644 index 00000000..f0a4087d --- /dev/null +++ b/axiom-query/src/components/subqueries/solidity_mappings/types.rs @@ -0,0 +1,99 @@ +//! Types are separated into: +//! - Circuit metadata that along with the circuit type determines the circuit configuration completely. +//! - Human readable _logical_ input and output to the circuit. These include private inputs and outputs that are only commited to in the public output. +//! - The in-circuit formatted versions of logical inputs and outputs. These include formatting in terms of field elements and accounting for all lengths needing to be fixed at compile time. +//! - We then provide conversion functions from human-readable to circuit formats. +//! - This circuit has no public instances (IO) other than the circuit's own component commitment and the promise commitments from any component calls. +use std::marker::PhantomData; + +use axiom_codec::{ + types::{ + field_elements::FieldSolidityNestedMappingSubquery, native::SolidityNestedMappingSubquery, + }, + HiLo, +}; +use axiom_eth::{ + halo2_base::AssignedValue, + impl_fix_len_call_witness, + utils::{ + build_utils::dummy::DummyFrom, + component::{circuit::CoreBuilderInput, ComponentType, ComponentTypeId, LogicalResult}, + }, +}; +use ethers_core::types::H256; +use serde::{Deserialize, Serialize}; + +use crate::{ + components::subqueries::common::OutputSubqueryShard, + utils::codec::AssignedSolidityNestedMappingSubquery, Field, RawField, +}; + +use super::circuit::CoreParamsSolidityNestedMappingSubquery; + +/// Identifier for the component type of this component circuit +pub struct ComponentTypeSolidityNestedMappingSubquery(PhantomData); + +/// Human readable. +/// The output value of any solidity nested mapping subquery is always `bytes32` right now. +pub type OutputSolidityNestedMappingShard = + OutputSubqueryShard; + +/// Circuit input for a shard of Solidity Nested Mapping subqueries. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CircuitInputSolidityNestedMappingShard { + /// Enriched subquery requests + pub requests: Vec>, +} + +impl DummyFrom + for CircuitInputSolidityNestedMappingShard +{ + fn dummy_from(core_params: CoreParamsSolidityNestedMappingSubquery) -> Self { + let CoreParamsSolidityNestedMappingSubquery { capacity } = core_params; + let dummy_subquery = + FieldSolidityNestedMappingSubquery { mapping_depth: F::ONE, ..Default::default() }; + Self { requests: vec![dummy_subquery; capacity] } + } +} + +/// The output value of any storage subquery is always `bytes32` right now. +/// Vector has been resized to the capacity. +pub type CircuitOutputSolidityNestedMappingShard = + OutputSubqueryShard, HiLo>; + +impl_fix_len_call_witness!( + FieldSolidityNestedMappingSubqueryCall, + FieldSolidityNestedMappingSubquery, + ComponentTypeSolidityNestedMappingSubquery +); + +// ===== This component has no public instances other than the output commitment and promise commitments from external component calls ===== + +impl ComponentType for ComponentTypeSolidityNestedMappingSubquery { + type InputValue = FieldSolidityNestedMappingSubquery; + type InputWitness = AssignedSolidityNestedMappingSubquery; + type OutputValue = HiLo; + type OutputWitness = HiLo>; + type LogicalInput = FieldSolidityNestedMappingSubquery; + + fn get_type_id() -> ComponentTypeId { + "axiom-query:ComponentTypeSolidityNestedMappingSubquery".to_string() + } + + fn logical_result_to_virtual_rows_impl( + ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + vec![(ins.input, ins.output)] + } + fn logical_input_to_virtual_rows_impl(li: &Self::LogicalInput) -> Vec { + vec![*li] + } +} + +impl From + for CircuitOutputSolidityNestedMappingShard +{ + fn from(output: OutputSolidityNestedMappingShard) -> Self { + output.convert_into() + } +} diff --git a/axiom-query/src/components/subqueries/storage/circuit.rs b/axiom-query/src/components/subqueries/storage/circuit.rs new file mode 100644 index 00000000..d57dfe19 --- /dev/null +++ b/axiom-query/src/components/subqueries/storage/circuit.rs @@ -0,0 +1,260 @@ +use axiom_codec::{utils::native::encode_addr_to_field, HiLo}; +use axiom_eth::{ + halo2_base::{ + gates::{flex_gate::threads::parallelize_core, GateInstructions}, + safe_types::SafeTypeChip, + AssignedValue, Context, + }, + halo2_proofs::plonk::ConstraintSystem, + keccak::{types::ComponentTypeKeccak, KeccakChip}, + mpt::MPTChip, + rlc::circuit::builder::RlcCircuitBuilder, + rlc::circuit::builder::RlcContextPair, + rlp::RlpChip, + storage::{EthStorageChip, EthStorageWitness}, + utils::{ + build_utils::aggregation::CircuitMetadata, + circuit_utils::bytes::{ + pack_bytes_to_hilo, safe_bytes32_to_hi_lo, unsafe_mpt_root_to_hi_lo, + }, + component::{ + circuit::{ + ComponentBuilder, ComponentCircuitImpl, CoreBuilder, CoreBuilderOutput, + CoreBuilderOutputParams, CoreBuilderParams, + }, + promise_collector::PromiseCaller, + promise_loader::{combo::PromiseBuilderCombo, single::PromiseLoader}, + types::LogicalEmpty, + utils::create_hasher, + LogicalResult, + }, + constrain_vec_equal, unsafe_bytes_to_assigned, + }, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + components::subqueries::{ + account::{ + types::{ComponentTypeAccountSubquery, FieldAccountSubqueryCall}, + STORAGE_ROOT_INDEX, + }, + common::{extract_logical_results, extract_virtual_table}, + }, + utils::codec::{ + AssignedAccountSubquery, AssignedStorageSubquery, AssignedStorageSubqueryResult, + }, + Field, +}; + +use super::types::{ + CircuitInputStorageShard, CircuitInputStorageSubquery, ComponentTypeStorageSubquery, +}; + +pub struct CoreBuilderStorageSubquery { + input: Option>, + params: CoreParamsStorageSubquery, + payload: Option<(KeccakChip, Vec>)>, +} + +/// Specify the output format of StorageSubquery component. +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct CoreParamsStorageSubquery { + /// The maximum number of subqueries of this type allowed in a single circuit. + pub capacity: usize, + /// The maximum depth of the storage MPT trie supported by this circuit. + /// The depth is defined as the maximum length of an storage proof, where the storage proof always ends in a terminal leaf node. + /// + /// In production this will be set to 13 based on the MPT analysis from https://hackmd.io/@axiom/BJBledudT + pub max_trie_depth: usize, +} +impl CoreBuilderParams for CoreParamsStorageSubquery { + fn get_output_params(&self) -> CoreBuilderOutputParams { + CoreBuilderOutputParams::new(vec![self.capacity]) + } +} + +type CKeccak = ComponentTypeKeccak; +type CAccount = ComponentTypeAccountSubquery; +/// Used for loading storage promise results. +pub type PromiseLoaderStorageSubquery = + PromiseBuilderCombo>, PromiseLoader>>; +pub type ComponentCircuitStorageSubquery = + ComponentCircuitImpl, PromiseLoaderStorageSubquery>; + +impl CircuitMetadata for CoreBuilderStorageSubquery { + const HAS_ACCUMULATOR: bool = false; + fn num_instance(&self) -> Vec { + unreachable!() + } +} + +impl ComponentBuilder for CoreBuilderStorageSubquery { + type Params = CoreParamsStorageSubquery; + + fn new(params: Self::Params) -> Self { + Self { input: None, params, payload: None } + } + fn get_params(&self) -> Self::Params { + self.params.clone() + } + fn clear_witnesses(&mut self) { + self.payload = None; + } + fn calculate_params(&mut self) -> Self::Params { + self.params.clone() + } + fn configure_with_params(_: &mut ConstraintSystem, _: Self::Params) {} +} + +impl CoreBuilder for CoreBuilderStorageSubquery { + type CompType = ComponentTypeStorageSubquery; + type PublicInstanceValue = LogicalEmpty; + type PublicInstanceWitness = LogicalEmpty>; + type CoreInput = CircuitInputStorageShard; + + fn feed_input(&mut self, input: Self::CoreInput) -> anyhow::Result<()> { + for r in &input.requests { + if r.proof.storage_pfs.len() != 1 { + anyhow::bail!( + "InvalidInput: each storage proof input must have exactly one storage proof" + ); + } + if r.proof.storage_pfs[0].2.max_depth != self.params.max_trie_depth { + anyhow::bail!("StorageSubquery: request MPT max depth {} does not match configured max depth {}", r.proof.storage_pfs[0].2.max_depth, self.params.max_trie_depth); + } + } + self.input = Some(input); + Ok(()) + } + fn virtual_assign_phase0( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_caller: PromiseCaller, + ) -> CoreBuilderOutput { + // preamble: to be removed + let keccak = + KeccakChip::new_with_promise_collector(builder.range_chip(), promise_caller.clone()); + let range_chip = keccak.range(); + let rlp = RlpChip::new(range_chip, None); + let mut poseidon = create_hasher(); + poseidon.initialize_consts(builder.base.main(0), keccak.gate()); + + // Assumption: we already have input when calling this function. + // TODO: automatically derive a dummy input from params. + let input = self.input.as_ref().unwrap(); + + let mpt = MPTChip::new(rlp, &keccak); + let chip = EthStorageChip::new(&mpt, None); + let pool = &mut builder.base.pool(0); + // actual logic + let payload = parallelize_core(pool, input.requests.clone(), |ctx, subquery| { + handle_single_storage_subquery_phase0(ctx, &chip, &subquery) + }); + + let vt = extract_virtual_table(payload.iter().map(|p| p.output)); + let lr: Vec> = + extract_logical_results(payload.iter().map(|p| p.output)); + + let ctx = pool.main(); + // promise calls to header component: + let account_storage_hash_idx = ctx.load_constant(F::from(STORAGE_ROOT_INDEX as u64)); + for p in payload.iter() { + let block_number = p.output.subquery.block_number; + let addr = p.output.subquery.addr; + let storage_root = p.storage_root; + let account_subquery = + AssignedAccountSubquery { block_number, addr, field_idx: account_storage_hash_idx }; + let promise_storage_root = promise_caller + .call::, ComponentTypeAccountSubquery>( + ctx, + FieldAccountSubqueryCall(account_subquery), + ) + .unwrap(); + constrain_vec_equal(ctx, &storage_root.hi_lo(), &promise_storage_root.hi_lo()); + } + self.payload = Some((keccak, payload)); + CoreBuilderOutput { public_instances: vec![], virtual_table: vt, logical_results: lr } + } + + fn virtual_assign_phase1(&mut self, builder: &mut RlcCircuitBuilder) { + let (keccak, payload) = self.payload.take().unwrap(); + // preamble + let range_chip = keccak.range(); + let rlc_chip = builder.rlc_chip(&range_chip.gate); + let rlp = RlpChip::new(range_chip, Some(&rlc_chip)); + let mpt = MPTChip::new(rlp, &keccak); + let chip = EthStorageChip::new(&mpt, None); + + // actual logic + builder.parallelize_phase1(payload, |(ctx_gate, ctx_rlc), payload| { + handle_single_storage_subquery_phase1((ctx_gate, ctx_rlc), &chip, payload) + }); + } +} + +pub struct PayloadStorageSubquery { + pub storage_witness: EthStorageWitness, + pub storage_root: HiLo>, + pub output: AssignedStorageSubqueryResult, +} + +/// Assigns `subquery` to virtual cells and then handles the subquery to get result. +/// **Assumes** that the storageHash is verified. Returns the assigned private witnesses of +/// `(block_number, address, storage_hash)`, to be looked up against Account Component promise. +pub fn handle_single_storage_subquery_phase0( + ctx: &mut Context, + chip: &EthStorageChip, + subquery: &CircuitInputStorageSubquery, +) -> PayloadStorageSubquery { + let gate = chip.gate(); + let range = chip.range(); + let safe = SafeTypeChip::new(range); + // assign address (H160) as single field element + let addr = ctx.load_witness(encode_addr_to_field(&subquery.proof.addr)); + // should have already validated input so storage_pfs has length 1 + let (slot, _value, mpt_proof) = subquery.proof.storage_pfs[0].clone(); + // assign `slot` as `SafeBytes32` + let unsafe_slot = unsafe_bytes_to_assigned(ctx, slot.as_bytes()); + let slot_bytes = safe.raw_bytes_to(ctx, unsafe_slot); + // convert slot to HiLo to save for later + let slot = safe_bytes32_to_hi_lo(ctx, gate, &slot_bytes); + + // assign storage proof + let mpt_proof = mpt_proof.assign(ctx); + // convert storageRoot from bytes to HiLo for later. `parse_storage_proof` will constrain these witnesses to be bytes + let storage_root = unsafe_mpt_root_to_hi_lo(ctx, gate, &mpt_proof); + // Check the storage MPT proof + let storage_witness = chip.parse_storage_proof_phase0(ctx, slot_bytes, mpt_proof); + // Left pad value to 32 bytes and convert to HiLo + let value = { + let w = storage_witness.value_witness(); + let inputs = w.field_cells.clone(); + let len = w.field_len; + let var_len_bytes = SafeTypeChip::unsafe_to_var_len_bytes_vec(inputs, len, 32); + let fixed_bytes = var_len_bytes.left_pad_to_fixed(ctx, gate); + pack_bytes_to_hilo(ctx, gate, fixed_bytes.bytes()) + }; + // set slot value to uint256(0) when the slot does not exist in the storage trie + let slot_is_empty = storage_witness.mpt_witness().slot_is_empty; + let value = HiLo::from_hi_lo(value.hi_lo().map(|x| gate.mul_not(ctx, slot_is_empty, x))); + + let block_number = ctx.load_witness(F::from(subquery.block_number)); + + PayloadStorageSubquery { + storage_witness, + storage_root, + output: AssignedStorageSubqueryResult { + subquery: AssignedStorageSubquery { block_number, addr, slot }, + value, + }, + } +} + +pub fn handle_single_storage_subquery_phase1( + ctx: RlcContextPair, + chip: &EthStorageChip, + payload: PayloadStorageSubquery, +) { + chip.parse_storage_proof_phase1(ctx, payload.storage_witness); +} diff --git a/axiom-query/src/components/subqueries/storage/mod.rs b/axiom-query/src/components/subqueries/storage/mod.rs new file mode 100644 index 00000000..771e0362 --- /dev/null +++ b/axiom-query/src/components/subqueries/storage/mod.rs @@ -0,0 +1,15 @@ +//! # Storage Subqueries Circuit +//! +//! Storage subquery +//! - `blockNumber` (uint32) +//! - `addr` (address = bytes20) +//! - `slot` (uint256) +//! + +/// Circuit and Component Implementation. +pub mod circuit; +/// Types +pub mod types; + +#[cfg(test)] +pub mod tests; diff --git a/axiom-query/src/components/subqueries/storage/tests.rs b/axiom-query/src/components/subqueries/storage/tests.rs new file mode 100644 index 00000000..3b6f6a03 --- /dev/null +++ b/axiom-query/src/components/subqueries/storage/tests.rs @@ -0,0 +1,208 @@ +use std::{marker::PhantomData, str::FromStr}; + +use axiom_codec::types::{field_elements::AnySubqueryResult, native::AccountSubquery}; +use axiom_eth::{ + halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr}, + keccak::{promise::generate_keccak_shards_from_calls, types::ComponentTypeKeccak}, + mpt::KECCAK_RLP_EMPTY_STRING, + providers::{setup_provider, storage::json_to_mpt_input}, + utils::component::{ + promise_loader::single::PromiseLoaderParams, ComponentCircuit, + ComponentPromiseResultsInMerkle, ComponentType, + }, +}; +use ethers_core::{ + types::{Address, Chain, H256}, + utils::keccak256, +}; +use ethers_providers::Middleware; +use futures::future::join_all; +use itertools::Itertools; +use rand::{rngs::StdRng, Rng}; +use rand_core::SeedableRng; +use tokio; + +use crate::components::{ + dummy_rlc_circuit_params, + subqueries::{ + account::{ + types::{ComponentTypeAccountSubquery, OutputAccountShard}, + STORAGE_ROOT_INDEX, + }, + block_header::tests::get_latest_block_number, + common::shard_into_component_promise_results, + storage::types::CircuitInputStorageSubquery, + }, +}; + +use super::{ + circuit::{ComponentCircuitStorageSubquery, CoreParamsStorageSubquery}, + types::CircuitInputStorageShard, +}; + +pub const STORAGE_PROOF_MAX_DEPTH: usize = 13; + +async fn test_mock_storage_subqueries( + k: u32, + network: Chain, + subqueries: Vec<(u64, &str, H256)>, // (blockNum, addr, slot) +) -> ComponentCircuitStorageSubquery { + let _ = env_logger::builder().is_test(true).try_init(); + + let _provider = setup_provider(network); + let provider = &_provider; + let (requests, storage_hashes): (Vec, Vec) = + join_all(subqueries.into_iter().map(|(block_num, addr, slot)| async move { + let addr = Address::from_str(addr).unwrap(); + let proof = provider.get_proof(addr, vec![slot], Some(block_num.into())).await.unwrap(); + let storage_hash = if proof.storage_hash.is_zero() { + // RPC provider may give zero storage hash for empty account, but the correct storage hash should be the null root = keccak256(0x80) + H256::from_slice(&KECCAK_RLP_EMPTY_STRING) + } else { + proof.storage_hash + }; + assert_eq!(proof.storage_proof.len(), 1, "Storage proof should have length 1 exactly"); + let proof = json_to_mpt_input(proof, 0, STORAGE_PROOF_MAX_DEPTH); + (CircuitInputStorageSubquery { block_number: block_num, proof }, storage_hash) + })) + .await + .into_iter() + .unzip(); + + let promise_account = OutputAccountShard { + results: requests + .iter() + .zip_eq(storage_hashes) + .map(|(r, storage_hash)| AnySubqueryResult { + subquery: AccountSubquery { + block_number: r.block_number as u32, + field_idx: STORAGE_ROOT_INDEX as u32, + addr: r.proof.addr, + }, + value: storage_hash, + }) + .collect(), + }; + + let keccak_f_capacity = 1200; + let account_capacity = promise_account.results.len(); + + let circuit_params = dummy_rlc_circuit_params(k as usize); + let mut circuit = ComponentCircuitStorageSubquery::new( + CoreParamsStorageSubquery { + capacity: requests.len(), + max_trie_depth: STORAGE_PROOF_MAX_DEPTH, + }, + ( + PromiseLoaderParams::new_for_one_shard(keccak_f_capacity), + PromiseLoaderParams::new_for_one_shard(account_capacity), + ), + circuit_params, + ); + + let input = CircuitInputStorageShard:: { requests, _phantom: PhantomData }; + circuit.feed_input(Box::new(input)).unwrap(); + circuit.calculate_params(); + let promises = [ + ( + ComponentTypeKeccak::::get_type_id(), + ComponentPromiseResultsInMerkle::from_single_shard( + generate_keccak_shards_from_calls(&circuit, keccak_f_capacity) + .unwrap() + .into_logical_results(), + ), + ), + ( + ComponentTypeAccountSubquery::::get_type_id(), + shard_into_component_promise_results::>( + promise_account.into(), + ), + ), + ] + .into_iter() + .collect(); + circuit.fulfill_promise_results(&promises).unwrap(); + + let instances: Vec = circuit.get_public_instances().into(); + MockProver::run(k, &circuit, vec![instances]).unwrap().assert_satisfied(); + circuit +} + +#[tokio::test] +async fn test_mock_storage_subqueries_slot0() { + let k = 18; + let subqueries = vec![ + (17143006, "0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B", H256::zero()), + (17143000, "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", H256::zero()), + (16356350, "0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB", H256::zero()), + (15411056, "0x1c479675ad559DC151F6Ec7ed3FbF8ceE79582B6", H256::zero()), + ]; + test_mock_storage_subqueries(k, Chain::Mainnet, subqueries).await; +} + +#[tokio::test] +async fn test_mock_storage_subqueries_uni_v3() { + let k = 18; + let network = Chain::Mainnet; + let mut rng = StdRng::seed_from_u64(0); + let address = "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"; // uniswap v3 eth-usdc 5bps pool + let latest = get_latest_block_number(network).await; + let subqueries = [0, 1, 2, 8] + .map(|x| (rng.gen_range(12376729..latest), address, H256::from_low_u64_be(x))) + .to_vec(); + test_mock_storage_subqueries(k, network, subqueries).await; +} + +#[tokio::test] +async fn test_mock_storage_subqueries_mapping() { + let k = 19; + let network = Chain::Mainnet; + let mut rng = StdRng::seed_from_u64(0); + let address = "0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB"; // cryptopunks + let slots = (0..8).map(|x| { + let mut bytes = [0u8; 64]; + bytes[31] = x; + bytes[63] = 10; + H256::from_slice(&keccak256(bytes)) + }); + let latest = get_latest_block_number(Chain::Mainnet).await; + let subqueries: Vec<_> = + slots.map(|slot| (rng.gen_range(3914495..latest), address, slot)).collect(); + test_mock_storage_subqueries(k, network, subqueries).await; +} + +// some of the slots will be empty, we test that the value returned is 0 +#[tokio::test] +async fn test_mock_storage_subqueries_empty() { + let k = 20; + let network = Chain::Mainnet; + let mut rng = StdRng::seed_from_u64(0); + let address = "0xbb9bc244d798123fde783fcc1c72d3bb8c189413"; // TheDAO Token + + // don't use random for re-producibility + let latest = 18317207; // get_latest_block_number(Chain::Mainnet).await; + let subqueries: Vec<_> = (0..64) + .map(|_| (rng.gen_range(1428757..latest), address, H256::random_using(&mut rng))) + .collect(); + test_mock_storage_subqueries(k, network, subqueries[19..20].to_vec()).await; +} + +#[tokio::test] +async fn test_mock_storage_subqueries_empty_precompile() { + let k = 18; + let subqueries = vec![ + (17143006, "0x0000000000000000000000000000000000000000", H256::zero()), + (17143000, "0x0000000000000000000000000000000000000001", H256::from_low_u64_be(1)), + (16356350, "0x0000000000000000000000000000000000000002", H256::from_low_u64_be(2)), + (15411056, "0x0000000000000000000000000000000000000003", H256::from_low_u64_be(3)), + ]; + test_mock_storage_subqueries(k, Chain::Mainnet, subqueries).await; +} + +#[tokio::test] +async fn test_mock_storage_subqueries_empty_account() { + let k = 18; + let subqueries = + vec![(4_000_000, "0xF57252Fc4ff36D8d10B0b83d8272020D2B8eDd55", H256::from_low_u64_be(295))]; + test_mock_storage_subqueries(k, Chain::Sepolia, subqueries).await; +} diff --git a/axiom-query/src/components/subqueries/storage/types.rs b/axiom-query/src/components/subqueries/storage/types.rs new file mode 100644 index 00000000..f96fadf9 --- /dev/null +++ b/axiom-query/src/components/subqueries/storage/types.rs @@ -0,0 +1,112 @@ +//! Types are separated into: +//! - Circuit metadata that along with the circuit type determines the circuit configuration completely. +//! - Human readable _logical_ input and output to the circuit. These include private inputs and outputs that are only commited to in the public output. +//! - The in-circuit formatted versions of logical inputs and outputs. These include formatting in terms of field elements and accounting for all lengths needing to be fixed at compile time. +//! - We then provide conversion functions from human-readable to circuit formats. +//! - This circuit has no public instances (IO) other than the circuit's own component commitment and the promise commitments from any component calls. +use std::marker::PhantomData; + +use axiom_codec::{ + types::{field_elements::FieldStorageSubquery, native::StorageSubquery}, + HiLo, +}; +use axiom_eth::{ + halo2_base::AssignedValue, + impl_fix_len_call_witness, + providers::storage::json_to_mpt_input, + storage::circuit::EthStorageInput, + utils::{ + build_utils::dummy::DummyFrom, + component::{circuit::CoreBuilderInput, ComponentType, ComponentTypeId, LogicalResult}, + }, +}; +use ethers_core::types::{EIP1186ProofResponse, H256}; +use serde::{Deserialize, Serialize}; + +use crate::utils::codec::AssignedStorageSubquery; +use crate::{ + components::subqueries::{ + account::types::GENESIS_ADDRESS_0_ACCOUNT_PROOF, common::OutputSubqueryShard, + }, + Field, +}; + +use super::circuit::CoreParamsStorageSubquery; + +/// Identifier for the component type of this component circuit +pub struct ComponentTypeStorageSubquery(PhantomData); + +/// Human readable. +/// The output value of any storage subquery is always `bytes32` right now. +pub type OutputStorageShard = OutputSubqueryShard; + +/// Circuit input for a shard of Storage subqueries. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CircuitInputStorageShard { + /// Enriched subquery requests + pub requests: Vec, + pub _phantom: PhantomData, +} + +/// Circuit input for a single Storage subquery. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CircuitInputStorageSubquery { + /// The block number to access the storage state at. + pub block_number: u64, + /// Storage proof formatted as MPT input. It will contain the account address. + /// ### Warning + /// `proof.acct_pf` will be empty and `proof` will **not** have state_root set. + pub proof: EthStorageInput, +} + +impl DummyFrom for CircuitInputStorageShard { + fn dummy_from(core_params: CoreParamsStorageSubquery) -> Self { + let CoreParamsStorageSubquery { capacity, max_trie_depth } = core_params; + let request = { + let pf: EIP1186ProofResponse = + serde_json::from_str(GENESIS_ADDRESS_0_ACCOUNT_PROOF).unwrap(); + let proof = json_to_mpt_input(pf, 0, max_trie_depth); + CircuitInputStorageSubquery { block_number: 0, proof } + }; + Self { requests: vec![request; capacity], _phantom: PhantomData } + } +} + +/// The output value of any storage subquery is always `bytes32` right now. +/// Vector has been resized to the capacity. +pub type CircuitOutputStorageShard = OutputSubqueryShard, HiLo>; + +impl_fix_len_call_witness!( + FieldStorageSubqueryCall, + FieldStorageSubquery, + ComponentTypeStorageSubquery +); + +// ===== The storage component has no public instances other than the component commitment and promise commitments from external component calls ===== + +impl ComponentType for ComponentTypeStorageSubquery { + type InputValue = FieldStorageSubquery; + type InputWitness = AssignedStorageSubquery; + type OutputValue = HiLo; + type OutputWitness = HiLo>; + type LogicalInput = FieldStorageSubquery; + + fn get_type_id() -> ComponentTypeId { + "axiom-query:ComponentTypeStorageSubquery".to_string() + } + + fn logical_result_to_virtual_rows_impl( + ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + vec![(ins.input, ins.output)] + } + fn logical_input_to_virtual_rows_impl(li: &Self::LogicalInput) -> Vec { + vec![*li] + } +} + +impl From for CircuitOutputStorageShard { + fn from(output: OutputStorageShard) -> Self { + output.convert_into() + } +} diff --git a/axiom-query/src/components/subqueries/transaction/circuit.rs b/axiom-query/src/components/subqueries/transaction/circuit.rs new file mode 100644 index 00000000..1b74860f --- /dev/null +++ b/axiom-query/src/components/subqueries/transaction/circuit.rs @@ -0,0 +1,524 @@ +use std::iter::zip; + +use axiom_codec::{ + constants::FIELD_IDX_BITS, + encoder::field_elements::SUBQUERY_OUTPUT_BYTES, + special_values::{ + TX_BLOCK_NUMBER_FIELD_IDX, TX_CALLDATA_HASH_FIELD_IDX, TX_CALLDATA_IDX_OFFSET, + TX_CONTRACT_DATA_IDX_OFFSET, TX_CONTRACT_DEPLOY_SELECTOR_VALUE, TX_DATA_LENGTH_FIELD_IDX, + TX_FUNCTION_SELECTOR_FIELD_IDX, TX_NO_CALLDATA_SELECTOR_VALUE, TX_TX_INDEX_FIELD_IDX, + TX_TX_TYPE_FIELD_IDX, + }, + HiLo, +}; +use axiom_eth::{ + block_header::TX_ROOT_INDEX, + halo2_base::{ + gates::{flex_gate::threads::parallelize_core, GateInstructions, RangeInstructions}, + safe_types::{SafeBool, SafeTypeChip, VarLenBytesVec}, + utils::bit_length, + AssignedValue, Context, + QuantumCell::Constant, + }, + halo2_proofs::plonk::ConstraintSystem, + keccak::{types::ComponentTypeKeccak, KeccakChip}, + mpt::MPTChip, + rlc::circuit::builder::RlcCircuitBuilder, + rlc::circuit::builder::RlcContextPair, + rlp::RlpChip, + transaction::{ + EthTransactionChip, EthTransactionChipParams, EthTransactionWitness, TRANSACTION_MAX_FIELDS, + }, + utils::{ + build_utils::aggregation::CircuitMetadata, + bytes_be_to_uint, + circuit_utils::{ + bytes::{pack_bytes_to_hilo, select_hi_lo, unsafe_mpt_root_to_hi_lo}, + extract_array_chunk_and_constrain_trailing_zeros, is_equal_usize, is_gte_usize, + is_in_range, is_lt_usize, min_with_usize, unsafe_constrain_trailing_zeros, + }, + component::{ + circuit::{ + ComponentBuilder, ComponentCircuitImpl, CoreBuilder, CoreBuilderOutput, + CoreBuilderOutputParams, CoreBuilderParams, + }, + promise_collector::PromiseCaller, + promise_loader::{combo::PromiseBuilderCombo, single::PromiseLoader}, + types::LogicalEmpty, + utils::create_hasher, + LogicalResult, + }, + constrain_vec_equal, + }, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::{ + components::subqueries::{ + block_header::types::{ComponentTypeHeaderSubquery, FieldHeaderSubqueryCall}, + common::{extract_logical_results, extract_virtual_table}, + }, + utils::codec::{AssignedHeaderSubquery, AssignedTxSubquery, AssignedTxSubqueryResult}, + Field, +}; + +use super::{ + types::{CircuitInputTxShard, CircuitInputTxSubquery, ComponentTypeTxSubquery}, + TX_DATA_FIELD_IDX, +}; + +pub struct CoreBuilderTxSubquery { + input: Option>, + params: CoreParamsTxSubquery, + payload: Option<(KeccakChip, Vec>)>, +} + +/// Specify the output format of TxSubquery component. +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct CoreParamsTxSubquery { + pub chip_params: EthTransactionChipParams, + /// The maximum number of subqueries of this type allowed in a single circuit. + pub capacity: usize, + /// The maximum depth of the transactions MPT trie supported by this circuit. + /// The depth is defined as the maximum length of a Merkle proof, where the proof always ends in a terminal node (if the proof ends in a branch, we extract the leaf and add it as a separate node). + /// + /// In practice this can always be set to 6, because + /// transaction index is within u16, so rlp(txIndex) is at most 3 bytes => 6 nibbles. + pub max_trie_depth: usize, +} +impl CoreBuilderParams for CoreParamsTxSubquery { + fn get_output_params(&self) -> CoreBuilderOutputParams { + CoreBuilderOutputParams::new(vec![self.capacity]) + } +} + +type CKeccak = ComponentTypeKeccak; +type CHeader = ComponentTypeHeaderSubquery; +pub type PromiseLoaderTxSubquery = + PromiseBuilderCombo>, PromiseLoader>>; +pub type ComponentCircuitTxSubquery = + ComponentCircuitImpl, PromiseLoaderTxSubquery>; + +impl CircuitMetadata for CoreBuilderTxSubquery { + const HAS_ACCUMULATOR: bool = false; + fn num_instance(&self) -> Vec { + unreachable!() + } +} + +impl ComponentBuilder for CoreBuilderTxSubquery { + type Params = CoreParamsTxSubquery; + + fn new(params: Self::Params) -> Self { + Self { input: None, params, payload: None } + } + fn get_params(&self) -> Self::Params { + self.params.clone() + } + fn clear_witnesses(&mut self) { + self.payload = None; + } + fn calculate_params(&mut self) -> Self::Params { + self.params.clone() + } + fn configure_with_params(_: &mut ConstraintSystem, _: Self::Params) {} +} + +impl CoreBuilder for CoreBuilderTxSubquery { + type CompType = ComponentTypeTxSubquery; + type PublicInstanceValue = LogicalEmpty; + type PublicInstanceWitness = LogicalEmpty>; + type CoreInput = CircuitInputTxShard; + + fn feed_input(&mut self, input: Self::CoreInput) -> anyhow::Result<()> { + for r in &input.requests { + if r.proof.proof.max_depth != self.params.max_trie_depth { + anyhow::bail!( + "TxSubquery: request proof max depth {} does not match the configured max_depth {}", + r.proof.proof.max_depth, + self.params.max_trie_depth + ); + } + } + self.input = Some(input); + Ok(()) + } + /// Includes computing the component commitment to the logical output (the subquery results). + /// **In addition** performs _promise calls_ to the Header Component to verify + /// all `(block_number, transaction_root)` pairs as additional "enriched" header subqueries. + /// These are checked against the supplied promise commitment using dynamic lookups + /// (behind the scenes) by `promise_caller`. + fn virtual_assign_phase0( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_caller: PromiseCaller, + ) -> CoreBuilderOutput { + // preamble: to be removed + let keccak = + KeccakChip::new_with_promise_collector(builder.range_chip(), promise_caller.clone()); + let range_chip = keccak.range(); + let rlp = RlpChip::new(range_chip, None); + let mut poseidon = create_hasher(); + poseidon.initialize_consts(builder.base.main(0), keccak.gate()); + + // Assumption: we already have input when calling this function. + // TODO: automatically derive a dummy input from params. + let input = self.input.as_ref().unwrap(); + + let mpt = MPTChip::new(rlp, &keccak); + let chip = EthTransactionChip::new(&mpt, self.params.chip_params); + let base_builder = &mut builder.base; + // actual logic + let payload = + parallelize_core(base_builder.pool(0), input.requests.clone(), |ctx, subquery| { + handle_single_tx_subquery_phase0(ctx, &chip, &subquery) + }); + + let vt = extract_virtual_table(payload.iter().map(|p| p.output)); + let lr: Vec> = + extract_logical_results(payload.iter().map(|p| p.output)); + let ctx = base_builder.main(0); + + // promise calls to header component: + // - for each block number in a subquery, we must make a promise call to check the transaction root of that block + let header_tx_root_idx = ctx.load_constant(F::from(TX_ROOT_INDEX as u64)); + for p in payload.iter() { + let block_number = p.output.subquery.block_number; + let tx_root = p.tx_root; + let header_subquery = + AssignedHeaderSubquery { block_number, field_idx: header_tx_root_idx }; + let promise_tx_root = promise_caller + .call::, ComponentTypeHeaderSubquery>( + ctx, + FieldHeaderSubqueryCall(header_subquery), + ) + .unwrap(); + constrain_vec_equal(ctx, &tx_root.hi_lo(), &promise_tx_root.hi_lo()); + } + self.payload = Some((keccak, payload)); + CoreBuilderOutput { public_instances: vec![], virtual_table: vt, logical_results: lr } + } + + fn virtual_assign_phase1(&mut self, builder: &mut RlcCircuitBuilder) { + let (keccak, payload) = self.payload.take().unwrap(); + // preamble + let range_chip = keccak.range(); + let rlc_chip = builder.rlc_chip(&range_chip.gate); + let rlp = RlpChip::new(range_chip, Some(&rlc_chip)); + let mpt = MPTChip::new(rlp, &keccak); + let chip = EthTransactionChip::new(&mpt, self.params.chip_params); + + // actual logic + builder.parallelize_phase1(payload, |(ctx_gate, ctx_rlc), payload| { + handle_single_tx_subquery_phase1((ctx_gate, ctx_rlc), &chip, payload) + }); + } +} + +pub struct PayloadTxSubquery { + pub tx_witness: EthTransactionWitness, + pub tx_root: HiLo>, + pub output: AssignedTxSubqueryResult, +} + +/// Assigns `subquery` to virtual cells and then handles the subquery to get result. +/// **Assumes** that the transactionsRoot is verified. Returns the assigned private witnesses of +/// `(block_number, transactionsRoot)`, to be looked up against Header Component promise. +pub fn handle_single_tx_subquery_phase0( + ctx: &mut Context, + chip: &EthTransactionChip, + subquery: &CircuitInputTxSubquery, +) -> PayloadTxSubquery { + let gate = chip.gate(); + let range = chip.range(); + // assign tx proof + let tx_proof = subquery.proof.clone().assign(ctx); + // convert transactionsRoot from bytes to HiLo for later. `parse_transaction_proof` will constrain these witnesses to be bytes + let tx_root = unsafe_mpt_root_to_hi_lo(ctx, gate, &tx_proof.proof); + // Check the transaction MPT proof + let tx_witness = chip.parse_transaction_proof_phase0(ctx, tx_proof); + let tx_type = tx_witness.transaction_type; + range.check_less_than_safe(ctx, tx_type, 3); + let tx_type_indicator: [_; 3] = gate.idx_to_indicator(ctx, tx_type, 3).try_into().unwrap(); + // index of `data` in rlp list + let data_list_idx = + gate.select_by_indicator(ctx, [5, 6, 7].map(|x| Constant(F::from(x))), tx_type_indicator); + // We always need the `data` field, so extract it + // this function also asserts that the MPT proof is an inclusion proof + let data = chip.extract_field(ctx, tx_witness, data_list_idx); + let tx_witness = data.transaction_witness; // pass through + // the byte length of `data` + let data_len = data.len; + let data = data.field_bytes; + + let field_or_calldata_idx = ctx.load_witness(F::from(subquery.field_or_calldata_idx as u64)); + range.range_check(ctx, field_or_calldata_idx, FIELD_IDX_BITS); + // if `field_idx` < `TX_CALLDATA_IDX_OFFSET`, then it is an actual tx rlp list item + let is_idx_in_list = + is_lt_usize(ctx, range, field_or_calldata_idx, TX_TX_TYPE_FIELD_IDX, FIELD_IDX_BITS); + // if `field_or_calldata_idx` is not `field_idx`, set it to 1. (Don't put 0 because that's invalid field_idx for type 0 tx) + let field_idx = gate.select(ctx, field_or_calldata_idx, Constant(F::ONE), is_idx_in_list); + + let list_idx = v2_map_field_idx_by_tx_type(ctx, range, field_idx, tx_type_indicator); + let value = extract_truncated_field(ctx, range, &tx_witness, list_idx, SUBQUERY_OUTPUT_BYTES); + // we should left pad with 0s to fixed *unless* `field_idx == TX_DATA_FIELD_IDX` (or accessList but that is not supported) + let value_fixed = value.left_pad_to_fixed(ctx, gate); + let is_not_value_type = + gate.is_equal(ctx, field_idx, Constant(F::from(TX_DATA_FIELD_IDX as u64))); + let value = zip(value.bytes(), value_fixed.bytes()) + .map(|(var, fixed)| gate.select(ctx, *var, *fixed, is_not_value_type)) + .collect_vec(); + let value = SafeTypeChip::unsafe_to_fix_len_bytes_vec(value, SUBQUERY_OUTPUT_BYTES); + let mut value = pack_bytes_to_hilo(ctx, gate, value.bytes()); + + let block_number = ctx.load_witness(F::from(subquery.block_number)); + let tx_idx = tx_witness.idx; + // time to handle special cases: + let [return_tx_type, return_block_num, return_tx_index, return_function_selector, return_calldata_hash, return_data_length] = + [ + TX_TX_TYPE_FIELD_IDX, + TX_BLOCK_NUMBER_FIELD_IDX, + TX_TX_INDEX_FIELD_IDX, + TX_FUNCTION_SELECTOR_FIELD_IDX, + TX_CALLDATA_HASH_FIELD_IDX, + TX_DATA_LENGTH_FIELD_IDX, + ] + .map(|const_idx| is_equal_usize(ctx, gate, field_or_calldata_idx, const_idx)); + let const_zero = ctx.load_zero(); + let from_lo = |lo| HiLo::from_hi_lo([const_zero, lo]); + value = select_hi_lo(ctx, gate, &from_lo(tx_type), &value, return_tx_type); + value = select_hi_lo(ctx, gate, &from_lo(block_number), &value, return_block_num); + value = select_hi_lo(ctx, gate, &from_lo(tx_idx), &value, return_tx_index); + + // function selector case + // index of `to` in rlp list + let to_list_idx = + gate.select_by_indicator(ctx, [3, 4, 5].map(|x| Constant(F::from(x))), tx_type_indicator); + // the byte length of `to` + let to_len = gate.select_from_idx( + ctx, + tx_witness.value_witness().field_witness.iter().map(|w| w.field_len), + to_list_idx, + ); + let function_selector = { + // if `to_len == 0` && `data_len != 0` return `TX_CONTRACT_DEPLOY_SELECTOR` + let mut is_contract_deploy = gate.is_zero(ctx, to_len); + let empty_data = gate.is_zero(ctx, data_len); + is_contract_deploy = gate.mul_not(ctx, empty_data, is_contract_deploy); + // if `return_function_selector == 1` then `is_contractor_deploy || empty_data == 1` + let no_sel = gate.add(ctx, is_contract_deploy, empty_data); + let ret1 = gate.select( + ctx, + Constant(F::from(TX_CONTRACT_DEPLOY_SELECTOR_VALUE as u64)), + Constant(F::from(TX_NO_CALLDATA_SELECTOR_VALUE as u64)), + is_contract_deploy, + ); + + let len_gte_4 = is_gte_usize(ctx, range, data_len, 4, 32); + let selector_bytes = + data[..4].iter().map(|b| SafeTypeChip::unsafe_to_byte(*b)).collect_vec(); + let ret2 = bytes_be_to_uint(ctx, gate, &selector_bytes, 4); + // valid if no_sel || len_gte4 + let mut is_valid = gate.or(ctx, no_sel, len_gte_4); + is_valid = gate.select(ctx, is_valid, Constant(F::ONE), return_function_selector); + gate.assert_is_const(ctx, &is_valid, &F::ONE); + gate.select(ctx, ret1, ret2, no_sel) + }; + value = select_hi_lo(ctx, gate, &from_lo(function_selector), &value, return_function_selector); + value = select_hi_lo(ctx, gate, &from_lo(data_len), &value, return_data_length); + + let (data_buf, return_data) = handle_data(ctx, range, &data, data_len, field_or_calldata_idx); + value = select_hi_lo(ctx, gate, &data_buf, &value, return_data); + // dbg!(return_data.as_ref().value()); + + // calldata hash case + // IMPORTANT: we set the length to 0 so it doesn't actually request a keccak unless `return_calldata_hash == 1` + let tmp_data_len = gate.mul(ctx, data_len, return_calldata_hash); + let data_hash = chip.keccak().keccak_var_len(ctx, data.clone(), tmp_data_len, 0).hi_lo(); + value = select_hi_lo(ctx, gate, &HiLo::from_hi_lo(data_hash), &value, return_calldata_hash); + // dbg!(field_or_calldata_idx.value()); + // dbg!(value.hi_lo().map(|v| *v.value())); + + // is valid if either: + // - `is_idx_in_list` and `field_idx` is validated (validation done by `v2_map_field_idx_by_tx_type`) + // - `field_or_calldata_idx` is one of the special cases (individual validation was done per case) + // This sum is guaranteed to be 0 or 1 because the cases are mutually exclusive: + let is_special_case = gate.sum( + ctx, + [ + return_tx_type, + return_block_num, + return_tx_index, + return_function_selector, + return_calldata_hash, + return_data_length, + return_data, + ], + ); + let is_valid = gate.or(ctx, is_idx_in_list, is_special_case); + gate.assert_is_const(ctx, &is_valid, &F::ONE); + + PayloadTxSubquery { + tx_witness, + tx_root, + output: AssignedTxSubqueryResult { + subquery: AssignedTxSubquery { block_number, tx_idx, field_or_calldata_idx }, + value, + }, + } +} + +pub fn handle_single_tx_subquery_phase1( + ctx: RlcContextPair, + chip: &EthTransactionChip, + payload: PayloadTxSubquery, +) { + chip.parse_transaction_proof_phase1(ctx, payload.tx_witness); +} + +fn handle_data( + ctx: &mut Context, + range: &impl RangeInstructions, + data_bytes: &[AssignedValue], + data_len: AssignedValue, + field_idx: AssignedValue, +) -> (HiLo>, SafeBool) { + let gate = range.gate(); + let in_calldata_range = is_in_range( + ctx, + range, + field_idx, + TX_CALLDATA_IDX_OFFSET..TX_CONTRACT_DATA_IDX_OFFSET, + FIELD_IDX_BITS, + ); + let in_contract_data_range = + is_gte_usize(ctx, range, field_idx, TX_CONTRACT_DATA_IDX_OFFSET, FIELD_IDX_BITS); + + let calldata_shift = gate.sub(ctx, field_idx, Constant(F::from(TX_CALLDATA_IDX_OFFSET as u64))); + let contract_data_shift = + gate.sub(ctx, field_idx, Constant(F::from(TX_CONTRACT_DATA_IDX_OFFSET as u64))); + let mut shift = gate.select(ctx, calldata_shift, contract_data_shift, in_calldata_range); + // shift by 4 if in_calldata_range + let buffer = (0..data_bytes.len()) + .map(|i| { + if i + 4 < data_bytes.len() { + gate.select(ctx, data_bytes[i + 4], data_bytes[i], in_calldata_range) + } else { + gate.mul_not(ctx, in_calldata_range, data_bytes[i]) + } + }) + .collect_vec(); + let is_valid_calldata = + is_gte_usize(ctx, range, data_len, 4, bit_length(data_bytes.len() as u64)); + let mut buffer_len = gate.sub_mul(ctx, data_len, Constant(F::from(4)), in_calldata_range); + // if !is_valid_calldata && in_calldata_range, then set buffer_len = 0 (otherwise it's negative and will overflow) + let buffer_len_is_negative = gate.mul_not(ctx, is_valid_calldata, in_calldata_range); + buffer_len = gate.mul_not(ctx, buffer_len_is_negative, buffer_len); + // is_in_range = (in_calldata_range && is_valid_calldata) || in_contract_data_range + let is_in_range = + gate.mul_add(ctx, in_calldata_range, is_valid_calldata, in_contract_data_range); + shift = gate.mul(ctx, shift, is_in_range); + let (buffer, is_lt_len) = extract_array_chunk_and_constrain_trailing_zeros( + ctx, + range, + &buffer, + buffer_len, + shift, + 32, + FIELD_IDX_BITS, + ); + + let is_in_range = SafeTypeChip::unsafe_to_bool(gate.and(ctx, is_in_range, is_lt_len)); + let buffer = SafeTypeChip::unsafe_to_fix_len_bytes_vec(buffer, 32); + (pack_bytes_to_hilo(ctx, gate, buffer.bytes()), is_in_range) +} + +/// Extracts the field at `field_idx` from the given rlp list decomposition of a transaction. +/// The field is truncated to the first `truncated_byte_len` bytes. +/// +/// We do not use `EthTransactionChip::extract_field` because without the truncation the +/// select operation can be very expensive if the `data` field is very long. +pub fn extract_truncated_field( + ctx: &mut Context, + range: &impl RangeInstructions, + witness: &EthTransactionWitness, + list_idx: AssignedValue, + truncated_byte_len: usize, +) -> VarLenBytesVec { + let gate = range.gate(); + let tx_values = &witness.value_witness().field_witness; + let indicator = gate.idx_to_indicator(ctx, list_idx, TRANSACTION_MAX_FIELDS); + assert_eq!(tx_values.len(), TRANSACTION_MAX_FIELDS); + let const_zero = ctx.load_zero(); + let mut field_bytes = (0..truncated_byte_len) + .map(|i| { + let entries = tx_values.iter().map(|w| *w.field_cells.get(i).unwrap_or(&const_zero)); + gate.select_by_indicator(ctx, entries, indicator.clone()) + }) + .collect_vec(); + let lens = tx_values.iter().map(|w| w.field_len); + let mut len = gate.select_by_indicator(ctx, lens, indicator); + // len = min(len, truncated_byte_len) + let max_bytes = tx_values.iter().map(|w| w.field_cells.len()).max().unwrap(); + let max_bits = bit_length(max_bytes as u64); + len = min_with_usize(ctx, range, len, truncated_byte_len, max_bits); + + unsafe_constrain_trailing_zeros(ctx, gate, &mut field_bytes, len); + + SafeTypeChip::unsafe_to_var_len_bytes_vec(field_bytes, len, truncated_byte_len) +} + +// spreadsheet is easiest way to explain: https://docs.google.com/spreadsheets/d/1KoNZTr5vkcPTekzUzXCo0EycsbhuC6z-Z0vMtD4_9us/edit?usp=sharing +/// Constrains that `tx_type` is in [0,2]. +/// We do not allow accessList in V2. +fn v2_map_field_idx_by_tx_type( + ctx: &mut Context, + range: &impl RangeInstructions, + field_idx: AssignedValue, + tx_type_indicator: [AssignedValue; 3], +) -> AssignedValue { + let max_bits = FIELD_IDX_BITS; + let gate = range.gate(); + let const_invalid = Constant(F::from(99)); + // a lot of hardcoded stuff + let in_range = range.is_less_than(ctx, field_idx, Constant(F::from(12)), max_bits); + let is_gte_4 = is_gte_usize(ctx, range, field_idx, 4, max_bits); + let is_gas_price = gate.is_equal(ctx, field_idx, Constant(F::from(8))); + let type_0 = { + let is_gte_9 = is_gte_usize(ctx, range, field_idx, 9, max_bits); + let shift_three = is_gte_9.into(); + let is_lt_9 = gate.not(ctx, is_gte_9); + let shift_two = gate.and(ctx, is_gte_4, is_lt_9); + let shift_one = gate.is_equal(ctx, field_idx, Constant(F::ONE)); + let mut is_valid = gate.sum(ctx, [shift_one, shift_two, shift_three]); + is_valid = gate.and(ctx, is_valid, in_range); + let mut idx = field_idx; + // the three shifts are mutually exclusive + idx = gate.sub_mul(ctx, idx, shift_three, Constant(F::from(3))); + idx = gate.sub_mul(ctx, idx, shift_two, Constant(F::from(2))); + idx = gate.sub_mul(ctx, idx, shift_one, Constant(F::ONE)); + idx = gate.select(ctx, Constant(F::from(1)), idx, is_gas_price); + gate.select(ctx, idx, const_invalid, is_valid) + }; + let type_1 = { + let shift_one = is_gte_4; + let is_max_priority_fee = gate.is_equal(ctx, field_idx, Constant(F::from(2))); + let is_max_fee = gate.is_equal(ctx, field_idx, Constant(F::from(3))); + let mut is_valid = gate.mul_not(ctx, is_max_priority_fee, in_range); + is_valid = gate.mul_not(ctx, is_max_fee, is_valid); + let mut idx = gate.sub_mul(ctx, field_idx, shift_one, Constant(F::ONE)); + idx = gate.select(ctx, Constant(F::from(2)), idx, is_gas_price); + gate.select(ctx, idx, const_invalid, is_valid) + }; + let type_2 = { + let is_valid = gate.mul_not(ctx, is_gas_price, in_range); + gate.select(ctx, field_idx, const_invalid, is_valid) + }; + + let true_idx = gate.select_by_indicator(ctx, [type_0, type_1, type_2], tx_type_indicator); + let is_invalid = gate.is_equal(ctx, true_idx, const_invalid); + gate.assert_is_const(ctx, &is_invalid, &F::ZERO); + + true_idx +} diff --git a/axiom-query/src/components/subqueries/transaction/mod.rs b/axiom-query/src/components/subqueries/transaction/mod.rs new file mode 100644 index 00000000..5efdfa6e --- /dev/null +++ b/axiom-query/src/components/subqueries/transaction/mod.rs @@ -0,0 +1,53 @@ +/*! +# Transaction Subqueries Circuit + +Transaction subquery +* Raw EVM transaction layout reference: +* `blockNumber` (uint32) +* `txIdx` (uint16) +* `fieldOrCalldataIdx` (uint32) + * If in `[0, 100)` -- refers to the `fieldIdx` (**see below for definition**) + * If in `[100, infty)` -- represents `100 + calldataIdx`, where for a given value of `calldataIdx`, we return bytes `[4 + 32 * calldataIdx, 4 + 32 * calldataIdx + 32)`. This byte alignment is to return chunks of the ABI encoding. + * If in `[100000, infty)` -- represents `100000 + contractDataIdx`, where for a given value of `contractDataIdx`, we return bytes`[32 * contractDataIdx, 32 * contractDataIdx + 32)` + * Should be indexed so that the same `fieldIdx` represents the same thing across transaction types. + * We will add a special `TX_TYPE_FIELD_IDX` (51) for the transaction type. + * We will add a special `BLOCK_NUMBER_FIELD_IDX` (52) for the block number. + * We will add a special `TX_IDX_FIELD_IDX` (53) for the transaction index. + * We will add a special `FUNCTION_SELECTOR_FIELD_IDX` (54) for the function selector, which will return either the function selector (first 4 bytes of calldata) or one of 2 special values: + * If it’s a pure EOA transfer, then it will return `NO_CALLDATA_SELECTOR` (bytes32(61)). + * If it’s a contract deploy transaction, it will return `CONTRACT_DEPLOY_SELECTOR` (bytes32(60)) + * The `CALLDATA_FIELD_IDX` (55) corresponding to calldata will return the Keccak hash of the calldata. + * We will exclude access to access lists in V2. + * [Nice to have **(not yet supported)**] We will later add a special `TX_TX_HASH_FIELD_IDX` (56) for the transaction hash. + +The `fieldIdx` is defined as an enum that is **independent of transaction type**: + +| TxField | `fieldIdx` | +|------------------------|-------| +| ChainId | 0 | +| Nonce | 1 | +| MaxPriorityFeePerGas | 2 | +| MaxFeePerGas | 3 | +| GasLimit | 4 | +| To | 5 | +| Value | 6 | +| Data | 7 | +| GasPrice | 8 | +| v | 9 | +| r | 10 | +| s | 11 | + +This numbering is chosen so that for a Type 2 (EIP-1559) Transaction, the `fieldIdx` corresponds to the index of the field in the RLP list representing the transaction, with the exception that `accessList` at `fieldIdx = 8` is disabled in V2. For other transaction types, we still use the table above to determine the `fieldIdx` corresponding to a transaction field, and the ZK circuit will re-map to the correct RLP list index. +*/ + +/// Circuit and Component Implementation. +pub mod circuit; +/// Types +pub mod types; + +#[cfg(test)] +pub mod tests; + +/// The `fieldIdx` corresponding to the `data` field in a transaction. Note this definition is _independent_ +/// of the transaction type -- it is specific to the subquery spec. +const TX_DATA_FIELD_IDX: usize = 7; diff --git a/axiom-query/src/components/subqueries/transaction/tests.rs b/axiom-query/src/components/subqueries/transaction/tests.rs new file mode 100644 index 00000000..21498771 --- /dev/null +++ b/axiom-query/src/components/subqueries/transaction/tests.rs @@ -0,0 +1,281 @@ +use std::{collections::HashMap, marker::PhantomData, str::FromStr, sync::Arc}; + +use axiom_codec::{ + special_values::{TX_CALLDATA_IDX_OFFSET, TX_FUNCTION_SELECTOR_FIELD_IDX}, + types::{ + field_elements::AnySubqueryResult, + native::{HeaderSubquery, TxSubquery}, + }, +}; +use axiom_eth::{ + block_header::TX_ROOT_INDEX, + halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr}, + keccak::{promise::generate_keccak_shards_from_calls, types::ComponentTypeKeccak}, + mpt::MPTInput, + providers::setup_provider, + transaction::EthTransactionChipParams, + transaction::{calc_max_val_len, EthTransactionProof}, + utils::component::{ + promise_loader::single::PromiseLoaderParams, ComponentCircuit, + ComponentPromiseResultsInMerkle, ComponentType, + }, +}; +use cita_trie::{MemoryDB, PatriciaTrie, Trie}; +use ethers_core::types::{Chain, H256}; +use ethers_providers::Middleware; +use futures::future::join_all; +use hasher::HasherKeccak; +use itertools::Itertools; +use tokio; + +use crate::components::{ + dummy_rlc_circuit_params, + subqueries::{ + block_header::types::{ComponentTypeHeaderSubquery, OutputHeaderShard}, + common::shard_into_component_promise_results, + }, +}; + +use super::{ + circuit::{ComponentCircuitTxSubquery, CoreParamsTxSubquery}, + types::{ + get_tx_key_from_index, BlockWithTransactions, CircuitInputTxShard, CircuitInputTxSubquery, + }, +}; + +/// transaction index is within u16, so rlp(txIndex) is at most 3 bytes => 6 nibbles +pub const TRANSACTION_PROOF_MAX_DEPTH: usize = 6; + +pub struct BlockTransactionsDb { + pub trie: PatriciaTrie, + pub root: H256, + pub tx_rlps: Vec>, +} + +impl BlockTransactionsDb { + pub fn new( + trie: PatriciaTrie, + root: H256, + tx_rlps: Vec>, + ) -> Self { + Self { trie, root, tx_rlps } + } +} + +pub fn construct_tx_tries_from_full_blocks( + blocks: Vec, +) -> anyhow::Result> { + let mut tries = HashMap::new(); + for block in blocks { + let mut trie = + PatriciaTrie::new(Arc::new(MemoryDB::new(true)), Arc::new(HasherKeccak::new())); + let mut tx_rlps = Vec::with_capacity(block.transactions.len()); + for (idx, tx) in block.transactions.into_iter().enumerate() { + let tx_key = get_tx_key_from_index(idx); + let tx_rlp = tx.rlp().to_vec(); + tx_rlps.push(tx_rlp.clone()); + trie.insert(tx_key, tx_rlp)?; + } + // safety check: + let root = trie.root()?; + if root != block.transactions_root.as_bytes() { + anyhow::bail!("Transactions trie incorrectly constructed"); + } + let root = block.transactions_root; + tries.insert(block.number.as_u64(), BlockTransactionsDb::new(trie, root, tx_rlps)); + } + Ok(tries) +} + +async fn test_mock_tx_subqueries( + k: u32, + network: Chain, + subqueries: Vec<(&str, usize)>, // txHash, field_or_calldata_idx + max_data_byte_len: usize, + max_access_list_len: usize, +) -> ComponentCircuitTxSubquery { + let _ = env_logger::builder().is_test(true).try_init(); + + let _provider = setup_provider(network); + let provider = &_provider; + let requests = join_all(subqueries.into_iter().map(|(tx_hash, field_idx)| async move { + let tx_hash = H256::from_str(tx_hash).unwrap(); + let tx = provider.get_transaction(tx_hash).await.unwrap().unwrap(); + let block_number = tx.block_number.unwrap().as_u32(); + let tx_idx = tx.transaction_index.unwrap().as_u32() as u16; + TxSubquery { block_number, tx_idx, field_or_calldata_idx: field_idx as u32 } + })) + .await; + + let block_nums = requests.iter().map(|r| r.block_number).sorted().dedup().collect_vec(); + let blocks = join_all(block_nums.iter().map(|&block_num| async move { + let block = provider.get_block_with_txs(block_num as u64).await.unwrap().unwrap(); + BlockWithTransactions::try_from(block).unwrap() + })) + .await; + + let promise_header = OutputHeaderShard { + results: blocks + .iter() + .map(|block| AnySubqueryResult { + subquery: HeaderSubquery { + block_number: block.number.as_u32(), + field_idx: TX_ROOT_INDEX as u32, + }, + value: block.transactions_root, + }) + .collect(), + }; + + let enable_types = [true, true, true]; + let tx_rlp_max_byte_len = + calc_max_val_len(max_data_byte_len, max_access_list_len, enable_types); + + let tx_tries = construct_tx_tries_from_full_blocks(blocks).unwrap(); + let mut requests_in_circuit = Vec::with_capacity(requests.len()); + for subquery in requests { + let block_number = subquery.block_number as u64; + let tx_idx = subquery.tx_idx as usize; + let tx_key = get_tx_key_from_index(tx_idx); + let db = tx_tries.get(&block_number).unwrap(); + let trie = &db.trie; + let tx_rlps = &db.tx_rlps; + let proof = trie.get_proof(&tx_key).unwrap(); + let value = tx_rlps.get(tx_idx).unwrap(); + let mpt_proof = MPTInput { + path: (&tx_key).into(), + value: value.to_vec(), + root_hash: db.root, + proof, + slot_is_empty: false, + value_max_byte_len: tx_rlp_max_byte_len, + max_depth: TRANSACTION_PROOF_MAX_DEPTH, + max_key_byte_len: 3, + key_byte_len: Some(tx_key.len()), + }; + let tx_proof = EthTransactionProof { tx_index: tx_idx, proof: mpt_proof }; + requests_in_circuit.push(CircuitInputTxSubquery { + block_number, + proof: tx_proof, + field_or_calldata_idx: subquery.field_or_calldata_idx, + }); + } + + let keccak_f_capacity = 200; + let header_capacity = promise_header.results.len(); + + let circuit_params = dummy_rlc_circuit_params(k as usize); + + let chip_params = EthTransactionChipParams { + max_data_byte_len, + max_access_list_len, + enable_types: [true, true, true], + network: None, + }; + let mut circuit = ComponentCircuitTxSubquery::new( + CoreParamsTxSubquery { + chip_params, + capacity: requests_in_circuit.len(), + max_trie_depth: TRANSACTION_PROOF_MAX_DEPTH, + }, + ( + PromiseLoaderParams::new_for_one_shard(keccak_f_capacity), + PromiseLoaderParams::new_for_one_shard(header_capacity), + ), + circuit_params, + ); + + let input = CircuitInputTxShard:: { requests: requests_in_circuit, _phantom: PhantomData }; + circuit.feed_input(Box::new(input)).unwrap(); + circuit.calculate_params(); + + let promises = [ + ( + ComponentTypeKeccak::::get_type_id(), + ComponentPromiseResultsInMerkle::from_single_shard( + generate_keccak_shards_from_calls(&circuit, keccak_f_capacity) + .unwrap() + .into_logical_results(), + ), + ), + ( + ComponentTypeHeaderSubquery::::get_type_id(), + shard_into_component_promise_results::>( + promise_header.into(), + ), + ), + ] + .into_iter() + .collect(); + circuit.fulfill_promise_results(&promises).unwrap(); + + let instances: Vec = circuit.get_public_instances().into(); + MockProver::run(k, &circuit, vec![instances]).unwrap().assert_satisfied(); + circuit +} + +#[tokio::test] +async fn test_mock_tx_subqueries_simple() { + let k = 18; + let subqueries = vec![ + ( + "0x7311924b41863b7d6dbd129dc0890903432a30af831f24697a5c5b522be5088f", + TX_FUNCTION_SELECTOR_FIELD_IDX, + ), // transfer + ( + "0x6a74ccb57011073104c89da78b02229e12c8a4160c5fe1c8278c6a73913ca289", + TX_FUNCTION_SELECTOR_FIELD_IDX, + ), + ( + "0x6a74ccb57011073104c89da78b02229e12c8a4160c5fe1c8278c6a73913ca289", + TX_CALLDATA_IDX_OFFSET, + ), + ( + "0x6a74ccb57011073104c89da78b02229e12c8a4160c5fe1c8278c6a73913ca289", + TX_CALLDATA_IDX_OFFSET + 1, + ), + ( + "0x9524b67377f7ff228fbe31c7edbfb4ba7bb374ceeac54030793b6727d1dc4505", + TX_FUNCTION_SELECTOR_FIELD_IDX, + ), + ]; + test_mock_tx_subqueries(k, Chain::Mainnet, subqueries, 200, 0).await; +} + +#[tokio::test] +async fn test_mock_tx_subqueries_legacy_vrs() { + let k = 18; + let subqueries = vec![ + ("0xbfcf97782f59c868a4f3248fce750488f567f312da4aa6632c64c2fe3cca4d8c", 8), + ("0xbfcf97782f59c868a4f3248fce750488f567f312da4aa6632c64c2fe3cca4d8c", 9), + ("0xbfcf97782f59c868a4f3248fce750488f567f312da4aa6632c64c2fe3cca4d8c", 10), + ("0xbfcf97782f59c868a4f3248fce750488f567f312da4aa6632c64c2fe3cca4d8c", 11), + ]; + test_mock_tx_subqueries(k, Chain::Goerli, subqueries, 4000, 0).await; +} + +#[cfg(feature = "keygen")] +#[tokio::test] +#[ignore] +async fn test_generate_tx_shard_pk() { + use axiom_eth::halo2_base::utils::{fs::read_params, halo2::ProvingKeyGenerator}; + + use crate::keygen::shard::ShardIntentTx; + + let core_params = CoreParamsTxSubquery { + chip_params: EthTransactionChipParams { + max_data_byte_len: 512, + max_access_list_len: 0, + enable_types: [true; 3], + network: None, + }, + capacity: 4, + max_trie_depth: TRANSACTION_PROOF_MAX_DEPTH, + }; + let loader_params = + (PromiseLoaderParams::new_for_one_shard(200), PromiseLoaderParams::new_for_one_shard(8)); + let k = 18; + let intent = ShardIntentTx { core_params, loader_params, k, lookup_bits: 8 }; + let kzg_params = read_params(k); + intent.create_pk_and_pinning(&kzg_params); +} diff --git a/axiom-query/src/components/subqueries/transaction/types.rs b/axiom-query/src/components/subqueries/transaction/types.rs new file mode 100644 index 00000000..80bb3942 --- /dev/null +++ b/axiom-query/src/components/subqueries/transaction/types.rs @@ -0,0 +1,158 @@ +//! Types are separated into: +//! - Circuit metadata that along with the circuit type determines the circuit configuration completely. +//! - Human readable _logical_ input and output to the circuit. These include private inputs and outputs that are only commited to in the public output. +//! - The in-circuit formatted versions of logical inputs and outputs. These include formatting in terms of field elements and accounting for all lengths needing to be fixed at compile time. +//! - We then provide conversion functions from human-readable to circuit formats. +//! - This circuit has no public instances (IO) other than the circuit's own component commitment and the promise commitments from any component calls. +use std::{marker::PhantomData, sync::Arc}; + +use anyhow::Result; +use axiom_codec::{ + types::{field_elements::FieldTxSubquery, native::TxSubquery}, + HiLo, +}; +use axiom_eth::{ + halo2_base::AssignedValue, + impl_fix_len_call_witness, + mpt::MPTInput, + providers::from_hex, + transaction::{calc_max_val_len as tx_calc_max_val_len, EthTransactionProof}, + utils::{ + build_utils::dummy::DummyFrom, + component::{circuit::CoreBuilderInput, ComponentType, ComponentTypeId, LogicalResult}, + }, +}; +use cita_trie::{MemoryDB, PatriciaTrie, Trie}; +use ethers_core::types::{Block, Transaction, H256, U64}; +use hasher::HasherKeccak; +use serde::{Deserialize, Serialize}; + +use crate::{ + components::subqueries::common::OutputSubqueryShard, utils::codec::AssignedTxSubquery, Field, +}; + +use super::circuit::CoreParamsTxSubquery; + +/// Identifier for the component type of this component circuit +pub struct ComponentTypeTxSubquery(PhantomData); + +/// Human readable. +/// The output value of any transaction subquery is always `bytes32` right now. +pub type OutputTxShard = OutputSubqueryShard; + +/// Circuit input for a shard of Tx subqueries. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CircuitInputTxShard { + /// Enriched subquery requests + pub requests: Vec, + pub _phantom: PhantomData, +} + +/// Circuit input for a single Tx subquery. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CircuitInputTxSubquery { + /// The block number to access the storage state at. + pub block_number: u64, + /// Transaction proof formatted as [axiom_eth::mpt::MPTInput]. Contains the transaction index. + pub proof: EthTransactionProof, + /// Special index to specify what subquery value to extract from the transaction. + pub field_or_calldata_idx: u32, +} + +impl DummyFrom for CircuitInputTxShard { + fn dummy_from(core_params: CoreParamsTxSubquery) -> Self { + let CoreParamsTxSubquery { chip_params, capacity, max_trie_depth } = core_params; + let mut trie = + PatriciaTrie::new(Arc::new(MemoryDB::new(true)), Arc::new(HasherKeccak::new())); + let tx = Transaction::default(); + let tx_rlp = tx.rlp().to_vec(); + trie.insert(vec![0x80], tx_rlp.clone()).unwrap(); + let mpt_input = MPTInput { + path: (&[0x80]).into(), + value: tx_rlp, + root_hash: Default::default(), + proof: trie.get_proof(&[0x80]).unwrap(), + value_max_byte_len: tx_calc_max_val_len( + chip_params.max_data_byte_len, + chip_params.max_access_list_len, + chip_params.enable_types, + ), + max_depth: max_trie_depth, + slot_is_empty: false, + max_key_byte_len: 3, + key_byte_len: Some(1), + }; + let tx_pf = EthTransactionProof { tx_index: 0, proof: mpt_input }; + let dummy_subquery = + CircuitInputTxSubquery { block_number: 0, proof: tx_pf, field_or_calldata_idx: 0 }; + Self { requests: vec![dummy_subquery; capacity], _phantom: PhantomData } + } +} + +/// The output value of any storage subquery is always `bytes32` right now. +/// Vector has been resized to the capacity. +pub type CircuitOutputTxShard = OutputSubqueryShard, HiLo>; + +impl_fix_len_call_witness!(FieldTxSubqueryCall, FieldTxSubquery, ComponentTypeTxSubquery); + +// ===== The storage component has no public instances other than the component commitment and promise commitments from external component calls ===== + +impl ComponentType for ComponentTypeTxSubquery { + type InputValue = FieldTxSubquery; + type InputWitness = AssignedTxSubquery; + type OutputValue = HiLo; + type OutputWitness = HiLo>; + type LogicalInput = FieldTxSubquery; + + fn get_type_id() -> ComponentTypeId { + "axiom-query:ComponentTypeTxSubquery".to_string() + } + + fn logical_result_to_virtual_rows_impl( + ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + vec![(ins.input, ins.output)] + } + fn logical_input_to_virtual_rows_impl(li: &Self::LogicalInput) -> Vec { + vec![*li] + } +} + +impl From for CircuitOutputTxShard { + fn from(output: OutputTxShard) -> Self { + output.convert_into() + } +} + +// ===== Block with Transactions ===== +/// A block with all transactions. We require the transactionsRoot to be provided for a safety check. +/// Deserialization should still work on an object with extra fields. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BlockWithTransactions { + /// Block number + pub number: U64, + /// Transactions root hash + pub transactions_root: H256, + /// All transactions in the block + pub transactions: Vec, +} + +impl TryFrom> for BlockWithTransactions { + type Error = &'static str; + fn try_from(block: Block) -> Result { + Ok(Self { + number: block.number.ok_or("Block not in chain")?, + transactions_root: block.transactions_root, + transactions: block.transactions, + }) + } +} + +pub fn get_tx_key_from_index(idx: usize) -> Vec { + let mut tx_key = rlp::encode(&from_hex(&format!("{idx:x}"))).to_vec(); + if idx == 0 { + tx_key = vec![0x80]; + } + tx_key +} diff --git a/axiom-query/src/global_constants.rs b/axiom-query/src/global_constants.rs new file mode 100644 index 00000000..7c039809 --- /dev/null +++ b/axiom-query/src/global_constants.rs @@ -0,0 +1,2 @@ +pub const ENABLE_ALL_TX_TYPES: [bool; 3] = [true, true, true]; +pub const RECEIPT_TOPIC_BOUNDS: (usize, usize) = (0, 4); diff --git a/axiom-query/src/keygen/agg/axiom_agg_1.rs b/axiom-query/src/keygen/agg/axiom_agg_1.rs new file mode 100644 index 00000000..d78d6813 --- /dev/null +++ b/axiom-query/src/keygen/agg/axiom_agg_1.rs @@ -0,0 +1,200 @@ +use std::{collections::BTreeMap, path::Path, sync::Arc}; + +use axiom_eth::{ + halo2_base::gates::circuit::CircuitBuilderStage, + halo2_proofs::{plonk::ProvingKey, poly::kzg::commitment::ParamsKZG}, + halo2curves::bn256::{Bn256, G1Affine}, + snark_verifier::verifier::plonk::PlonkProtocol, + snark_verifier_sdk::{ + halo2::{ + aggregation::AggregationConfigParams, + utils::{ + AggregationDependencyIntent, AggregationDependencyIntentOwned, + KeygenAggregationCircuitIntent, + }, + }, + Snark, + }, + utils::{ + build_utils::{ + aggregation::get_dummy_aggregation_params, + keygen::{compile_agg_dep_to_protocol, read_srs_from_dir}, + pinning::aggregation::AggTreeId, + }, + snark_verifier::EnhancedSnark, + }, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use crate::{ + axiom_aggregation1::types::{InputAxiomAggregation1, FINAL_AGG_VKEY_HASH_IDX}, + keygen::{ + shard::{keccak::ShardIntentKeccak, CircuitIntentVerifyCompute}, + ProvingKeySerializer, SupportedPinning, + }, + subquery_aggregation::types::SUBQUERY_AGGREGATION_AGG_VKEY_HASH_IDX, +}; + +use super::{ + common::{parse_agg_intent, ForceBasicConfigParams}, + impl_keygen_intent_for_aggregation, impl_pkey_serializer_for_aggregation, + single_type::IntentTreeSingleType, + subquery_agg::RecursiveSubqueryAggIntent, +}; + +/// ** !! IMPORTANT !! ** +/// Do not change the order of this enum, which determines how inputs are parsed. +// Determines order of circuit IDs in `to_agg` +#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum AxiomAgg1InputSnark { + VerifyCompute, + SubqueryAgg, + Keccak, +} + +#[serde_as] +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct AxiomAgg1Params { + /// The compiled verification keys of the dependency circuits to aggregate. + /// Since Axiom Aggregation 1 is universal aggregation, we remove the `domain` and `preprocessed` from `PlonkProtocol` since those + /// are loaded as witnesses. + #[serde_as(as = "BTreeMap<_, axiom_eth::utils::snark_verifier::Base64Bytes>")] + pub to_agg: BTreeMap>, + pub agg_params: AggregationConfigParams, +} + +/// Only implements [ProvingKeySerializer] and not [KeygenCircuitIntent]. +#[derive(Serialize, Deserialize)] +pub struct RecursiveAxiomAgg1Intent { + pub intent_verify_compute: CircuitIntentVerifyCompute, + pub intent_subquery_agg: RecursiveSubqueryAggIntent, + pub intent_keccak: IntentTreeSingleType, + pub k: u32, + /// For different versions of this circuit to be aggregated by the same universal aggregation circuit, + /// we may wish to force configure the circuit to have a certain number of columns without auto-configuration. + #[serde(skip_serializing_if = "Option::is_none")] + pub force_params: Option, +} + +/// Non-recursive intent. Currently only used internally as an intermediary for recursive intent. +/// This will implement [KeygenCircuitIntent] where the pinning is not "wrapped" into an enum. +/// The pinning type is `GenericAggPinning`. +#[derive(Clone, Debug)] +struct AxiomAgg1Intent { + // This is from bad UX; only svk = kzg_params.get_g()[0] is used + pub kzg_params: Arc>, + /// For passing to AxiomAgg1Params via macro + pub to_agg: BTreeMap>, + /// Circuit intents for snarks to be aggregated by Axiom Aggregation 1 + pub deps: BTreeMap, + /// The log_2 domain size of the current aggregation circuit + pub k: u32, + /// For different versions of this circuit to be aggregated by the same universal aggregation circuit, + /// we may wish to force configure the circuit to have a certain number of columns without auto-configuration. + pub force_params: Option, +} + +impl AxiomAgg1Intent { + pub fn children(&self) -> Vec { + self.deps.values().map(|(tree, _)| tree.clone()).collect() + } +} + +impl KeygenAggregationCircuitIntent for AxiomAgg1Intent { + fn intent_of_dependencies(&self) -> Vec { + self.deps.values().map(|(_, d)| d.into()).collect() + } + fn build_keygen_circuit_from_snarks(self, snarks: Vec) -> Self::AggregationCircuit { + let mut deps = BTreeMap::from_iter(self.deps.keys().cloned().zip_eq(snarks)); + let snark_verify_compute = deps.remove(&AxiomAgg1InputSnark::VerifyCompute).unwrap(); + let snark_subquery_agg = deps.remove(&AxiomAgg1InputSnark::SubqueryAgg).unwrap(); + let snark_keccak_agg = deps.remove(&AxiomAgg1InputSnark::Keccak).unwrap(); + + // No agg_vkey_hash_idx because the compute_snark is separately tagged by the query_schema + let snark_verify_compute = EnhancedSnark::new(snark_verify_compute, None); + let snark_subquery_agg = + EnhancedSnark::new(snark_subquery_agg, Some(SUBQUERY_AGGREGATION_AGG_VKEY_HASH_IDX)); + let snark_keccak_agg = EnhancedSnark::new(snark_keccak_agg, None); + + let input = + InputAxiomAggregation1 { snark_verify_compute, snark_subquery_agg, snark_keccak_agg }; + let mut force = false; + let agg_params = if let Some(force_params) = self.force_params { + force = true; + force_params.into_agg_params(self.k) + } else { + get_dummy_aggregation_params(self.k as usize) + }; + let mut circuit = + input.build(CircuitBuilderStage::Keygen, agg_params, &self.kzg_params).unwrap(); + if !force { + circuit.calculate_params(Some(20)); + } + circuit + } +} + +impl_keygen_intent_for_aggregation!( + AxiomAgg1Intent, + AxiomAgg1Params, + Some(FINAL_AGG_VKEY_HASH_IDX) +); +impl_pkey_serializer_for_aggregation!(AxiomAgg1Intent, AxiomAgg1Params, AxiomAgg1); + +impl ProvingKeySerializer for RecursiveAxiomAgg1Intent { + fn create_and_serialize_proving_key( + self, + params_dir: &Path, + data_dir: &Path, + ) -> anyhow::Result<(AggTreeId, ProvingKey, SupportedPinning)> { + let mut deps = BTreeMap::new(); + fn process_intent( + key: AxiomAgg1InputSnark, + intent: impl ProvingKeySerializer, + params_dir: &Path, + data_dir: &Path, + deps: &mut BTreeMap, + ) -> anyhow::Result<()> { + let (tree_id, pk, pinning) = + intent.create_and_serialize_proving_key(params_dir, data_dir)?; + deps.insert(key, (tree_id, parse_agg_intent(pk.get_vk(), pinning))); + Ok(()) + } + process_intent( + AxiomAgg1InputSnark::VerifyCompute, + self.intent_verify_compute, + params_dir, + data_dir, + &mut deps, + )?; + process_intent( + AxiomAgg1InputSnark::SubqueryAgg, + self.intent_subquery_agg, + params_dir, + data_dir, + &mut deps, + )?; + process_intent( + AxiomAgg1InputSnark::Keccak, + self.intent_keccak, + params_dir, + data_dir, + &mut deps, + )?; + let kzg_params = Arc::new(read_srs_from_dir(params_dir, self.k)?); + let to_agg = deps + .iter() + .map(|(&k, (_, dep))| (k, compile_agg_dep_to_protocol(&kzg_params, dep, true))) + .collect(); + let axiom_agg1_intent = AxiomAgg1Intent { + to_agg, + deps, + k: self.k, + kzg_params, + force_params: self.force_params, + }; + axiom_agg1_intent.create_and_serialize_proving_key(params_dir, data_dir) + } +} diff --git a/axiom-query/src/keygen/agg/axiom_agg_2.rs b/axiom-query/src/keygen/agg/axiom_agg_2.rs new file mode 100644 index 00000000..937de69e --- /dev/null +++ b/axiom-query/src/keygen/agg/axiom_agg_2.rs @@ -0,0 +1,143 @@ +use std::{path::Path, sync::Arc}; + +use axiom_eth::{ + halo2_base::gates::circuit::CircuitBuilderStage, + halo2_proofs::{plonk::ProvingKey, poly::kzg::commitment::ParamsKZG}, + halo2curves::bn256::{Bn256, G1Affine}, + snark_verifier::verifier::plonk::PlonkProtocol, + snark_verifier_sdk::{ + halo2::{ + aggregation::AggregationConfigParams, + utils::{ + AggregationDependencyIntent, AggregationDependencyIntentOwned, + KeygenAggregationCircuitIntent, + }, + }, + Snark, + }, + utils::{ + build_utils::{ + aggregation::get_dummy_aggregation_params, + keygen::{compile_agg_dep_to_protocol, read_srs_from_dir}, + pinning::aggregation::AggTreeId, + }, + snark_verifier::EnhancedSnark, + }, +}; +use ethers_core::types::Address; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use crate::{ + axiom_aggregation1::types::FINAL_AGG_VKEY_HASH_IDX, + axiom_aggregation2::circuit::InputAxiomAggregation2, + keygen::{ProvingKeySerializer, SupportedPinning}, +}; + +use super::{ + axiom_agg_1::RecursiveAxiomAgg1Intent, + common::{parse_agg_intent, ForceBasicConfigParams}, + impl_keygen_intent_for_aggregation, impl_pkey_serializer_for_aggregation, +}; + +#[serde_as] +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct AxiomAgg2Params { + /// The compiled verification key of the dependency circuit to aggregate. + /// Since Axiom Aggregation 2 is universal aggregation, we remove the `domain` and `preprocessed` from `PlonkProtocol` since those + /// are loaded as witnesses. + #[serde_as(as = "Box")] + pub to_agg: Box>, + pub agg_params: AggregationConfigParams, +} + +/// Only implements [ProvingKeySerializer] and not [KeygenCircuitIntent]. +#[derive(Serialize, Deserialize)] +pub struct RecursiveAxiomAgg2Intent { + pub axiom_agg1_intent: RecursiveAxiomAgg1Intent, + pub k: u32, + /// Force the number of columns in the axiom agg 2 circuit. + #[serde(skip_serializing_if = "Option::is_none")] + pub force_params: Option, +} + +/// Non-recursive intent. Currently only used internally as an intermediary for recursive intent. +/// This will implement [KeygenCircuitIntent] where the pinning is not "wrapped" into an enum. +/// The pinning type is `GenericAggPinning`. +#[derive(Clone, Debug)] +struct AxiomAgg2Intent { + // This is from bad UX; only svk = kzg_params.get_g()[0] is used + pub kzg_params: Arc>, + /// Circuit ID of the Axiom Aggregation 1 circuit + pub child: AggTreeId, + /// For passing to AxiomAgg2Params via macro + pub to_agg: Box>, + /// Circuit intent for Axiom Aggregation 1 circuit to be aggregated by Axiom Aggregation 2 + pub axiom_agg1_intent: AggregationDependencyIntentOwned, + /// The log_2 domain size of the current aggregation circuit + pub k: u32, + /// Force the number of columns in the axiom agg 2 circuit. + pub force_params: Option, +} + +impl AxiomAgg2Intent { + pub fn children(&self) -> Vec { + vec![self.child.clone()] + } +} + +impl KeygenAggregationCircuitIntent for AxiomAgg2Intent { + fn intent_of_dependencies(&self) -> Vec { + vec![(&self.axiom_agg1_intent).into()] + } + fn build_keygen_circuit_from_snarks(self, mut snarks: Vec) -> Self::AggregationCircuit { + let snark_axiom_agg1 = snarks.pop().unwrap(); + let snark_axiom_agg1 = EnhancedSnark::new(snark_axiom_agg1, Some(FINAL_AGG_VKEY_HASH_IDX)); + + let payee = Address::zero(); // dummy + let input = InputAxiomAggregation2 { snark_axiom_agg1, payee }; + let mut force = false; + let agg_params = if let Some(force_params) = self.force_params { + force = true; + force_params.into_agg_params(self.k) + } else { + get_dummy_aggregation_params(self.k as usize) + }; + let mut circuit = + input.build(CircuitBuilderStage::Keygen, agg_params, &self.kzg_params).unwrap(); + if !force { + circuit.calculate_params(Some(20)); + } + circuit + } +} + +impl_keygen_intent_for_aggregation!( + AxiomAgg2Intent, + AxiomAgg2Params, + Some(FINAL_AGG_VKEY_HASH_IDX) +); +impl_pkey_serializer_for_aggregation!(AxiomAgg2Intent, AxiomAgg2Params, AxiomAgg2); + +impl ProvingKeySerializer for RecursiveAxiomAgg2Intent { + fn create_and_serialize_proving_key( + self, + params_dir: &Path, + data_dir: &Path, + ) -> anyhow::Result<(AggTreeId, ProvingKey, SupportedPinning)> { + let (agg1_tree_id, pk, pinning) = + self.axiom_agg1_intent.create_and_serialize_proving_key(params_dir, data_dir)?; + let axiom_agg1_intent = parse_agg_intent(pk.get_vk(), pinning); + let kzg_params = Arc::new(read_srs_from_dir(params_dir, self.k)?); + let to_agg = compile_agg_dep_to_protocol(&kzg_params, &axiom_agg1_intent, true); + let axiom_agg2_intent = AxiomAgg2Intent { + kzg_params, + child: agg1_tree_id, + axiom_agg1_intent, + to_agg: Box::new(to_agg), + k: self.k, + force_params: self.force_params, + }; + axiom_agg2_intent.create_and_serialize_proving_key(params_dir, data_dir) + } +} diff --git a/axiom-query/src/keygen/agg/common.rs b/axiom-query/src/keygen/agg/common.rs new file mode 100644 index 00000000..61263052 --- /dev/null +++ b/axiom-query/src/keygen/agg/common.rs @@ -0,0 +1,105 @@ +use axiom_eth::{ + halo2_proofs::plonk::VerifyingKey, + halo2curves::{ + bn256::{Fr, G1Affine}, + ff::PrimeField, + }, + snark_verifier_sdk::halo2::{ + aggregation::AggregationConfigParams, utils::AggregationDependencyIntentOwned, + }, + utils::build_utils::pinning::aggregation::GenericAggPinning, +}; +use enum_dispatch::enum_dispatch; +use hex::FromHex; +use serde::{Deserialize, Serialize}; + +/// Fields of [`AggregationConfigParams`] besides `degree` and `lookup_bits`. +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub struct ForceBasicConfigParams { + pub num_advice: usize, + pub num_lookup_advice: usize, + pub num_fixed: usize, +} + +impl ForceBasicConfigParams { + pub fn into_agg_params(self, k: u32) -> AggregationConfigParams { + AggregationConfigParams { + degree: k, + lookup_bits: k as usize - 1, + num_advice: self.num_advice, + num_lookup_advice: self.num_lookup_advice, + num_fixed: self.num_fixed, + } + } +} + +// enum_dispatch cannot cross crates, so we need to define the trait here +/// Trait for a pinning for a node in the aggregation tree. It may or may not be for an aggregation circuit. +#[enum_dispatch] +pub trait AggTreePinning { + fn num_instance(&self) -> Vec; + fn accumulator_indices(&self) -> Option>; + /// Aggregate vk hash, if universal aggregation circuit. + /// * ((i, j), agg_vkey_hash), where the hash is located at (i, j) in the public instance columns + fn agg_vk_hash_data(&self) -> Option<((usize, usize), axiom_eth::halo2curves::bn256::Fr)>; +} + +pub fn parse_agg_intent( + vk: &VerifyingKey, + pinning: impl AggTreePinning, +) -> AggregationDependencyIntentOwned { + let num_instance = pinning.num_instance(); + let accumulator_indices = pinning.accumulator_indices(); + AggregationDependencyIntentOwned { + vk: vk.clone(), + num_instance, + accumulator_indices, + agg_vk_hash_data: pinning.agg_vk_hash_data(), + } +} + +impl AggTreePinning for GenericAggPinning { + fn num_instance(&self) -> Vec { + self.num_instance.clone() + } + fn accumulator_indices(&self) -> Option> { + Some(self.accumulator_indices.clone()) + } + fn agg_vk_hash_data(&self) -> Option<((usize, usize), Fr)> { + // agg_vk_hash in pinning is represented in big endian for readability + self.agg_vk_hash_data.as_ref().map(|((i, j), hash_str)| { + assert_eq!(&hash_str[..2], "0x"); + let mut bytes_be = Vec::from_hex(&hash_str[2..]).unwrap(); + bytes_be.reverse(); + let bytes_le = bytes_be; + let agg_vkey_hash = Fr::from_repr(bytes_le.try_into().unwrap()).unwrap(); + ((*i, *j), agg_vkey_hash) + }) + } +} + +#[cfg(test)] +#[test] +fn test_parse_agg_intent() { + use axiom_eth::{ + halo2curves::{bn256::G2Affine, ff::Field}, + utils::build_utils::{ + aggregation::get_dummy_aggregation_params, pinning::aggregation::GenericAggParams, + }, + }; + use rand::rngs::StdRng; + use rand_core::SeedableRng; + + let params = GenericAggParams { to_agg: vec![], agg_params: get_dummy_aggregation_params(10) }; + let mut rng = StdRng::seed_from_u64(0); + let agg_vk_hash = Fr::random(&mut rng); + let agg_pinning = GenericAggPinning { + params, + num_instance: vec![1], + accumulator_indices: (0..12).map(|j| (0, j)).collect(), + agg_vk_hash_data: Some(((0, 0), format!("{:?}", agg_vk_hash))), + dk: (G1Affine::generator(), G2Affine::generator(), G2Affine::random(&mut rng)).into(), + break_points: Default::default(), + }; + assert_eq!(agg_pinning.agg_vk_hash_data(), Some(((0, 0), agg_vk_hash))); +} diff --git a/axiom-query/src/keygen/agg/mod.rs b/axiom-query/src/keygen/agg/mod.rs new file mode 100644 index 00000000..b8002d86 --- /dev/null +++ b/axiom-query/src/keygen/agg/mod.rs @@ -0,0 +1,127 @@ +use axiom_eth::utils::build_utils::pinning::aggregation::{GenericAggParams, GenericAggPinning}; +use enum_dispatch::enum_dispatch; +use serde::{Deserialize, Serialize}; + +use self::{ + axiom_agg_1::AxiomAgg1Params, axiom_agg_2::AxiomAgg2Params, common::AggTreePinning, + subquery_agg::SubqueryAggParams, +}; + +pub mod axiom_agg_1; +pub mod axiom_agg_2; +pub mod common; +pub mod single_type; +pub mod subquery_agg; + +/// ** !! IMPORTANT !! ** +/// Enum names are used to deserialize the pinning file. Please be careful if you need renaming. +#[derive(Serialize, Deserialize, Clone)] +#[enum_dispatch(AggTreePinning)] +pub enum SupportedAggPinning { + AxiomAgg1(GenericAggPinning), + AxiomAgg2(GenericAggPinning), + SingleTypeAggregation(GenericAggPinning), + SubqueryAggregation(GenericAggPinning), +} + +/// # Assumptions +/// * $agg_params must have exactly the fields `to_agg, svk, agg_params`. +/// * $agg_intent must have a `to_agg` field of exactly the same type as $agg_params. +macro_rules! impl_keygen_intent_for_aggregation { + ($agg_intent:ty, $agg_params:ident, $agg_vk_hash_idx:expr) => { + impl + axiom_eth::halo2_base::utils::halo2::KeygenCircuitIntent< + axiom_eth::halo2curves::bn256::Fr, + > for $agg_intent + { + type ConcreteCircuit = + axiom_eth::snark_verifier_sdk::halo2::aggregation::AggregationCircuit; + /// We omit here tags (e.g., hash of vkeys) of the dependencies, they should be recorded separately. + /// * The first coordinate is the `svk = kzg_params.get_g()[0]` of the KZG trusted setup used. + /// * The second coordinate is `num_instance`. + type Pinning = + axiom_eth::utils::build_utils::pinning::aggregation::GenericAggPinning<$agg_params>; + fn get_k(&self) -> u32 { + self.k + } + fn build_keygen_circuit(self) -> Self::ConcreteCircuit { + self.build_keygen_circuit_shplonk() + } + fn get_pinning_after_keygen( + self, + kzg_params: &axiom_eth::halo2_proofs::poly::kzg::commitment::ParamsKZG< + axiom_eth::halo2curves::bn256::Bn256, + >, + circuit: &Self::ConcreteCircuit, + ) -> Self::Pinning { + use axiom_eth::halo2_proofs::poly::commitment::ParamsProver; + use axiom_eth::snark_verifier_sdk::CircuitExt; + use axiom_eth::utils::build_utils::pinning::{ + aggregation::GenericAggPinning, CircuitPinningInstructions, + }; + use axiom_eth::utils::snark_verifier::NUM_FE_ACCUMULATOR; + let pinning = circuit.pinning(); + let agg_params = $agg_params { to_agg: self.to_agg, agg_params: pinning.params }; + let svk = kzg_params.get_g()[0]; + let dk = (svk, kzg_params.g2(), kzg_params.s_g2()); + let agg_vk_hash_data = $agg_vk_hash_idx.map(|idx| { + let i = 0; + let j = NUM_FE_ACCUMULATOR + idx; + let agg_vk_hash = format!("{:?}", circuit.instances()[i][j]); + ((i, j), agg_vk_hash) + }); + GenericAggPinning { + params: agg_params, + num_instance: circuit.num_instance(), + accumulator_indices: Self::ConcreteCircuit::accumulator_indices().unwrap(), + agg_vk_hash_data, + dk: dk.into(), + break_points: pinning.break_points, + } + } + } + }; +} + +/// A macro to auto-implement ProvingKeySerializer, where you specify which enum variant of SupportedAggPinning you want to use via $agg_pinning_name. +/// # Assumptions +/// * $agg_intent must have a `children()` function that returns `Vec` to give the child aggregation trees. +macro_rules! impl_pkey_serializer_for_aggregation { + ($agg_intent:ty, $agg_params:ty, $agg_pinning_name:ident) => { + impl crate::keygen::ProvingKeySerializer for $agg_intent { + fn create_and_serialize_proving_key( + self, + params_dir: &std::path::Path, + data_dir: &std::path::Path, + ) -> anyhow::Result<( + axiom_eth::utils::build_utils::pinning::aggregation::AggTreeId, + axiom_eth::halo2_proofs::plonk::ProvingKey, + crate::keygen::SupportedPinning, + )> { + use crate::keygen::{agg::SupportedAggPinning, SupportedPinning}; + use axiom_eth::halo2_base::utils::halo2::{ + KeygenCircuitIntent, ProvingKeyGenerator, + }; + use axiom_eth::utils::build_utils::{ + keygen::{read_srs_from_dir, write_pk_and_pinning}, + pinning::aggregation::{AggTreeId, GenericAggPinning}, + }; + let k = self.get_k(); + let children = self.children(); + let kzg_params = read_srs_from_dir(params_dir, k)?; + let (pk, pinning_json) = self.create_pk_and_pinning(&kzg_params); + let pinning: GenericAggPinning<$agg_params> = serde_json::from_value(pinning_json)?; + let aggregate_vk_hash = pinning.agg_vk_hash_data.as_ref().map(|x| x.1.clone()); + let pinning = + SupportedPinning::Agg(SupportedAggPinning::$agg_pinning_name(pinning)); + let circuit_id = + write_pk_and_pinning(data_dir, &pk, &serde_json::to_value(&pinning)?)?; + let tree_id = AggTreeId { circuit_id, children, aggregate_vk_hash }; + Ok((tree_id, pk, pinning)) + } + } + }; +} + +pub(crate) use impl_keygen_intent_for_aggregation; +pub(crate) use impl_pkey_serializer_for_aggregation; diff --git a/axiom-query/src/keygen/agg/single_type.rs b/axiom-query/src/keygen/agg/single_type.rs new file mode 100644 index 00000000..6194ecdd --- /dev/null +++ b/axiom-query/src/keygen/agg/single_type.rs @@ -0,0 +1,110 @@ +use std::{path::Path, sync::Arc}; + +use axiom_eth::{ + halo2_base::utils::halo2::ProvingKeyGenerator, + halo2_proofs::plonk::ProvingKey, + halo2curves::bn256::G1Affine, + utils::{ + build_utils::{ + keygen::{read_srs_from_dir, write_pk_and_pinning}, + pinning::aggregation::{AggTreeId, GenericAggParams, GenericAggPinning}, + }, + merkle_aggregation::keygen::AggIntentMerkle, + }, +}; +use enum_dispatch::enum_dispatch; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +use crate::keygen::{ + shard::{ + ShardIntentAccount, ShardIntentHeader, ShardIntentReceipt, ShardIntentResultsRoot, + ShardIntentSolidityMapping, ShardIntentStorage, ShardIntentTx, + }, + ProvingKeySerializer, SupportedPinning, +}; + +use super::{common::parse_agg_intent, SupportedAggPinning}; + +#[derive(Serialize, Deserialize)] +#[enum_dispatch(ProvingKeySerializer)] +pub enum SupportedIntentTreeSingleType { + Header(IntentTreeSingleType), + Account(IntentTreeSingleType), + Storage(IntentTreeSingleType), + Tx(IntentTreeSingleType), + Receipt(IntentTreeSingleType), + SolidityMapping(IntentTreeSingleType), + ResultsRoot(IntentTreeSingleType), +} + +/// Node in tree of aggregation intents. This is a complete tree +/// where the leaves all have the same configuration intent. +/// +/// If we have a recursive chain `node0 -> node1 -> ... -> node_{m-1} -> leaf`, +/// then the tree has depth `m` and `node0.num_children * node1.num_children * ... * node_{m-1}.num_children` total leaves. +#[derive(Clone, Serialize, Deserialize)] +pub enum IntentTreeSingleType { + Leaf(LeafIntent), + Node(IntentNodeSingleType), +} + +/// Node in aggregation tree which has `num_children` children (dependencies). +/// Each child has the same intent `child_intent`. +/// This struct is typeless and needs to be deserialized. +/// +/// Typical use case: deserialize `child_intent` to `IntentTreeSingleType` +/// for recursive struct. +#[derive(Clone, Serialize, Deserialize)] +pub struct IntentNodeSingleType { + /// log_2 domain size of the current aggregation circuit + pub k: u32, + /// Must be a power of 2 + pub num_children: usize, + pub child_intent: serde_json::Value, +} + +impl ProvingKeySerializer for IntentTreeSingleType +where + LeafIntent: ProvingKeySerializer + DeserializeOwned, +{ + /// ## Assumptions + /// - All pinnings have a "num_instance" field. + fn create_and_serialize_proving_key( + self, + params_dir: &Path, + data_dir: &Path, + ) -> anyhow::Result<(AggTreeId, ProvingKey, SupportedPinning)> { + match self { + IntentTreeSingleType::Leaf(intent) => { + intent.create_and_serialize_proving_key(params_dir, data_dir) + } + IntentTreeSingleType::Node(node) => { + let child_intent: IntentTreeSingleType = + serde_json::from_value(node.child_intent).unwrap(); + // Recursive call + let (child_id, child_pk, child_pinning) = + child_intent.create_and_serialize_proving_key(params_dir, data_dir)?; + let child_intent = parse_agg_intent(child_pk.get_vk(), child_pinning); + let kzg_params = Arc::new(read_srs_from_dir(params_dir, node.k)?); + let agg_intent = AggIntentMerkle { + kzg_params: kzg_params.clone(), + to_agg: vec![child_id.clone(); node.num_children], + deps: vec![child_intent; node.num_children], + k: node.k, + }; + let (pk, pinning_json) = agg_intent.create_pk_and_pinning(&kzg_params); + let pinning: GenericAggPinning = + serde_json::from_value(pinning_json)?; + let pinning = + SupportedPinning::Agg(SupportedAggPinning::SingleTypeAggregation(pinning)); + let circuit_id = write_pk_and_pinning(data_dir, &pk, &pinning)?; + let tree_id = AggTreeId { + circuit_id, + children: vec![child_id; node.num_children], + aggregate_vk_hash: None, + }; + Ok((tree_id, pk, pinning)) + } + } + } +} diff --git a/axiom-query/src/keygen/agg/subquery_agg.rs b/axiom-query/src/keygen/agg/subquery_agg.rs new file mode 100644 index 00000000..b66429fb --- /dev/null +++ b/axiom-query/src/keygen/agg/subquery_agg.rs @@ -0,0 +1,199 @@ +use std::{collections::BTreeMap, path::Path, sync::Arc}; + +use axiom_eth::{ + halo2_base::gates::circuit::CircuitBuilderStage, + halo2_proofs::{plonk::ProvingKey, poly::kzg::commitment::ParamsKZG}, + halo2curves::bn256::{Bn256, Fr, G1Affine}, + snark_verifier::verifier::plonk::PlonkProtocol, + snark_verifier_sdk::{ + halo2::{ + aggregation::AggregationConfigParams, + utils::{ + AggregationDependencyIntent, AggregationDependencyIntentOwned, + KeygenAggregationCircuitIntent, + }, + }, + Snark, + }, + utils::{ + build_utils::{ + aggregation::get_dummy_aggregation_params, + keygen::{compile_agg_dep_to_protocol, read_srs_from_dir}, + pinning::aggregation::{AggTreeId, GenericAggPinning}, + }, + snark_verifier::EnhancedSnark, + }, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use crate::{ + keygen::{ProvingKeySerializer, SupportedPinning}, + subquery_aggregation::types::{ + InputSubqueryAggregation, SUBQUERY_AGGREGATION_AGG_VKEY_HASH_IDX, + }, +}; + +use super::{ + common::{parse_agg_intent, ForceBasicConfigParams}, + impl_keygen_intent_for_aggregation, impl_pkey_serializer_for_aggregation, + single_type::SupportedIntentTreeSingleType, +}; + +/// ** !! IMPORTANT !! ** +/// Do not change the order of this enum, which determines how inputs are parsed. +// Determines order of circuit IDs in `to_agg` +#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum SubqueryAggInputSnark { + Header, + Account, + Storage, + Tx, + Receipt, + SolidityMapping, + ResultsRoot, +} + +#[serde_as] +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct SubqueryAggParams { + /// The compiled verification keys of the dependency circuits to aggregate. + /// Since Subquery Aggregation is universal aggregation, we remove the `domain` and `preprocessed` from `PlonkProtocol` since those + /// are loaded as witness es. + #[serde_as(as = "BTreeMap<_, axiom_eth::utils::snark_verifier::Base64Bytes>")] + pub to_agg: BTreeMap>, + pub agg_params: AggregationConfigParams, +} + +pub type SubqueryAggPinning = GenericAggPinning; + +/// Only implements [ProvingKeySerializer] and not [KeygenCircuitIntent]. +#[derive(Serialize, Deserialize)] +pub struct RecursiveSubqueryAggIntent { + pub deps: Vec, + pub k: u32, + /// For different versions of this circuit to be aggregated by the same universal aggregation circuit, + /// we may wish to force configure the circuit to have a certain number of columns without auto-configuration. + #[serde(skip_serializing_if = "Option::is_none")] + pub force_params: Option, +} + +/// Non-recursive intent. Currently only used internally as an intermediary for recursive intent. +/// This will implement [KeygenCircuitIntent] where the pinning is not "wrapped" into an enum. +/// The pinning type is `GenericAggPinning`. +#[derive(Clone, Debug)] +struct SubqueryAggIntent { + // This is from bad UX; only svk = kzg_params.get_g()[0] is used + pub kzg_params: Arc>, + /// For passing to SubqueryAggParams via macro + pub to_agg: BTreeMap>, + /// Circuit intents for subset of circuit types that are enabled + pub deps: BTreeMap, + /// The log_2 domain size of the current aggregation circuit + pub k: u32, + /// For different versions of this circuit to be aggregated by the same universal aggregation circuit, + /// we may wish to force configure the circuit to have a certain number of columns without auto-configuration. + pub force_params: Option, +} + +impl SubqueryAggIntent { + pub fn children(&self) -> Vec { + self.deps.values().map(|(tree, _)| tree.clone()).collect() + } +} + +impl KeygenAggregationCircuitIntent for SubqueryAggIntent { + fn intent_of_dependencies(&self) -> Vec { + self.deps.values().map(|(_, d)| d.into()).collect() + } + fn build_keygen_circuit_from_snarks(self, snarks: Vec) -> Self::AggregationCircuit { + let mut deps = BTreeMap::from_iter(self.deps.keys().cloned().zip_eq(snarks)); + let mut remove_and_wrap = + |k: &SubqueryAggInputSnark| deps.remove(k).map(|s| EnhancedSnark::new(s, None)); + // TODO: don't do manual conversion + let snark_header = remove_and_wrap(&SubqueryAggInputSnark::Header); + let snark_results_root = remove_and_wrap(&SubqueryAggInputSnark::ResultsRoot); + let snark_account = remove_and_wrap(&SubqueryAggInputSnark::Account); + let snark_storage = remove_and_wrap(&SubqueryAggInputSnark::Storage); + let snark_tx = remove_and_wrap(&SubqueryAggInputSnark::Tx); + let snark_receipt = remove_and_wrap(&SubqueryAggInputSnark::Receipt); + let snark_solidity_mapping = remove_and_wrap(&SubqueryAggInputSnark::SolidityMapping); + let promise_commit_keccak = Fr::zero(); // just a dummy + + let input = InputSubqueryAggregation { + snark_header: snark_header.unwrap(), + snark_account, + snark_storage, + snark_solidity_mapping, + snark_tx, + snark_receipt, + promise_commit_keccak, + snark_results_root: snark_results_root.unwrap(), + }; + let mut force = false; + let agg_params = if let Some(force_params) = self.force_params { + force = true; + force_params.into_agg_params(self.k) + } else { + get_dummy_aggregation_params(self.k as usize) + }; + let mut circuit = + input.build(CircuitBuilderStage::Keygen, agg_params, &self.kzg_params).unwrap(); + if !force { + circuit.calculate_params(Some(20)); + } + circuit + } +} + +impl_keygen_intent_for_aggregation!( + SubqueryAggIntent, + SubqueryAggParams, + Some(SUBQUERY_AGGREGATION_AGG_VKEY_HASH_IDX) +); +impl_pkey_serializer_for_aggregation!(SubqueryAggIntent, SubqueryAggParams, SubqueryAggregation); + +fn get_key(supported: &SupportedIntentTreeSingleType) -> SubqueryAggInputSnark { + type S = SupportedIntentTreeSingleType; + type I = SubqueryAggInputSnark; + match supported { + S::Header(_) => I::Header, + S::Account(_) => I::Account, + S::Storage(_) => I::Storage, + S::Tx(_) => I::Tx, + S::Receipt(_) => I::Receipt, + S::SolidityMapping(_) => I::SolidityMapping, + S::ResultsRoot(_) => I::ResultsRoot, + } +} + +impl ProvingKeySerializer for RecursiveSubqueryAggIntent { + fn create_and_serialize_proving_key( + self, + params_dir: &Path, + data_dir: &Path, + ) -> anyhow::Result<(AggTreeId, ProvingKey, SupportedPinning)> { + let mut deps = BTreeMap::new(); + for intent in self.deps { + let key = get_key(&intent); + let (child_tree_id, pk, pinning) = + intent.create_and_serialize_proving_key(params_dir, data_dir)?; + let intent = parse_agg_intent(pk.get_vk(), pinning); + assert!(deps.insert(key, (child_tree_id, intent)).is_none()); + } + let kzg_params = Arc::new(read_srs_from_dir(params_dir, self.k)?); + let to_agg = deps + .iter() + .map(|(&k, (_, dep))| (k, compile_agg_dep_to_protocol(&kzg_params, dep, true))) + .collect(); + let subquery_agg_intent = SubqueryAggIntent { + to_agg, + deps, + k: self.k, + kzg_params, + force_params: self.force_params, + }; + subquery_agg_intent.create_and_serialize_proving_key(params_dir, data_dir) + } +} diff --git a/axiom-query/src/keygen/mod.rs b/axiom-query/src/keygen/mod.rs new file mode 100644 index 00000000..3b7d4963 --- /dev/null +++ b/axiom-query/src/keygen/mod.rs @@ -0,0 +1,60 @@ +//! Module with utility functions to generate proving keys and verifying keys for production circuits. + +use std::path::Path; + +use axiom_eth::{ + halo2_proofs::plonk::ProvingKey, halo2curves::bn256::G1Affine, + utils::build_utils::pinning::aggregation::AggTreeId, +}; +use enum_dispatch::enum_dispatch; +use serde::{Deserialize, Serialize}; + +use self::{ + agg::{ + axiom_agg_1::RecursiveAxiomAgg1Intent, axiom_agg_2::RecursiveAxiomAgg2Intent, + common::AggTreePinning, single_type::*, subquery_agg::RecursiveSubqueryAggIntent, + SupportedAggPinning, + }, + shard::{keccak::ShardIntentKeccak, *}, +}; + +pub mod agg; +pub mod shard; + +pub type CircuitId = String; + +#[derive(Serialize, Deserialize)] +#[enum_dispatch(ProvingKeySerializer)] +pub enum SupportedRecursiveIntent { + Subquery(SupportedIntentTreeSingleType), + VerifyCompute(CircuitIntentVerifyCompute), + SubqueryAgg(RecursiveSubqueryAggIntent), + Keccak(IntentTreeSingleType), + AxiomAgg1(RecursiveAxiomAgg1Intent), + AxiomAgg2(RecursiveAxiomAgg2Intent), +} + +/// ** !! IMPORTANT !! ** +/// Enum names are used to deserialize the pinning file. Please be careful if you need renaming. +#[derive(Serialize, Deserialize, Clone)] +#[enum_dispatch(AggTreePinning)] +pub enum SupportedPinning { + Shard(SupportedShardPinning), + Agg(SupportedAggPinning), +} + +// We only serialize to [SupportedPinning] so the JSON will have the name of the pinning in it. +/// Trait specific to this crate for keygen since it uses enum_dispatch and must return a pinning in enum [SupportedPinning]. +#[enum_dispatch] +pub trait ProvingKeySerializer: Sized { + /// Recursively creates and serializes proving keys and pinnings. + /// + /// Computes `circuit_id` as the blake3 hash of the halo2 VerifyingKey written to bytes. Writes proving key to `circuit_id.pk`, verifying key to `circuit_id.vk` and pinning to `circuit_id.json` in the `data_dir` directory. + /// + /// Returns the `circuit_id, proving_key, pinning`. + fn create_and_serialize_proving_key( + self, + params_dir: &Path, + data_dir: &Path, + ) -> anyhow::Result<(AggTreeId, ProvingKey, SupportedPinning)>; +} diff --git a/axiom-query/src/keygen/shard/keccak.rs b/axiom-query/src/keygen/shard/keccak.rs new file mode 100644 index 00000000..4fe36fc3 --- /dev/null +++ b/axiom-query/src/keygen/shard/keccak.rs @@ -0,0 +1,95 @@ +use axiom_eth::{ + halo2_base::utils::halo2::KeygenCircuitIntent, + halo2_proofs::{ + plonk::Circuit, + poly::{commitment::ParamsProver, kzg::commitment::ParamsKZG}, + }, + halo2curves::bn256::Bn256, + rlc::virtual_region::RlcThreadBreakPoints, + utils::{ + component::circuit::{CoreBuilderOutputParams, CoreBuilderParams}, + keccak::get_keccak_unusable_rows_from_capacity, + }, + zkevm_hashes::{ + keccak::component::circuit::shard::{ + KeccakComponentShardCircuit, KeccakComponentShardCircuitParams, + }, + util::eth_types::Field as RawField, + }, +}; +use serde::{Deserialize, Serialize}; + +use super::{ComponentShardPinning, Fr}; + +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct ShardIntentKeccak { + pub core_params: CoreParamsKeccak, + pub k: u32, +} + +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct CoreParamsKeccak { + pub capacity: usize, +} + +// Not sure this is needed +impl CoreBuilderParams for CoreParamsKeccak { + fn get_output_params(&self) -> CoreBuilderOutputParams { + CoreBuilderOutputParams::new(vec![self.capacity]) + } +} + +impl KeygenCircuitIntent for ShardIntentKeccak { + type ConcreteCircuit = KeccakComponentShardCircuit; + type Pinning = ComponentShardPinning; + + fn get_k(&self) -> u32 { + self.k + } + fn build_keygen_circuit(self) -> Self::ConcreteCircuit { + let circuit_params = KeccakComponentShardCircuitParams::new( + self.k as usize, + 0, // unusable_rows will be recalculated later in tuning + self.core_params.capacity, + false, + ); + let mut circuit = KeccakComponentShardCircuit::new(vec![], circuit_params, false); + tune_keccak_component_shard_circuit(&mut circuit); + circuit + } + fn get_pinning_after_keygen( + self, + kzg_params: &ParamsKZG, + circuit: &Self::ConcreteCircuit, + ) -> Self::Pinning { + let break_points = circuit.base_circuit_break_points(); + assert_eq!(break_points.len(), 1); + let svk = kzg_params.get_g()[0]; + let dk = (svk, kzg_params.g2(), kzg_params.s_g2()); + ComponentShardPinning { + params: circuit.params(), + num_instance: vec![1], // keccak component shard only has 1 instance + break_points: RlcThreadBreakPoints { base: break_points, rlc: vec![] }, + dk: dk.into(), + } + } +} + +/// Finds and sets optimal configuration parameters for [KeccakComponentShardCircuit]. +pub fn tune_keccak_component_shard_circuit( + circuit: &mut KeccakComponentShardCircuit, +) { + let circuit_params = circuit.params(); + let k = circuit_params.k(); + let capacity = circuit_params.capacity(); + let (unusable, _) = get_keccak_unusable_rows_from_capacity(k, capacity); + let mut circuit_params = KeccakComponentShardCircuitParams::new( + k, + unusable, + capacity, + circuit_params.publish_raw_outputs(), + ); + circuit_params.base_circuit_params = + KeccakComponentShardCircuit::::calculate_base_circuit_params(&circuit_params); + *circuit.params_mut() = circuit_params; +} diff --git a/axiom-query/src/keygen/shard/mod.rs b/axiom-query/src/keygen/shard/mod.rs new file mode 100644 index 00000000..d8021e86 --- /dev/null +++ b/axiom-query/src/keygen/shard/mod.rs @@ -0,0 +1,299 @@ +use std::{any::TypeId, path::Path}; + +use axiom_eth::{ + halo2_base::{ + gates::circuit::CircuitBuilderStage, + utils::halo2::{KeygenCircuitIntent, ProvingKeyGenerator}, + }, + halo2_proofs::{ + plonk::{Circuit, ProvingKey}, + poly::{commitment::ParamsProver, kzg::commitment::ParamsKZG}, + }, + halo2curves::bn256::{Bn256, Fr, G1Affine}, + rlc::virtual_region::RlcThreadBreakPoints, + snark_verifier::pcs::kzg::KzgDecidingKey, + snark_verifier_sdk::CircuitExt, + utils::{ + build_utils::{ + aggregation::CircuitMetadata, + dummy::DummyFrom, + keygen::{get_dummy_rlc_circuit_params, read_srs_from_dir, write_pk_and_pinning}, + pinning::aggregation::AggTreeId, + }, + component::{ + circuit::{ + ComponentCircuitImpl, CoreBuilder, CoreBuilderInput, CoreBuilderParams, + PromiseBuilder, + }, + promise_loader::utils::DummyPromiseBuilder, + ComponentCircuit, + }, + }, + zkevm_hashes::keccak::component::circuit::shard::KeccakComponentShardCircuit, +}; +use enum_dispatch::enum_dispatch; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +use crate::{ + components::{ + results::circuit::{ + ComponentCircuitResultsRoot, CoreBuilderResultsRoot, PromiseLoaderResultsRoot, + }, + subqueries::{ + account::circuit::{ + ComponentCircuitAccountSubquery, CoreBuilderAccountSubquery, + PromiseLoaderAccountSubquery, + }, + block_header::circuit::{ + ComponentCircuitHeaderSubquery, CoreBuilderHeaderSubquery, + PromiseLoaderHeaderSubquery, + }, + receipt::circuit::{ + ComponentCircuitReceiptSubquery, CoreBuilderReceiptSubquery, + PromiseLoaderReceiptSubquery, + }, + solidity_mappings::circuit::{ + ComponentCircuitSolidityNestedMappingSubquery, + CoreBuilderSolidityNestedMappingSubquery, + PromiseLoaderSolidityNestedMappingSubquery, + }, + storage::circuit::{ + ComponentCircuitStorageSubquery, CoreBuilderStorageSubquery, + PromiseLoaderStorageSubquery, + }, + transaction::circuit::{ + ComponentCircuitTxSubquery, CoreBuilderTxSubquery, PromiseLoaderTxSubquery, + }, + }, + }, + verify_compute::circuit::{ + ComponentCircuitVerifyCompute, CoreBuilderVerifyCompute, PromiseLoaderVerifyCompute, + }, +}; + +use self::keccak::ShardIntentKeccak; + +use super::{agg::common::AggTreePinning, ProvingKeySerializer, SupportedPinning}; + +/// Keccak component shard requires special treatment. +pub mod keccak; + +pub type ShardIntentHeader = + ComponentShardCircuitIntent, PromiseLoaderHeaderSubquery>; +pub type ShardIntentAccount = + ComponentShardCircuitIntent, PromiseLoaderAccountSubquery>; +pub type ShardIntentStorage = + ComponentShardCircuitIntent, PromiseLoaderStorageSubquery>; +pub type ShardIntentTx = + ComponentShardCircuitIntent, PromiseLoaderTxSubquery>; +pub type ShardIntentReceipt = + ComponentShardCircuitIntent, PromiseLoaderReceiptSubquery>; +pub type ShardIntentSolidityMapping = ComponentShardCircuitIntent< + CoreBuilderSolidityNestedMappingSubquery, + PromiseLoaderSolidityNestedMappingSubquery, +>; +pub type ShardIntentResultsRoot = + ComponentShardCircuitIntent, PromiseLoaderResultsRoot>; +// You should never shard verify compute, but the struct is the same. +pub type CircuitIntentVerifyCompute = + ComponentShardCircuitIntent; + +#[derive(Clone, Serialize, Deserialize)] +#[enum_dispatch(AggTreePinning)] +pub enum SupportedShardPinning { + ShardHeader(ComponentShardPinning>), + ShardAccount(ComponentShardPinning>), + ShardStorage(ComponentShardPinning>), + ShardTx(ComponentShardPinning>), + ShardReceipt(ComponentShardPinning>), + ShardSolidityMapping(ComponentShardPinning>), + ShardResultsRoot(ComponentShardPinning>), + ShardKeccak(ComponentShardPinning>), + ShardVerifyCompute(ComponentShardPinning), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ComponentShardCircuitIntent, P: PromiseBuilder> +where + C::Params: CoreBuilderParams, +{ + pub core_params: C::Params, + pub loader_params: P::Params, + pub k: u32, + #[serde(default = "default_lookup_bits")] + pub lookup_bits: usize, +} + +impl Clone for ComponentShardCircuitIntent +where + C: CoreBuilder, + P: PromiseBuilder, + C::Params: CoreBuilderParams + Clone, + P::Params: Clone, +{ + fn clone(&self) -> Self { + Self { + core_params: self.core_params.clone(), + loader_params: self.loader_params.clone(), + k: self.k, + lookup_bits: self.lookup_bits, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ComponentShardPinning> { + pub params: C::Params, + /// Number of instances in each instance column + pub num_instance: Vec, + /// g1 generator, g2 generator, s_g2 (s is generator of trusted setup). + /// Together with domain size `2^k`, this commits to the trusted setup used. + /// This is all that's needed to verify the final ecpairing check on the KZG proof. + pub dk: KzgDecidingKey, + pub break_points: RlcThreadBreakPoints, +} + +impl> Clone for ComponentShardPinning +where + C::Params: Clone, +{ + fn clone(&self) -> Self { + Self { + params: self.params.clone(), + num_instance: self.num_instance.clone(), + break_points: self.break_points.clone(), + dk: self.dk.clone(), + } + } +} + +impl KeygenCircuitIntent for ComponentShardCircuitIntent +where + C: CoreBuilder + CircuitMetadata + 'static, + C::Params: CoreBuilderParams, + C::CoreInput: DummyFrom, + P: DummyPromiseBuilder, +{ + type ConcreteCircuit = ComponentCircuitImpl; + type Pinning = ComponentShardPinning; + + fn get_k(&self) -> u32 { + self.k + } + + /// ## Panics + /// In any situation where creating the keygen circuit fails. + fn build_keygen_circuit(self) -> Self::ConcreteCircuit { + let Self { core_params, loader_params, k, mut lookup_bits } = self; + if TypeId::of::() == TypeId::of::() { + // VerifyCompute is aggregation circuit, so optimal lookup bits is `k - 1` + lookup_bits = k as usize - 1; + log::debug!("Verify Compute lookup bits: {lookup_bits}"); + } + let rlc_params = get_dummy_rlc_circuit_params(k as usize, lookup_bits); + let mut circuit = ComponentCircuitImpl::::new_from_stage( + CircuitBuilderStage::Keygen, + core_params.clone(), + loader_params, + rlc_params, + ); + let default_input = C::CoreInput::dummy_from(core_params); + circuit.feed_input(Box::new(default_input)).unwrap(); + circuit.promise_builder.borrow_mut().fulfill_dummy_promise_results(); + circuit.calculate_params(); + + circuit + } + + fn get_pinning_after_keygen( + self, + kzg_params: &ParamsKZG, + circuit: &Self::ConcreteCircuit, + ) -> Self::Pinning { + let circuit_params = circuit.params(); + let break_points = circuit.rlc_builder.borrow().break_points(); + // get public instances + circuit.clear_witnesses(); + circuit.virtual_assign_phase0().unwrap(); + let num_instance = + circuit.rlc_builder.borrow().base.assigned_instances.iter().map(|x| x.len()).collect(); + circuit.clear_witnesses(); // prevent drop warning + let svk = kzg_params.get_g()[0]; + let dk = (svk, kzg_params.g2(), kzg_params.s_g2()); + ComponentShardPinning { params: circuit_params, num_instance, break_points, dk: dk.into() } + } +} + +fn default_lookup_bits() -> usize { + 8 +} + +impl ProvingKeySerializer for ComponentShardCircuitIntent +where + C: CoreBuilder + CircuitMetadata + 'static, + C::Params: CoreBuilderParams + Clone, + C::CoreInput: DummyFrom, + P: DummyPromiseBuilder, + P::Params: Clone, + ComponentShardPinning>: + Serialize + DeserializeOwned + Into, +{ + fn create_and_serialize_proving_key( + self, + params_dir: &Path, + data_dir: &Path, + ) -> anyhow::Result<(AggTreeId, ProvingKey, SupportedPinning)> { + let k = self.get_k(); + let kzg_params = read_srs_from_dir(params_dir, k)?; + let (pk, pinning_json) = self.create_pk_and_pinning(&kzg_params); + let pinning: >::Pinning = + serde_json::from_value(pinning_json)?; + let pinning: SupportedShardPinning = pinning.into(); + let pinning = SupportedPinning::Shard(pinning); + let circuit_id = write_pk_and_pinning(data_dir, &pk, &serde_json::to_value(&pinning)?)?; + // ** !! Warning !! ** + // Currently all shard component circuits are **leaves** in the aggregation tree. + // This implementation would need to change if that changes. + // + // VerifyCompute is an aggregation circuit but does not have children as an aggregation tree because the snark to be aggregated is part of the input. + // Thus all shard circuits are leaves in the aggregation tree. + let leaf_id = AggTreeId { circuit_id, children: vec![], aggregate_vk_hash: None }; + Ok((leaf_id, pk, pinning)) + } +} +// special case (for now) +// if we need to do this more than twice, we should make a macro +impl ProvingKeySerializer for ShardIntentKeccak { + fn create_and_serialize_proving_key( + self, + params_dir: &Path, + data_dir: &Path, + ) -> anyhow::Result<(AggTreeId, ProvingKey, SupportedPinning)> { + let k = self.get_k(); + let kzg_params = read_srs_from_dir(params_dir, k)?; + let (pk, pinning_json) = self.create_pk_and_pinning(&kzg_params); + let pinning: >::Pinning = + serde_json::from_value(pinning_json)?; + let pinning: SupportedShardPinning = pinning.into(); + let pinning = SupportedPinning::Shard(pinning); + let circuit_id = write_pk_and_pinning(data_dir, &pk, &serde_json::to_value(&pinning)?)?; + let leaf_id = AggTreeId { circuit_id, children: vec![], aggregate_vk_hash: None }; + Ok((leaf_id, pk, pinning)) + } +} + +impl> AggTreePinning for ComponentShardPinning { + fn num_instance(&self) -> Vec { + self.num_instance.clone() + } + fn accumulator_indices(&self) -> Option> { + C::accumulator_indices() + } + // ** !! Assertion !! ** + // No ComponentShardPinning has non-None agg_vk_hash_data. + // While VerifyCompute is a universal aggregation circuit, the compute snark's + // vkey is committed to separately in the querySchema. + fn agg_vk_hash_data(&self) -> Option<((usize, usize), Fr)> { + None + } +} diff --git a/axiom-query/src/lib.rs b/axiom-query/src/lib.rs new file mode 100644 index 00000000..542e1628 --- /dev/null +++ b/axiom-query/src/lib.rs @@ -0,0 +1,23 @@ +#![feature(io_error_other)] +#![feature(associated_type_defaults)] + +pub use axiom_eth; + +use axiom_eth::halo2_proofs::halo2curves::ff; +pub use axiom_eth::{Field, RawField}; + +pub mod axiom_aggregation1; +pub mod axiom_aggregation2; +/// Components Complex +pub mod components; +/// Global configuration constants +pub mod global_constants; +pub mod subquery_aggregation; +pub mod utils; +pub mod verify_compute; + +#[cfg(feature = "keygen")] +pub mod keygen; + +/// This means we can concatenate arrays with individual max length 2^32. +pub const DEFAULT_RLC_CACHE_BITS: usize = 32; diff --git a/axiom-query/src/subquery_aggregation/circuit.rs b/axiom-query/src/subquery_aggregation/circuit.rs new file mode 100644 index 00000000..4c4e6053 --- /dev/null +++ b/axiom-query/src/subquery_aggregation/circuit.rs @@ -0,0 +1,248 @@ +use std::collections::HashMap; + +use anyhow::{bail, Result}; +use axiom_eth::{ + halo2_base::gates::{circuit::CircuitBuilderStage, GateChip}, + halo2_proofs::poly::kzg::commitment::ParamsKZG, + halo2curves::bn256::Bn256, + snark_verifier_sdk::{halo2::aggregation::AggregationCircuit, SHPLONK}, + utils::{ + build_utils::pinning::aggregation::AggregationCircuitPinning, + component::{ + promise_loader::multi::ComponentTypeList, types::ComponentPublicInstances, + utils::create_hasher, ComponentType, + }, + snark_verifier::{ + create_universal_aggregation_circuit, AggregationCircuitParams, NUM_FE_ACCUMULATOR, + }, + }, +}; +use itertools::{zip_eq, Itertools}; + +use crate::components::{ + results::{circuit::SubqueryDependencies, types::LogicalPublicInstanceResultsRoot}, + subqueries::{ + account::types::ComponentTypeAccountSubquery, + block_header::types::{ComponentTypeHeaderSubquery, LogicalPublicInstanceHeader}, + receipt::types::ComponentTypeReceiptSubquery, + solidity_mappings::types::ComponentTypeSolidityNestedMappingSubquery, + storage::types::ComponentTypeStorageSubquery, + transaction::types::ComponentTypeTxSubquery, + }, +}; + +use super::types::{InputSubqueryAggregation, LogicalPublicInstanceSubqueryAgg, F}; + +impl InputSubqueryAggregation { + /// Builds general circuit + /// + /// Warning: this MUST return a circuit implementing `CircuitExt` with accumulator indices provided. + /// In particular, do not return `BaseCircuitBuilder`. + pub fn build( + self, + stage: CircuitBuilderStage, + circuit_params: AggregationCircuitParams, + kzg_params: &ParamsKZG, + ) -> Result { + // dependency checks + if self.snark_storage.is_some() && self.snark_account.is_none() { + bail!("Storage snark requires Account snark"); + } + if self.snark_solidity_mapping.is_some() && self.snark_storage.is_none() { + bail!("SolidityMapping snark requires Storage snark"); + } + const NUM_SNARKS: usize = 7; + let snarks = vec![ + Some(self.snark_header), + self.snark_account, + self.snark_storage, + self.snark_tx, + self.snark_receipt, + self.snark_solidity_mapping, + Some(self.snark_results_root), + ]; + let snarks_enabled = snarks.iter().map(|s| s.is_some()).collect_vec(); + let subquery_type_ids = [ + ComponentTypeHeaderSubquery::::get_type_id(), + ComponentTypeAccountSubquery::::get_type_id(), + ComponentTypeStorageSubquery::::get_type_id(), + ComponentTypeTxSubquery::::get_type_id(), + ComponentTypeReceiptSubquery::::get_type_id(), + ComponentTypeSolidityNestedMappingSubquery::::get_type_id(), + ]; + if snarks.iter().flatten().any(|s| s.agg_vk_hash_idx.is_some()) { + bail!("[SubqueryAggregation] No snark should be universal."); + } + let snarks = snarks.into_iter().flatten().map(|s| s.inner).collect_vec(); + let agg_vkey_hash_indices = vec![None; snarks.len()]; + let (mut circuit, previous_instances, agg_vkey_hash) = + create_universal_aggregation_circuit::( + stage, + circuit_params, + kzg_params, + snarks, + agg_vkey_hash_indices, + ); + + let builder = &mut circuit.builder; + let ctx = builder.main(0); + + // Parse aggregated component public instances + let mut previous_instances = previous_instances.into_iter(); + let mut get_next_pis = + || ComponentPublicInstances::try_from(previous_instances.next().unwrap()); + let mut pis = Vec::with_capacity(NUM_SNARKS); + for snark_enabled in snarks_enabled { + if snark_enabled { + pis.push(Some(get_next_pis()?)); + } else { + pis.push(None); + } + } + let pis_header = pis[0].clone().unwrap(); + let pis_results = pis.pop().unwrap().unwrap(); + + // Load promise commit keccak as a public input + let promise_keccak = ctx.load_witness(self.promise_commit_keccak); + // ======== Create Poseidon hasher =========== + let gate = GateChip::default(); + let mut hasher = create_hasher(); + hasher.initialize_consts(ctx, &gate); + // Insert subquery output commits + // Unclear if this is a necessary precaution, but we store based on `subquery_type_ids` so the order does not depend on ordering in other modules + let mut subquery_commits = HashMap::new(); + // Insert subquery promise commits + let mut subquery_promises = HashMap::new(); + for (type_id, pi) in zip_eq(subquery_type_ids, &pis) { + if let Some(pi) = pi { + subquery_commits.insert(type_id.clone(), pi.output_commit); + subquery_promises.insert(type_id, pi.promise_result_commit); + } + } + // Hash each subquery output commit with the `promise_commit_keccak`, to be compared with subquery promises later. + // This matches the promise public output computation in `ComponentCircuitImpl::generate_public_instances`. + // The dependencies of a non-Header subquery circuit are always [Keccak, ] + // We only need to calculate the hash for components that are called: Header, Account, Storage. Currently Tx, Receipt, SolidityNestedMapping are not called. + let mut hashed_commits = HashMap::new(); + for type_id in [ + ComponentTypeHeaderSubquery::::get_type_id(), + ComponentTypeAccountSubquery::::get_type_id(), + ComponentTypeStorageSubquery::::get_type_id(), + ] { + if let Some(output_commit) = subquery_commits.get(&type_id) { + hashed_commits.insert( + type_id, + hasher.hash_fix_len_array(ctx, &gate, &[promise_keccak, *output_commit]), + ); + } + } + + // ======== Manually check all promise calls between subqueries: ======= + // Header calls Keccak + { + let hashed_commit_keccak = hasher.hash_fix_len_array(ctx, &gate, &[promise_keccak]); + let header_promise_commit = + subquery_promises[&ComponentTypeHeaderSubquery::::get_type_id()]; + log::debug!("hash(promise_keccak): {:?}", hashed_commit_keccak.value()); + log::debug!("header_promise_commit: {:?}", header_promise_commit.value()); + ctx.constrain_equal(&hashed_commit_keccak, &header_promise_commit); + } + // Below when we say promise_header and commit_header, we actually mean promise_keccak_header and commit_keccak_header because both have been hashed with a promise_keccak. + // Account calls Keccak & Header + if let Some(promise_header) = + subquery_promises.get(&ComponentTypeAccountSubquery::::get_type_id()) + { + let commit_header = hashed_commits[&ComponentTypeHeaderSubquery::::get_type_id()]; + log::debug!("account:commit_header: {:?}", commit_header.value()); + log::debug!("account:promise_header: {:?}", promise_header.value()); + ctx.constrain_equal(&commit_header, promise_header); + } + // Storage calls Keccak & Account + if let Some(promise_account) = + subquery_promises.get(&ComponentTypeStorageSubquery::::get_type_id()) + { + let commit_account = hashed_commits[&ComponentTypeAccountSubquery::::get_type_id()]; + log::debug!("storage:commit_account: {:?}", commit_account.value()); + log::debug!("storage:promise_account: {:?}", promise_account.value()); + ctx.constrain_equal(&commit_account, promise_account); + } + // Tx calls Keccak & Header + if let Some(promise_header) = + subquery_promises.get(&ComponentTypeTxSubquery::::get_type_id()) + { + let commit_header = hashed_commits[&ComponentTypeHeaderSubquery::::get_type_id()]; + log::debug!("tx:commit_header: {:?}", commit_header.value()); + log::debug!("tx:promise_header: {:?}", promise_header.value()); + ctx.constrain_equal(&commit_header, promise_header); + } + // Receipt calls Keccak & Header + if let Some(promise_header) = + subquery_promises.get(&ComponentTypeReceiptSubquery::::get_type_id()) + { + let commit_header = hashed_commits[&ComponentTypeHeaderSubquery::::get_type_id()]; + log::debug!("receipt:commit_header: {:?}", commit_header.value()); + log::debug!("receipt:promise_header: {:?}", promise_header.value()); + ctx.constrain_equal(&commit_header, promise_header); + } + // SolidityNestedMapping calls Keccak & Storage + if let Some(promise_storage) = + subquery_promises.get(&ComponentTypeSolidityNestedMappingSubquery::::get_type_id()) + { + let commit_storage = hashed_commits[&ComponentTypeStorageSubquery::::get_type_id()]; + log::debug!("solidity_nested_mapping:commit_storage: {:?}", commit_storage.value()); + log::debug!("solidity_nested_mapping:promise_storage: {:?}", promise_storage.value()); + ctx.constrain_equal(&commit_storage, promise_storage); + } + + // Get keccakPacked(blockhashMmr) + let LogicalPublicInstanceHeader { mmr_keccak } = pis_header.other.try_into()?; + + // ======== results root ========= + // MUST match order in `InputResultsRootShard::build` + let type_ids = SubqueryDependencies::::get_component_type_ids(); + // We now collect the promises from snarks in the order they were commited to in ResultsRoot + let mut results_deps_commits = Vec::new(); + results_deps_commits.push(promise_keccak); + for t_id in &type_ids { + if let Some(commit) = subquery_commits.get(t_id) { + results_deps_commits.push(*commit); + } + } + + let results_promise_commit = hasher.hash_fix_len_array(ctx, &gate, &results_deps_commits); + + log::debug!("results_promise_commit: {:?}", results_promise_commit.value()); + log::debug!("promise_result_commit: {:?}", pis_results.promise_result_commit.value()); + ctx.constrain_equal(&results_promise_commit, &pis_results.promise_result_commit); + + // We have implicitly checked all Components use the same `promise_keccak` above. + + let LogicalPublicInstanceResultsRoot { results_root_poseidon, commit_subquery_hashes } = + pis_results.other.try_into().unwrap(); + + let logical_pis = LogicalPublicInstanceSubqueryAgg { + promise_keccak, + agg_vkey_hash, + results_root_poseidon, + commit_subquery_hashes, + mmr_keccak, + }; + if builder.assigned_instances.len() != 1 { + bail!("should only have 1 instance column"); + } + assert_eq!(builder.assigned_instances[0].len(), NUM_FE_ACCUMULATOR); + builder.assigned_instances[0].extend(logical_pis.flatten()); + + Ok(circuit) + } + + pub fn prover_circuit( + self, + pinning: AggregationCircuitPinning, + kzg_params: &ParamsKZG, + ) -> Result { + Ok(self + .build(CircuitBuilderStage::Prover, pinning.params, kzg_params)? + .use_break_points(pinning.break_points)) + } +} diff --git a/axiom-query/src/subquery_aggregation/mod.rs b/axiom-query/src/subquery_aggregation/mod.rs new file mode 100644 index 00000000..53a0e142 --- /dev/null +++ b/axiom-query/src/subquery_aggregation/mod.rs @@ -0,0 +1,13 @@ +//! # Subquery Aggregation Circuit +//! +//! Aggregation all subquery circuits and resultsRoot circuit. +//! Currently these are all of the components _except_ the keccak component. +//! +//! The reasoning is that the keccak component should be aggregated separately +//! and run in parallel to the Subquery Aggregation Circuit and all other components. + +pub mod circuit; +pub mod types; + +#[cfg(test)] +pub mod tests; diff --git a/axiom-query/src/subquery_aggregation/tests.rs b/axiom-query/src/subquery_aggregation/tests.rs new file mode 100644 index 00000000..f78ec095 --- /dev/null +++ b/axiom-query/src/subquery_aggregation/tests.rs @@ -0,0 +1,351 @@ +use std::{ + collections::HashMap, + env, + fs::File, + io::{Read, Write}, +}; + +use axiom_codec::{ + constants::NUM_SUBQUERY_TYPES, + types::native::{HeaderSubquery, SubqueryType}, +}; +use axiom_eth::{ + halo2_base::{ + gates::circuit::CircuitBuilderStage, + halo2_proofs::{ + dev::MockProver, + halo2curves::bn256::{Bn256, Fr}, + poly::kzg::commitment::ParamsKZG, + }, + utils::fs::{gen_srs, read_params}, + }, + keccak::types::{ComponentTypeKeccak, OutputKeccakShard}, + snark_verifier_sdk::{halo2::gen_snark_shplonk, CircuitExt}, + utils::{ + build_utils::pinning::{Halo2CircuitPinning, PinnableCircuit, RlcCircuitPinning}, + component::{ + circuit::ComponentBuilder, + promise_loader::{ + comp_loader::SingleComponentLoaderParams, multi::MultiPromiseLoaderParams, + single::PromiseLoaderParams, + }, + ComponentCircuit, ComponentPromiseResultsInMerkle, ComponentType, + GroupedPromiseResults, + }, + snark_verifier::{AggregationCircuitParams, EnhancedSnark, NUM_FE_ACCUMULATOR}, + }, +}; +use ethers_core::types::H256; +use test_log::test; + +use crate::components::{ + results::{ + circuit::{ComponentCircuitResultsRoot, CoreParamsResultRoot}, + types::CircuitInputResultsRootShard, + }, + subqueries::{ + block_header::{ + circuit::{ + ComponentCircuitHeaderSubquery, CoreParamsHeaderSubquery, + PromiseLoaderHeaderSubquery, + }, + types::{CircuitInputHeaderShard, ComponentTypeHeaderSubquery}, + }, + common::{shard_into_component_promise_results, OutputSubqueryShard}, + }, +}; + +use super::types::{InputSubqueryAggregation, SUBQUERY_AGGREGATION_AGG_VKEY_HASH_IDX}; + +fn generate_snark + PinnableCircuit>( + name: &'static str, + params: &ParamsKZG, + keygen_circuit: C, + load_prover_circuit: &impl Fn(RlcCircuitPinning) -> C, +) -> anyhow::Result { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + let pinning_path = format!("{cargo_manifest_dir}/configs/test/{name}.json"); + let pk_path = format!("{cargo_manifest_dir}/data/test/{name}.pk"); + let (pk, pinning) = keygen_circuit.create_pk(params, pk_path, pinning_path)?; + let vk = pk.get_vk(); + let mut vk_file = File::create(format!("data/test/{name}.vk"))?; + vk.write(&mut vk_file, axiom_eth::halo2_proofs::SerdeFormat::RawBytes)?; + let mut vk_file = File::create(format!("data/test/{name}.vk.txt"))?; + write!(vk_file, "{:?}", vk.pinned())?; + + let component_circuit = load_prover_circuit(pinning); + + let snark_path = format!("data/test/{name}.snark"); + let snark = gen_snark_shplonk(params, &pk, component_circuit, Some(snark_path)); + Ok(EnhancedSnark { inner: snark, agg_vk_hash_idx: None }) +} + +fn read_header_pinning( +) -> anyhow::Result<(CoreParamsHeaderSubquery, PromiseLoaderParams, RlcCircuitPinning)> { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + let header_core_params: CoreParamsHeaderSubquery = serde_json::from_reader(File::open( + format!("{cargo_manifest_dir}/configs/test/header_subquery_core_params.json"), + )?)?; + let header_promise_params: as ComponentBuilder>::Params = + serde_json::from_reader(File::open(format!( + "{cargo_manifest_dir}/configs/test/header_subquery_loader_params.json" + ))?)?; + let header_rlc_params: RlcCircuitPinning = serde_json::from_reader(File::open(format!( + "{cargo_manifest_dir}/configs/test/header_subquery.json" + ))?)?; + Ok((header_core_params, header_promise_params, header_rlc_params)) +} + +fn generate_header_snark( + params: &ParamsKZG, +) -> anyhow::Result<(EnhancedSnark, GroupedPromiseResults)> { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + + let mut promise_results = HashMap::new(); + let promise_keccak: OutputKeccakShard = serde_json::from_reader( + File::open(format!("{cargo_manifest_dir}/data/test/promise_results_keccak_for_agg.json")) + .unwrap(), + )?; + let promise_header: OutputSubqueryShard = serde_json::from_reader( + File::open(format!("{cargo_manifest_dir}/data/test/promise_results_header_for_agg.json")) + .unwrap(), + )?; + let keccak_merkle = ComponentPromiseResultsInMerkle::::from_single_shard( + promise_keccak.into_logical_results(), + ); + promise_results.insert(ComponentTypeKeccak::::get_type_id(), keccak_merkle); + promise_results.insert( + ComponentTypeHeaderSubquery::::get_type_id(), + shard_into_component_promise_results::>( + promise_header.convert_into(), + ), + ); + + let header_input: CircuitInputHeaderShard = serde_json::from_reader(File::open(format!( + "{cargo_manifest_dir}/data/test/input_header_for_agg.json" + ))?)?; + let (header_core_params, header_promise_params, header_rlc_params) = read_header_pinning()?; + let header_circuit = ComponentCircuitHeaderSubquery::::new( + header_core_params.clone(), + header_promise_params.clone(), + header_rlc_params.params, + ); + header_circuit.feed_input(Box::new(header_input.clone())).unwrap(); + header_circuit.fulfill_promise_results(&promise_results).unwrap(); + + let header_snark = + generate_snark("header_subquery_for_agg", params, header_circuit, &|pinning| { + let circuit = ComponentCircuitHeaderSubquery::::prover( + header_core_params.clone(), + header_promise_params.clone(), + pinning, + ); + circuit.feed_input(Box::new(header_input.clone())).unwrap(); + circuit.fulfill_promise_results(&promise_results).unwrap(); + circuit + })?; + Ok((header_snark, promise_results)) +} + +fn get_test_input(params: &ParamsKZG) -> anyhow::Result { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + + let (header_snark, promise_results) = generate_header_snark(params)?; + let keccak_commit = + promise_results.get(&ComponentTypeKeccak::::get_type_id()).unwrap().leaves()[0].commit; + + let results_input: CircuitInputResultsRootShard = serde_json::from_reader(File::open( + format!("{cargo_manifest_dir}/data/test/input_results_root_for_agg.json"), + )?)?; + + let result_rlc_pinning: RlcCircuitPinning = serde_json::from_reader(File::open(format!( + "{cargo_manifest_dir}/configs/test/results_root_for_agg.json" + ))?)?; + + let mut enabled_types = [false; NUM_SUBQUERY_TYPES]; + enabled_types[SubqueryType::Header as usize] = true; + let mut params_per_comp = HashMap::new(); + params_per_comp.insert( + ComponentTypeHeaderSubquery::::get_type_id(), + SingleComponentLoaderParams::new(0, vec![3]), + ); + let promise_results_params = MultiPromiseLoaderParams { params_per_component: params_per_comp }; + + let mut results_circuit = ComponentCircuitResultsRoot::::new( + CoreParamsResultRoot { enabled_types, capacity: results_input.subqueries.len() }, + (PromiseLoaderParams::new_for_one_shard(200), promise_results_params.clone()), + result_rlc_pinning.params, + ); + results_circuit.feed_input(Box::new(results_input.clone()))?; + results_circuit.fulfill_promise_results(&promise_results).unwrap(); + results_circuit.calculate_params(); + + let results_snark = + generate_snark("results_root_for_agg", params, results_circuit, &|pinning| { + let results_circuit = ComponentCircuitResultsRoot::::prover( + CoreParamsResultRoot { enabled_types, capacity: results_input.subqueries.len() }, + (PromiseLoaderParams::new_for_one_shard(200), promise_results_params.clone()), + pinning, + ); + results_circuit.feed_input(Box::new(results_input.clone())).unwrap(); + results_circuit.fulfill_promise_results(&promise_results).unwrap(); + results_circuit + })?; + + Ok(InputSubqueryAggregation { + snark_header: header_snark, + snark_results_root: results_snark, + snark_account: None, + snark_storage: None, + snark_solidity_mapping: None, + snark_tx: None, + snark_receipt: None, + promise_commit_keccak: keccak_commit, + }) +} + +#[test] +fn test_mock_subquery_agg() -> anyhow::Result<()> { + let k = 19; + let params = gen_srs(k as u32); + + let input = get_test_input(¶ms)?; + let mut agg_circuit = input.build( + CircuitBuilderStage::Mock, + AggregationCircuitParams { + degree: k as u32, + num_advice: 0, + num_lookup_advice: 0, + num_fixed: 0, + lookup_bits: 8, + }, + //rlc_circuit_params.base.try_into().unwrap(), + ¶ms, + )?; + agg_circuit.calculate_params(Some(9)); + let instances = agg_circuit.instances(); + MockProver::run(k as u32, &agg_circuit, instances).unwrap().assert_satisfied(); + Ok(()) +} + +#[test] +#[ignore = "prover"] +fn test_generate_header_snark() -> anyhow::Result<()> { + let k = 18; + let params = read_params(k); + generate_header_snark(¶ms)?; + Ok(()) +} + +#[cfg(feature = "keygen")] +#[test] +#[ignore = "use axiom srs"] +fn test_generate_header_pk() -> anyhow::Result<()> { + use crate::keygen::shard::ShardIntentHeader; + use axiom_eth::halo2_base::utils::halo2::ProvingKeyGenerator; + let k = 18; + let params = read_params(k); + // Generate the snark and pk using a real input + generate_header_snark(¶ms)?; + + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + let name = "header_subquery_for_agg"; + let pk_path = format!("{cargo_manifest_dir}/data/test/{name}.pk"); + let mut buf1 = Vec::new(); + let mut f = File::open(pk_path)?; + f.read_to_end(&mut buf1)?; + + let (core_params, loader_params, rlc_pinning) = read_header_pinning()?; + let intent = ShardIntentHeader { + core_params, + loader_params, + k: rlc_pinning.k() as u32, + lookup_bits: rlc_pinning.params.base.lookup_bits.unwrap_or(0), + }; + let (pk, _) = intent.create_pk_and_pinning(¶ms); + let mut buf2 = Vec::new(); + pk.write(&mut buf2, axiom_eth::halo2_proofs::SerdeFormat::RawBytesUnchecked)?; + + if buf1 != buf2 { + panic!("proving key mismatch"); + } + Ok(()) +} + +#[test] +#[ignore = "prover"] +fn test_prover_subquery_agg() -> anyhow::Result<()> { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + + let k = 20; + let params = gen_srs(k as u32); + + let input = get_test_input(¶ms)?; + let mut keygen_circuit = input.clone().build( + CircuitBuilderStage::Keygen, + AggregationCircuitParams { degree: k as u32, lookup_bits: k - 1, ..Default::default() }, + ¶ms, + )?; + keygen_circuit.calculate_params(Some(20)); + let instance1 = keygen_circuit.instances(); + let abs_agg_vk_hash_idx = SUBQUERY_AGGREGATION_AGG_VKEY_HASH_IDX + NUM_FE_ACCUMULATOR; + let name = "subquery_aggregation_for_agg"; + let pinning_path = format!("{cargo_manifest_dir}/configs/test/{name}.json"); + let pk_path = format!("{cargo_manifest_dir}/data/test/{name}.pk"); + let (pk, pinning) = keygen_circuit.create_pk(¶ms, pk_path, pinning_path)?; + + #[cfg(feature = "keygen")] + { + // test keygen + use axiom_eth::halo2_proofs::{plonk::keygen_vk, SerdeFormat}; + use axiom_eth::snark_verifier_sdk::{halo2::gen_dummy_snark_from_protocol, SHPLONK}; + use axiom_eth::utils::build_utils::aggregation::get_dummy_aggregation_params; + let [dum_snark_header, dum_snark_results] = + [&input.snark_header, &input.snark_results_root].map(|s| { + EnhancedSnark::new( + gen_dummy_snark_from_protocol::(s.inner.protocol.clone()), + None, + ) + }); + let input = InputSubqueryAggregation { + snark_header: dum_snark_header, + snark_results_root: dum_snark_results, + snark_account: None, + snark_storage: None, + snark_solidity_mapping: None, + snark_tx: None, + snark_receipt: None, + promise_commit_keccak: Default::default(), + }; + let mut circuit = + input.build(CircuitBuilderStage::Keygen, get_dummy_aggregation_params(k), ¶ms)?; + circuit.calculate_params(Some(20)); + let vk = keygen_vk(¶ms, &circuit)?; + if pk.get_vk().to_bytes(SerdeFormat::RawBytes) != vk.to_bytes(SerdeFormat::RawBytes) { + panic!("vk mismatch"); + } + let instance2 = circuit.instances(); + assert_eq!( + instance1[0][abs_agg_vk_hash_idx], instance2[0][abs_agg_vk_hash_idx], + "agg vkey hash mismatch" + ); + } + + let mut prover_circuit = input.build(CircuitBuilderStage::Prover, pinning.params, ¶ms)?; + prover_circuit.set_break_points(pinning.break_points); + + let snark = gen_snark_shplonk(¶ms, &pk, prover_circuit, None::<&str>); + let instance3 = snark.instances.clone(); + let snark = EnhancedSnark { + inner: snark, + agg_vk_hash_idx: Some(SUBQUERY_AGGREGATION_AGG_VKEY_HASH_IDX), + }; + assert_eq!( + instance1[0][abs_agg_vk_hash_idx], instance3[0][abs_agg_vk_hash_idx], + "agg vkey hash mismatch" + ); + + let snark_path = format!("{cargo_manifest_dir}/data/test/{name}.snark.json"); + serde_json::to_writer(File::create(snark_path)?, &snark)?; + Ok(()) +} diff --git a/axiom-query/src/subquery_aggregation/types.rs b/axiom-query/src/subquery_aggregation/types.rs new file mode 100644 index 00000000..632139bf --- /dev/null +++ b/axiom-query/src/subquery_aggregation/types.rs @@ -0,0 +1,116 @@ +use std::iter; +use std::marker::PhantomData; + +use anyhow::anyhow; +use axiom_codec::HiLo; +use axiom_eth::{ + halo2_base::AssignedValue, + impl_flatten_conversion, + utils::{ + component::{types::LogicalEmpty, ComponentType, ComponentTypeId, LogicalResult}, + snark_verifier::EnhancedSnark, + }, +}; +use serde::{Deserialize, Serialize}; + +pub(crate) type F = axiom_eth::halo2curves::bn256::Fr; + +#[derive(Clone, Debug, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InputSubqueryAggregation { + /// Header snark always required + pub snark_header: EnhancedSnark, + + #[serde(skip_serializing_if = "Option::is_none")] + pub snark_account: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub snark_storage: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub snark_solidity_mapping: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub snark_tx: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub snark_receipt: Option, + + /// The keccak commit is provided as a public input. + /// The SubqueryAggregation circuit will check that all subquery component circuits use the same promise commit for keccak. + /// It will not check the promise commit itself. That will be done by AxiomAggregation1, which aggregates this SubqueryAggregation circuit. + pub promise_commit_keccak: F, + + /// Results root snark always required + pub snark_results_root: EnhancedSnark, +} + +pub struct ComponentTypeSubqueryAgg { + _phatnom: PhantomData, +} +impl ComponentType for ComponentTypeSubqueryAgg { + type InputValue = LogicalEmpty; + type InputWitness = LogicalEmpty>; + type OutputValue = LogicalEmpty; + type OutputWitness = LogicalEmpty>; + type LogicalInput = LogicalEmpty; + + fn get_type_id() -> ComponentTypeId { + "axiom-query:ComponentTypeSubqueryAgg".to_string() + } + + fn logical_result_to_virtual_rows_impl( + _ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + unreachable!() + } + fn logical_input_to_virtual_rows_impl(_li: &Self::LogicalInput) -> Vec { + unreachable!() + } +} + +/// The public instances **without** the accumulator +const FIELD_SIZE_PUBLIC_INSTANCES: [usize; 6] = [ + 9999, // promise_keccak + 9999, // agg_vk_hash + 9999, // results_root_poseidon + 9999, // commit_subquery_hashes + 128, 128, // mmr_keccak +]; +pub const SUBQUERY_AGGREGATION_AGG_VKEY_HASH_IDX: usize = 1; + +/// Public instances **without** the accumulator (accumulator is 12 field elements) +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct LogicalPublicInstanceSubqueryAgg { + pub promise_keccak: T, + pub agg_vkey_hash: T, + pub results_root_poseidon: T, + pub commit_subquery_hashes: T, + pub mmr_keccak: HiLo, +} + +impl TryFrom> for LogicalPublicInstanceSubqueryAgg { + type Error = anyhow::Error; + fn try_from(value: Vec) -> Result { + let [promise_keccak, agg_vkey_hash, results_root_poseidon, commit_subquery_hashes, mmr_hi, mmr_lo] = + value + .try_into() + .map_err(|_| anyhow!("LogicalPublicInstanceSubqueryAgg invalid length"))?; + Ok(Self { + promise_keccak, + agg_vkey_hash, + results_root_poseidon, + commit_subquery_hashes, + mmr_keccak: HiLo::from_hi_lo([mmr_hi, mmr_lo]), + }) + } +} + +impl LogicalPublicInstanceSubqueryAgg { + pub fn flatten(&self) -> Vec { + iter::empty() + .chain([self.promise_keccak, self.agg_vkey_hash]) + .chain([self.results_root_poseidon, self.commit_subquery_hashes]) + .chain(self.mmr_keccak.hi_lo()) + .collect() + } +} + +// This is not used: +impl_flatten_conversion!(LogicalPublicInstanceSubqueryAgg, FIELD_SIZE_PUBLIC_INSTANCES); diff --git a/axiom-query/src/utils/client_circuit/default_circuit.rs b/axiom-query/src/utils/client_circuit/default_circuit.rs new file mode 100644 index 00000000..18f78fea --- /dev/null +++ b/axiom-query/src/utils/client_circuit/default_circuit.rs @@ -0,0 +1,109 @@ +use std::marker::PhantomData; + +use axiom_eth::{ + halo2_base::{ + gates::circuit::{BaseCircuitParams, BaseConfig}, + utils::ScalarField, + }, + halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner}, + halo2curves::bn256::{Bn256, Fr, G1Affine}, + plonk::{keygen_vk_custom, Circuit, ConstraintSystem, Error, VerifyingKey}, + poly::kzg::commitment::ParamsKZG, + }, + rlc::circuit::{RlcCircuitParams, RlcConfig}, +}; + +use super::metadata::AxiomV2CircuitMetadata; + +/// We only care about evaluations (custom gates) but not the domain, so we use a very small dummy +pub(super) const DUMMY_K: u32 = 7; + +/// Dummy circuit just to get the correct constraint system corresponding +/// to the circuit metadata. +#[derive(Clone)] +struct DummyAxiomCircuit { + metadata: AxiomV2CircuitMetadata, + _marker: PhantomData, +} + +/// An enum to choose between a circuit with only basic columns and gates or a circuit that in addition has RLC columns and gates. +/// The distinction is that even when `RlcConfig` has `num_rlc_columns` set to 0, it will always have the challenge `gamma`. +/// Therefore we use this enum to more clearly distinguish between the two cases. +#[derive(Clone, Debug)] +pub enum MaybeRlcConfig { + Rlc(RlcConfig), + Base(BaseConfig), +} + +// For internal use only +impl Circuit for DummyAxiomCircuit { + type Config = MaybeRlcConfig; + type FloorPlanner = SimpleFloorPlanner; + type Params = AxiomV2CircuitMetadata; + + fn without_witnesses(&self) -> Self { + self.clone() + } + + fn params(&self) -> Self::Params { + self.metadata.clone() + } + + fn configure_with_params( + meta: &mut ConstraintSystem, + metadata: Self::Params, + ) -> Self::Config { + let num_phase = metadata.num_challenge.len(); + assert!(num_phase == 1 || num_phase == 2, "only support 2 phases for now"); + let base_circuit_params = BaseCircuitParams { + k: DUMMY_K as usize, + num_advice_per_phase: metadata + .num_advice_per_phase + .iter() + .map(|x| *x as usize) + .collect(), + num_fixed: metadata.num_fixed as usize, + num_lookup_advice_per_phase: metadata + .num_lookup_advice_per_phase + .iter() + .map(|x| *x as usize) + .collect(), + lookup_bits: Some(DUMMY_K as usize - 1), // doesn't matter because we replace fixed commitments later + num_instance_columns: metadata.num_instance.len(), + }; + if num_phase == 1 { + assert!(metadata.num_rlc_columns == 0, "rlc columns only allowed in phase1"); + // Note that BaseConfig ignores lookup bits if there are no lookup advice columns + MaybeRlcConfig::Base(BaseConfig::configure(meta, base_circuit_params)) + } else { + let rlc_circuit_params = RlcCircuitParams { + base: base_circuit_params, + num_rlc_columns: metadata.num_rlc_columns as usize, + }; + MaybeRlcConfig::Rlc(RlcConfig::configure(meta, rlc_circuit_params)) + } + } + + fn configure(_: &mut ConstraintSystem) -> Self::Config { + unreachable!("must use configure_with_params") + } + + fn synthesize( + &self, + _config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + layouter.assign_region(|| "dummy", |_region| Ok(())) + } +} + +/// For internal use only, num_instance will be replaced later +pub(crate) fn dummy_vk_from_metadata( + params: &ParamsKZG, + metadata: AxiomV2CircuitMetadata, +) -> anyhow::Result> { + let dummy_circuit = DummyAxiomCircuit:: { metadata, _marker: PhantomData }; + let vk = keygen_vk_custom(params, &dummy_circuit, false)?; + Ok(vk) +} diff --git a/axiom-query/src/utils/client_circuit/metadata.rs b/axiom-query/src/utils/client_circuit/metadata.rs new file mode 100644 index 00000000..db0e6263 --- /dev/null +++ b/axiom-query/src/utils/client_circuit/metadata.rs @@ -0,0 +1,155 @@ +use anyhow::bail; +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use ethers_core::types::H256; +use serde::{Deserialize, Serialize}; + +/// All configuration parameters of an Axiom Client Circuit that are +/// hard-coded into the Verify Compute Circuit (which is an Aggregation Circuit with Universality::Full). +/// +/// This metadata is only for a circuit built using `RlcCircuitBuilder` +/// or `BaseCircuitBuilder`, where the circuit _may_ be an aggregation circuit. +#[derive(Clone, Debug, Default, Serialize, Deserialize, Hash)] +#[serde(rename_all = "camelCase")] +pub struct AxiomV2CircuitMetadata { + /// Version byte for domain separation on version of Axiom client, halo2-lib, snark-verifier (for example if we switch to mv_lookup) + /// If `version = x`, this should be thought of as Axiom Query v2.x + pub version: u8, + /// Number of instances in each instance polynomial + pub num_instance: Vec, + /// Number of challenges to squeeze from transcript after each phase. + /// This `num_challenge` counts only the challenges used inside the circuit - it excludes challenges that are part of the halo2 system. + /// The full challenges, which is what `plonk_protocol.num_challenge` stores, is: + /// ```ignore + /// [ + /// my_phase0_challenges, + /// ... + /// [..my_phasen_challenges, theta], + /// [beta, gamma], + /// [alpha], + /// ] + /// ``` + pub num_challenge: Vec, + + /// Boolean for whether this is an Aggregation Circuit which has a KZG accumulator in the public instances. If true, it must be the first 12 instances. + pub is_aggregation: bool, + + // RlcCircuitParams: + /// The number of advice columns per phase + pub num_advice_per_phase: Vec, + /// The number of special advice columns that have range lookup enabled per phase + pub num_lookup_advice_per_phase: Vec, + /// Number of advice columns for the RLC custom gate + pub num_rlc_columns: u16, + /// The number of fixed columns **only** for constants + pub num_fixed: u8, + + // This is specific to the current Verify Compute Circuit implementation and provided just for data availability: + /// The maximum number of user outputs. Used to determine where to split off `compute_snark`'s public instances between user outputs and data subqueries. + /// This does **not** include the old accumulator elliptic curve points if + /// the `compute_snark` is from an aggregation circuit. + pub max_outputs: u16, +} + +impl AxiomV2CircuitMetadata { + pub fn encode(&self) -> anyhow::Result { + let mut encoded = vec![]; + encoded.write_u8(self.version)?; + + encoded.write_u8(self.num_instance.len().try_into()?)?; + for &num_instance in &self.num_instance { + encoded.write_u32::(num_instance)?; + } + + let num_phase = self.num_challenge.len(); + if num_phase == 0 { + bail!("num_challenge must be non-empty") + } + encoded.write_u8(num_phase.try_into()?)?; + for &num_challenge in &self.num_challenge { + encoded.write_u8(num_challenge)?; + } + + encoded.write_u8(self.is_aggregation as u8)?; + + // encode RlcCircuitParams: + if self.num_advice_per_phase.len() > num_phase { + bail!("num_advice_per_phase must be <= num_phase") + } + let mut num_advice_cols = self.num_advice_per_phase.clone(); + num_advice_cols.resize(num_phase, 0); + for num_advice_col in num_advice_cols { + encoded.write_u16::(num_advice_col)?; + } + + if self.num_lookup_advice_per_phase.len() > num_phase { + bail!("num_lookup_advice_per_phase must be <= num_phase") + } + let mut num_lookup_advice_cols = self.num_lookup_advice_per_phase.clone(); + num_lookup_advice_cols.resize(num_phase, 0); + for num_lookup_advice_col in num_lookup_advice_cols { + encoded.write_u8(num_lookup_advice_col)?; + } + + encoded.write_u16::(self.num_rlc_columns)?; + encoded.write_u8(self.num_fixed)?; + + encoded.write_u16::(self.max_outputs)?; + + if encoded.len() > 32 { + bail!("circuit metadata cannot be packed into bytes32") + } + encoded.resize(32, 0); + Ok(H256::from_slice(&encoded)) + } +} + +pub fn decode_axiom_v2_circuit_metadata(encoded: H256) -> anyhow::Result { + let mut reader = &encoded[..]; + let version = reader.read_u8()?; + if version != 0 { + bail!("invalid version") + } + let num_instance_len = reader.read_u8()? as usize; + let mut num_instance = Vec::with_capacity(num_instance_len); + for _ in 0..num_instance_len { + num_instance.push(reader.read_u32::()?); + } + let num_phase = reader.read_u8()? as usize; + let mut num_challenge = Vec::with_capacity(num_phase); + for _ in 0..num_phase { + num_challenge.push(reader.read_u8()?); + } + + let is_aggregation = reader.read_u8()?; + if is_aggregation > 1 { + bail!("is_aggregation is not boolean"); + } + let is_aggregation = is_aggregation == 1; + + // decode RlcCircuitParams: + let mut num_advice_per_phase = Vec::with_capacity(num_phase); + for _ in 0..num_phase { + num_advice_per_phase.push(reader.read_u16::()?); + } + let mut num_lookup_advice_per_phase = Vec::with_capacity(num_phase); + for _ in 0..num_phase { + num_lookup_advice_per_phase.push(reader.read_u8()?); + } + + let num_rlc_columns = reader.read_u16::()?; + let num_fixed = reader.read_u8()?; + + let max_outputs = reader.read_u16::()?; + + Ok(AxiomV2CircuitMetadata { + version, + num_instance, + num_challenge, + num_advice_per_phase, + num_lookup_advice_per_phase, + num_rlc_columns, + num_fixed, + is_aggregation, + max_outputs, + }) +} diff --git a/axiom-query/src/utils/client_circuit/mod.rs b/axiom-query/src/utils/client_circuit/mod.rs new file mode 100644 index 00000000..516b4949 --- /dev/null +++ b/axiom-query/src/utils/client_circuit/mod.rs @@ -0,0 +1,12 @@ +pub mod default_circuit; +/// Client circuit metadata type and encoding/decoding +pub mod metadata; +pub mod vkey; + +// Notes: + +// - USER_MAX_OUTPUTS is the number of logical outputs from the user +// - each output is assumed to consist of USER_RESULT_BYTES bytes, this is the format they get it in solidity +// - in ZK circuit, each output is represented using USER_RESULT_FIELD_ELEMENTS field elements +// - this means the user outputs take up `USER_MAX_OUTPUTS * USER_RESULT_FIELD_ELEMENTS` public instance cells +// - currently there may be hardcoded assumptions that results are bytes32, represented as 2 field elements in HiLo form, but the idea is the use the generic constants as much as possible diff --git a/axiom-query/src/utils/client_circuit/vkey.rs b/axiom-query/src/utils/client_circuit/vkey.rs new file mode 100644 index 00000000..a09d31cc --- /dev/null +++ b/axiom-query/src/utils/client_circuit/vkey.rs @@ -0,0 +1,77 @@ +use std::io::Read; + +use anyhow::Result; +use axiom_codec::utils::reader::{read_curve_compressed, read_field_le, read_h256}; +use axiom_eth::{ + halo2_proofs::poly::kzg::commitment::ParamsKZG, + halo2curves::{ + bn256::{Bn256, G1Affine}, + CurveAffine, + }, + snark_verifier::{ + system::halo2::{compile, Config}, + util::arithmetic::{root_of_unity, Domain}, + verifier::plonk::PlonkProtocol, + }, + snark_verifier_sdk::{halo2::aggregation::AggregationCircuit, CircuitExt}, +}; +use rand::{rngs::StdRng, SeedableRng}; +use serde::{Deserialize, Serialize}; + +use super::{ + default_circuit::{dummy_vk_from_metadata, DUMMY_K}, + metadata::{decode_axiom_v2_circuit_metadata, AxiomV2CircuitMetadata}, +}; + +#[derive(Clone, Debug, Serialize, Deserialize, Hash)] +pub struct OnchainVerifyingKey { + pub circuit_metadata: AxiomV2CircuitMetadata, + pub transcript_initial_state: C::Scalar, + pub preprocessed: Vec, +} + +impl OnchainVerifyingKey { + pub fn read(reader: &mut impl Read) -> Result + where + C::Scalar: axiom_codec::Field, + { + let encoded_circuit_metadata = read_h256(reader)?; + let circuit_metadata = decode_axiom_v2_circuit_metadata(encoded_circuit_metadata)?; + + let transcript_initial_state = read_field_le(reader)?; + let mut preprocessed = Vec::new(); + while let Ok(point) = read_curve_compressed(reader) { + preprocessed.push(point); + } + Ok(OnchainVerifyingKey { circuit_metadata, preprocessed, transcript_initial_state }) + } +} + +impl OnchainVerifyingKey { + // @dev Remark: PlonkProtocol fields are public so we can perform "surgery" on them, whereas halo2 VerifyingKey has all fields private so we can't. + pub fn into_plonk_protocol(self, k: usize) -> Result> { + let OnchainVerifyingKey { circuit_metadata, transcript_initial_state, preprocessed } = self; + // We can make a dummy trusted setup here because we replace the fixed commitments afterwards + let kzg_params = ParamsKZG::::setup(DUMMY_K, StdRng::seed_from_u64(0)); + let dummy_vk = dummy_vk_from_metadata(&kzg_params, circuit_metadata.clone())?; + let num_instance = circuit_metadata.num_instance.iter().map(|x| *x as usize).collect(); + let acc_indices = circuit_metadata + .is_aggregation + .then(|| AggregationCircuit::accumulator_indices().unwrap()); + let mut protocol = compile( + &kzg_params, + &dummy_vk, + Config::kzg().with_num_instance(num_instance).with_accumulator_indices(acc_indices), + ); + // See [snark_verifier::system::halo2::compile] to see how [PlonkProtocol] is constructed + // These are the parts of `protocol` that are different for different vkeys or different `k` + protocol.domain = Domain::new(k, root_of_unity(k)); + protocol.domain_as_witness = None; + protocol.preprocessed = preprocessed; + protocol.transcript_initial_state = Some(transcript_initial_state); + // Do not MSM public instances (P::QUERY_INSTANCE should be false) + protocol.instance_committing_key = None; + protocol.linearization = None; + Ok(protocol) + } +} diff --git a/axiom-query/src/utils/codec.rs b/axiom-query/src/utils/codec.rs new file mode 100644 index 00000000..28783232 --- /dev/null +++ b/axiom-query/src/utils/codec.rs @@ -0,0 +1,70 @@ +use axiom_codec::{ + encoder::field_elements::{ + FIELD_ENCODED_SOLIDITY_NESTED_MAPPING_DEPTH_IDX, NUM_FE_ANY, + NUM_FE_SOLIDITY_NESTED_MAPPING_WITHOUT_KEYS, + }, + types::{field_elements::*, native::SubqueryType}, +}; +use axiom_eth::halo2_base::{ + gates::GateInstructions, + utils::ScalarField, + AssignedValue, Context, + QuantumCell::{Constant, Existing}, +}; + +pub type AssignedSubqueryKey = SubqueryKey>; +pub type AssignedSubqueryOutput = SubqueryOutput>; + +pub type AssignedHeaderSubquery = FieldHeaderSubquery>; +pub type AssignedAccountSubquery = FieldAccountSubquery>; +pub type AssignedStorageSubquery = FieldStorageSubquery>; +pub type AssignedTxSubquery = FieldTxSubquery>; +pub type AssignedReceiptSubquery = FieldReceiptSubquery>; +pub type AssignedSolidityNestedMappingSubquery = + FieldSolidityNestedMappingSubquery>; + +pub type AssignedHeaderSubqueryResult = FieldHeaderSubqueryResult>; +pub type AssignedAccountSubqueryResult = FieldAccountSubqueryResult>; +pub type AssignedStorageSubqueryResult = FieldStorageSubqueryResult>; +pub type AssignedTxSubqueryResult = FieldTxSubqueryResult>; +pub type AssignedReceiptSubqueryResult = FieldReceiptSubqueryResult>; +pub type AssignedSolidityNestedMappingSubqueryResult = + FieldSolidityNestedMappingSubqueryResult>; + +pub type AssignedSubqueryResult = FlattenedSubqueryResult>; + +pub fn assign_flattened_subquery_result( + ctx: &mut Context, + f: &FlattenedSubqueryResult, +) -> AssignedSubqueryResult { + let key = f.key.0.map(|x| ctx.load_witness(x)); + let value = f.value.0.map(|x| ctx.load_witness(x)); + FlattenedSubqueryResult::new(SubqueryKey(key), SubqueryOutput(value)) +} + +/// Parses the subquery type and determines the actual number of field elements in the +/// field element encoding, **including** the subquery type. +/// +/// This is `1` more than the flattened length of `FieldSubquery**` when the subquery type is not SolidityNestedMapping. When the subquery type is SolidityNestedMapping, the number of field elements is `1 + NUM_FE_SOLIDITY_NESTED_MAPPING_WITHOUT_KEYS + 2 * mapping_depth`. +pub fn get_num_fe_from_subquery_key( + ctx: &mut Context, + gate: &impl GateInstructions, + key: &AssignedSubqueryKey, +) -> AssignedValue { + // num_fe_by_type will include 1 field element for the type itself, while NUM_FE_ANY does not + let num_fe_by_type: Vec<_> = NUM_FE_ANY + .iter() + .enumerate() + .map(|(subtype, &num_fe)| { + if subtype == SubqueryType::SolidityNestedMapping as usize { + let num_without_keys = + Constant(F::from(NUM_FE_SOLIDITY_NESTED_MAPPING_WITHOUT_KEYS as u64 + 1)); + let mapping_depth = key.0[FIELD_ENCODED_SOLIDITY_NESTED_MAPPING_DEPTH_IDX]; + Existing(gate.mul_add(ctx, mapping_depth, Constant(F::from(2)), num_without_keys)) + } else { + Constant(F::from(num_fe as u64 + 1)) + } + }) + .collect(); + gate.select_from_idx(ctx, num_fe_by_type, key.0[0]) +} diff --git a/axiom-query/src/utils/mod.rs b/axiom-query/src/utils/mod.rs new file mode 100644 index 00000000..710a5fde --- /dev/null +++ b/axiom-query/src/utils/mod.rs @@ -0,0 +1,4 @@ +/// Utilities to reconstruct the halo2 VerifyingKey and proof of a client circuit from abridged on-chain data +pub mod client_circuit; +/// Utilities halo2 functions related to codec +pub mod codec; diff --git a/axiom-query/src/verify_compute/README.md b/axiom-query/src/verify_compute/README.md new file mode 100644 index 00000000..ee90bc15 --- /dev/null +++ b/axiom-query/src/verify_compute/README.md @@ -0,0 +1,214 @@ +# Verify Compute Circuit + +## Query Schema + +The Verify Compute circuit is an `AggregationCircuit` with `VerifierUniverality::Full` that verifies a user supplied `compute_snark`. +Unlike internal universal aggregation circuits that hash part of the verifying key of the aggregated snark into `aggVkeyHash`, here we uniquely tag the verifying key of +`compute_snark` by calculating a `query_schema` inside the Verify Compute circuit and exposing it as a public output. + +In the Axiom V2 Query protocol, we will use _different_ Verify Compute circuits in different aggregation strategies. The guarantee is that: + +> If a `compute_snark` is verified by the Axiom V2 Query protocol, then its `query_schema` is a **unique** identifier for the circuit that was used to create the `compute_snark`. + +We explain below how this guarantee is achieved. + +### Background + +The `halo2` [`VerifyingKey`](https://github.com/axiom-crypto/halo2/blob/f335ffc4440620e3afaa5ba3373764b60a528c51/halo2_proofs/src/plonk.rs#L47) contains the full context of a concrete circuit and the `halo2` proof protocol ensures that if a proof verifies against a `VerifyingKey` and a trusted setup, then the proof is valid precisely for the concrete circuit the `VerifyingKey` was constructed from. This is what is used when you verify a proof directly in Rust using `halo2`. + +The `snark-verifier` crate re-formats the `VerifyingKey` struct into the [`PlonkProtocol`](https://github.com/axiom-crypto/snark-verifier/blob/5c5791fb27c48b004c93d5a4e168f971d4350ce5/snark-verifier/src/verifier/plonk/protocol.rs#L54) struct using the [`compile`](https://github.com/axiom-crypto/snark-verifier/blob/5c5791fb27c48b004c93d5a4e168f971d4350ce5/snark-verifier/src/system/halo2.rs#L82) function. This struct still contains the full context of the circuit used to generate a proof and is all that is needed to verify a proof. + +An aggregation circuit created using `snark-verifier-sdk` will verify a given snark against its `PlonkProtocol`, but defer the final pairing check in the KZG opening by RLC-ing the `G1Affine` points in the pairing check into a running `KzgAccumulator`, which is added to the public instances of the aggregation circuit. Only the final EVM or Rust native verifier will read these `G1Affine` points from the public instances and do the final pairing check. + +In a plain aggregation circuit with `VerifierUniversality::None`, all parts of the `PlonkProtocol` are loaded as constants in the aggregation circuit. In an aggregation circuit with `VerifierUniversality::Full`, the following parts of `PlonkProtocol` are loaded as witnesses: + +- the `log_2` domain size `k` +- the transcript initial state (optional starting value of the transcript) +- the `preprocessed` commitments to the fixed columns and commitment to the permutation argument (sigma polynomials) + +Even with `VerifierUniversality::Full`, the following properties of the `PlonkProtocol` of the snark(s) to be aggregated are hard-coded into the aggregation circuit, meaning they are loaded either explicitly or implicitly as constants and changes to these parameters determine different aggregation circuits: + +- the number of instance columns and number of instances per column +- the number of challenges and number of challenge phases +- the number of advice columns (in each phase) +- the custom gates used by the circuit +- whether the snark to be aggregated has a `KzgAccumulator` in its public instances that needs a deferred pairing check, and if so, what public instance indices they are located at + +Here is the `PlonkProtocol` struct in full: + +```rust +pub struct PlonkProtocol +where + C: CurveAffine, + L: Loader, +{ + // Loaded as witnesses in `Universality::Full`: + /// Working domain. + pub domain: Domain, + /// Prover and verifier common initial state to write to transcript if any. + pub transcript_initial_state: Option, + /// Commitments of preprocessed polynomials. + pub preprocessed: Vec, + + // Always loaded as constants: + /// Number of instances in each instance polynomial. + pub num_instance: Vec, + /// Number of witness polynomials in each phase. + pub num_witness: Vec, + /// Number of challenges to squeeze from transcript after each phase. + pub num_challenge: Vec, + /// Evaluations to read from transcript. + pub evaluations: Vec, + /// [`crate::pcs::PolynomialCommitmentScheme`] queries to verify. + pub queries: Vec, + /// Structure of quotient polynomial. + pub quotient: QuotientPolynomial, + /// Instance polynomials committing key if any. + pub instance_committing_key: Option>, + /// Linearization strategy. + pub linearization: Option, + /// Indices (instance polynomial index, row) of encoded accumulators + pub accumulator_indices: Vec>, +} +``` + +In our use cases the `instance_committing_key` and `linearization` are always `None` so we do not discuss them. + +### Encoded Query Schema + +We define + +```javascript +encoded_query_schema = solidityPacked( + ["uint8", "uint16", "uint8", "bytes32", "bytes32[]"], + [k, result_len, onchain_vkey_len, onchain_vkey] +); + +query_schema = keccak(encoded_query_schema); +``` + +where `k` is the `log_2` size of the domain (number of rows in the circuit), `result_len` is the length of `compute_results` in the public instances of `compute_snark`, where `onchain_vkey_len = onchain_vkey.len()` for `onchain_vkey: bytes32[]`. + +We will define `onchain_vkey` so that it is a serialization of `PlonkProtocol` **under the assumption** that the `PlonkProtocol` is from a circuit created using a `halo2-base` `BaseCircuitBuilder` or `axiom-eth` `RlcCircuitBuilder`, where the circuit _may_ be an aggregation circuit. This `onchain_vkey` is submitted on-chain as calldata as part of a `sendQuery` call, so it is designed to be the minimal context required to reconstruct the `PlonkProtocol` of the `compute_snark` to be verified. + +We define the `onchain_vkey: bytes32[]` as the concatenation of the following fields: + +- `encoded_circuit_metadata: bytes32` - this is loaded as a constant (see below) +- `transcript_initial_state: bytes32` (`Fr`) - this is loaded as a witness +- `preprocessed: bytes32[]` (`Vec`) - this is loaded as a witness + +The parts of `PlonkProtocol` that are loaded as witnesses in the Verify Compute circuit are all included in the `encoded_query_schema`. We now explain how the constant parts are accounted for. + +We define `encoded_circuit_metadata` as an encoding into a `bytes32` of: + +- `version: u8` - a reserved byte for versioning in case `halo2-base` or `halo2` changes +- `num_instance: Vec` +- `RlcCircuitParams` except `k` +- `is_aggregation: bool` + +This encoding is done outside of the circuit, and the encoded `bytes32` is loaded _as a constant_ in the Verify Compute circuit. This can be done because all variables that are encoded are constant in the circuit. +To ensure that what is encoded in `encoded_circuit_metadata` indeed matches what is used in the Verify Compute circuit, we generate the `PlonkProtocol` from the `encoded_circuit_metadata` and the provided `compute_snark` itself. + +Explicitly, `encoded_circuit_metadata` will be a packing of the following struct into a `bytes32`: + +```rust +pub struct AxiomV2CircuitMetadata { + /// Version byte for domain separation on version of Axiom client, halo2-lib, snark-verifier. + /// If `version = x`, this should be thought of as Axiom Query v2.x + pub version: u8, + /// Number of instances in each instance polynomial + pub num_instance: Vec, + /// Number of challenges to squeeze from transcript after each phase. + pub num_challenge: Vec, + + /// Boolean for whether this is an Aggregation Circuit which has a KZG accumulator in the public instances. If true, it must be the first 12 instances. + pub is_aggregation: bool, + + // RlcCircuitParams: + /// The number of advice columns per phase + pub num_advice_per_phase: Vec, + /// The number of special advice columns that have range lookup enabled per phase + pub num_lookup_advice_per_phase: Vec, + /// Number of advice columns for the RLC custom gate + pub num_rlc_columns: u16, + /// The number of fixed columns + pub num_fixed: u8, + + // This is specific to the current Verify Compute Circuit implementation and provided just for data availability: + /// The maximum number of user outputs. Used to determine where to split off `compute_snark`'s public instances between user outputs and data subqueries. + /// This does **not** include the old accumulator elliptic curve points if + /// the `compute_snark` is from an aggregation circuit. + pub user_max_outputs: usize, +} +``` + +The actual encoding is defined [here](../utils/client_circuit/metadata.rs). + +> The Axiom V2 Query protocol will only create Verify Compute circuits that verify snarks created using `RlcCircuitBuilder` or `BaseCircuitBuilder`, where the circuits _may_ be aggregation circuits. + +This can be checked by inspection of the circuits used in production. + +Given this, the `encoded_query_schema` contains all information needed to reconstruct `PlonkProtocol`. Therefore `query_schema` is a unique identifier for `PlonkProtocol` and hence the circuit that `compute_snark` came from. + +## Query Hash + +The Verify Compute Circuit also computes the commitment to the entire query by calculating + +```javascript +query_hash = keccak(encoded_query); +encoded_query = solidityPacked( + ["uint8", "uint64", "bytes32", "bytes"], + [version, source_chain_id, data_query_hash, encoded_compute_query] +); +encoded_compute_query = solidityPacked( + ["bytes", "uint32", "bytes"], + [encoded_query_schema, proof_len, compute_proof] +); +``` + +where `proof_len = compute_proof.len()` in bytes and + +```javascript +compute_proof = solidityPacked( + ["bytes32[2]", "bytes32[]", "bytes"], + [compute_accumulator, compute_results, compute_proof_transcript] +); +``` + +where + +- `compute_accumulator` is either `[bytes32(0), bytes32(0)]` if there is no accumulator, or `[lhs, rhs]` where `lhs, rhs` are compressed `G1Affine` points as `bytes32` representing the KZG accumulator, +- `compute_results` are the public instances of the `compute_snark`, converted from hi-lo form to `bytes32`, that cannot be recovered from data subqueries. The length of `compute_results` equals `result_len`. +- `compute_proof_transcript` is the actual `Vec` halo2 proof. + +## Data Query Format + +Each data query consists of the fields: + +```rust +pub struct AxiomV2DataQuery { + pub source_chain_id: u64, + pub subqueries: Vec, +} +``` + +and each subquery consists of the fields: + +```rust +pub struct Subquery { + /// uint16 type of subquery + pub subquery_type: SubqueryType, + /// Subquery data encoded, _without_ the subquery type. Length is variable and **not** resized. + pub encoded_subquery_data: Bytes, +} +``` + +where `SubqueryType` is an enum represented as `uint16`. + +We commit to the data query by: + +- `dataQueryHash` (bytes32): The Keccak hash of `sourceChainId` concatenated with the array with entries given by: + - `subquery_hash = keccak(solidityPacked(["uint16", "bytes"], [subquery_type, encoded_subquery_data])` + +The individual `subquery_hash`s are already computed by the [Results Root Circuit](../components/results/), so +the Verify Compute Circuit just concatenates them all and computes `keccak` of the concatenation. diff --git a/axiom-query/src/verify_compute/circuit.rs b/axiom-query/src/verify_compute/circuit.rs new file mode 100644 index 00000000..4daeed1a --- /dev/null +++ b/axiom-query/src/verify_compute/circuit.rs @@ -0,0 +1,363 @@ +use std::iter::zip; + +use anyhow::bail; +use axiom_codec::{ + constants::{MAX_SUBQUERY_OUTPUTS, USER_RESULT_FIELD_ELEMENTS, USER_RESULT_LEN_BYTES}, + types::field_elements::SUBQUERY_RESULT_LEN, + HiLo, +}; +use axiom_eth::{ + halo2_base::{ + gates::{GateInstructions, RangeInstructions}, + halo2_proofs::halo2curves::bn256::Fr, + safe_types::{SafeBytes32, SafeTypeChip}, + AssignedValue, + QuantumCell::Constant, + }, + halo2_proofs::plonk::ConstraintSystem, + keccak::{types::ComponentTypeKeccak, KeccakChip}, + rlc::{ + circuit::builder::RlcCircuitBuilder, + types::{AssignedVarLenVec, ConcatVarFixedArrayWitness}, + }, + snark_verifier_sdk::{ + halo2::aggregation::{ + aggregate_snarks, AggregationCircuit, PreprocessedAndDomainAsWitness, + SnarkAggregationOutput, VerifierUniversality, + }, + CircuitExt, SHPLONK, + }, + utils::{ + build_utils::aggregation::CircuitMetadata, + circuit_utils::unsafe_lt_mask, + component::{ + circuit::{ComponentBuilder, ComponentCircuitImpl, CoreBuilder, CoreBuilderOutput}, + promise_collector::PromiseCaller, + promise_loader::single::PromiseLoader, + types::FixLenLogical, + utils::create_hasher, + NUM_COMPONENT_OWNED_INSTANCES, + }, + enforce_conditional_equality, + snark_verifier::NUM_FE_ACCUMULATOR, + uint_to_bytes_be, + }, +}; +use itertools::{zip_eq, EitherOrBoth, Itertools}; + +use crate::{ + components::results::results_root::get_results_root_poseidon, ff::Field as _, + utils::client_circuit::vkey::OnchainVerifyingKey, + verify_compute::types::LogicalPublicInstanceVerifyCompute, +}; + +use super::{ + query_hash::{ + encode_compute_query_phase1, encode_query_schema, get_data_query_hash, get_query_hash_v2, + get_query_schema_hash, + }, + types::{CircuitInputVerifyCompute, ComponentTypeVerifyCompute, CoreParamsVerifyCompute}, +}; + +type F = Fr; // Specialize to Fr for aggregation + +pub struct CoreBuilderVerifyCompute { + input: Option, + params: CoreParamsVerifyCompute, + payload: Option<(KeccakChip, ConcatVarFixedArrayWitness)>, +} + +pub type PromiseLoaderVerifyCompute = PromiseLoader>; +pub type ComponentCircuitVerifyCompute = + ComponentCircuitImpl; + +impl CircuitMetadata for CoreBuilderVerifyCompute { + const HAS_ACCUMULATOR: bool = true; + /// IMPORTANT: Unlike most aggregation circuits, the accumulator indices here DO NOT start from 0 because the first [NUM_COMPONENT_OWNED_INSTANCES] are owned by [`ComponentCircuitImpl`]. + fn accumulator_indices() -> Option> { + Some((0..NUM_FE_ACCUMULATOR).map(|i| (0, NUM_COMPONENT_OWNED_INSTANCES + i)).collect()) + } + fn num_instance(&self) -> Vec { + // For reference only, overriden by `num_instance` in `ComponentCircuitImpl` + vec![ + NUM_COMPONENT_OWNED_INSTANCES + NUM_FE_ACCUMULATOR + super::types::NUM_LOGICAL_INSTANCE, + ] + } +} + +impl CoreBuilderVerifyCompute { + pub fn client_max_outputs(&self) -> usize { + self.params.client_metadata().max_outputs as usize + } +} + +impl ComponentBuilder for CoreBuilderVerifyCompute { + type Params = CoreParamsVerifyCompute; + + fn new(params: Self::Params) -> Self { + Self { input: None, params, payload: None } + } + fn get_params(&self) -> Self::Params { + self.params.clone() + } + fn clear_witnesses(&mut self) { + self.payload = None; + } + fn calculate_params(&mut self) -> Self::Params { + self.params.clone() + } + fn configure_with_params(_: &mut ConstraintSystem, _: Self::Params) {} +} + +impl CoreBuilder for CoreBuilderVerifyCompute { + type CompType = ComponentTypeVerifyCompute; + type PublicInstanceValue = LogicalPublicInstanceVerifyCompute; + type PublicInstanceWitness = LogicalPublicInstanceVerifyCompute>; + type CoreInput = CircuitInputVerifyCompute; + + /// Checks that the circuit params (fixed in circuit) match the parts of the inputs that are witnesses (variable in circuit). + /// + /// If `nonempty_compute_query == false`, then `compute_snark` must be a dummy snark that will verify. + fn feed_input(&mut self, mut input: Self::CoreInput) -> anyhow::Result<()> { + let cap = self.params.subquery_results_capacity(); + let len = input.subquery_results.results.len(); + if cap < len { + bail!("Feed CircuitInputVerifyCompute Error: length of subquery_results {len} is greater than subquery_results_capacity {cap}"); + } + input.subquery_results.resize_with_first(cap); + if self.params.preprocessed_len() != input.compute_snark().protocol.preprocessed.len() { + bail!("Feed CircuitInputVerifyCompute Error: preprocessed_len does not match compute_snark"); + } + // Enforce that the PlonkProtocol in compute_snark **must** be consistent with circuit_params.circuit_metadata + // circuit_metadata is fixed, but transcript_initial_state and preprocessed may be variable + let compute_snark = &mut input.compute_snark; + let client_vk = OnchainVerifyingKey { + circuit_metadata: self.params.client_metadata().clone(), + transcript_initial_state: compute_snark.protocol.transcript_initial_state.unwrap(), + preprocessed: std::mem::take(&mut compute_snark.protocol.preprocessed), + }; + compute_snark.protocol = client_vk.into_plonk_protocol(compute_snark.protocol.domain.k)?; + + self.input = Some(input); + Ok(()) + } + + fn virtual_assign_phase0( + &mut self, + builder: &mut RlcCircuitBuilder, + promise_caller: PromiseCaller, + ) -> CoreBuilderOutput { + // preamble + let keccak_chip = + KeccakChip::new_with_promise_collector(builder.range_chip(), promise_caller.clone()); + let keccak = &keccak_chip; + let range = keccak.range(); + let gate = range.gate(); + let safe = SafeTypeChip::new(range); + + // Assumption: we already have input when calling this function. + let input = self.input.as_ref().unwrap(); + // verify compute + let pool = builder.base.pool(0); + let SnarkAggregationOutput { + mut preprocessed, + mut previous_instances, + accumulator, + mut proof_transcripts, + } = aggregate_snarks::( + pool, + range, + self.params.svk().into(), + [input.compute_snark().clone()], + VerifierUniversality::Full, + ); + let ctx = builder.base.main(0); + let source_chain_id = ctx.load_witness(F::from(input.source_chain_id)); + let ne_cq = safe.load_bool(ctx, input.nonempty_compute_query); + // get information from the compute snark + let PreprocessedAndDomainAsWitness { mut preprocessed, k } = preprocessed.pop().unwrap(); + let transcript_init_state = preprocessed.pop().unwrap(); + let compute_proof_transcript = proof_transcripts.pop().unwrap(); + let mut compute_instances = previous_instances.pop().unwrap(); + // check if `compute_snark` was an aggregation circuit. if it is, remove the old accumulator from the compute_snark public instances + let compute_accumulator = { + let acc_indices = &input.compute_snark().protocol.accumulator_indices; + if acc_indices.is_empty() { + None + } else { + assert_eq!(acc_indices.len(), 1); + // For uniformity, we only allow aggregation circuit accumulator indices to be (0,0), ..., (0, 4 * LIMBS - 1) + assert_eq!(&acc_indices[0], &AggregationCircuit::accumulator_indices().unwrap()); + Some(compute_instances.drain(0..NUM_FE_ACCUMULATOR).collect_vec()) + } + }; + let mut compute_results = compute_instances; + let query_instances = + compute_results.split_off(USER_RESULT_FIELD_ELEMENTS * self.client_max_outputs()); + let result_len = ctx.load_witness(F::from(input.result_len as u64)); + const RESULT_LEN_BITS: usize = 8 * USER_RESULT_LEN_BYTES; + range.range_check(ctx, result_len, RESULT_LEN_BITS); + // Note: result_len is the length in number of HiLos, so `compute_results` should be thought of as having variable length `2 * result_len` + let compute_results = AssignedVarLenVec { values: compute_results, len: result_len }; + // get query schema + let encoded_query_schema = encode_query_schema( + ctx, + range, + k, + result_len, + self.params.client_metadata(), + transcript_init_state, + &preprocessed, + ); + let query_schema = get_query_schema_hash(ctx, keccak, &encoded_query_schema, ne_cq); + // load subquery hashes + let subquery_hashes = &input.subquery_results.subquery_hashes; + let (subquery_hashes, subquery_hashes_hilo): (Vec<_>, Vec<_>) = subquery_hashes + .iter() + .map(|subquery_hash| { + let hilo = subquery_hash.hi_lo(); + let hilo = hilo.map(|x| ctx.load_witness(x)); + let bytes = hilo.map(|x| uint_to_bytes_be(ctx, range, &x, 16)).concat(); + (SafeBytes32::try_from(bytes).unwrap(), hilo) + }) + .unzip(); + // get data query hash + let num_subqueries = input.subquery_results.num_subqueries as u64; + let num_subqueries = ctx.load_witness(F::from(num_subqueries)); + let total_subquery_capacity = input.subquery_results.results.len() as u64; + range.check_less_than_safe(ctx, num_subqueries, total_subquery_capacity + 1); + let (data_query_hash, encoded_source_chain_id) = + get_data_query_hash(ctx, keccak, source_chain_id, &subquery_hashes, num_subqueries); + // get query hash + let (query_hash, concat_proof_witness) = get_query_hash_v2( + ctx, + keccak, + &encoded_source_chain_id, + &data_query_hash, + &encoded_query_schema, + compute_accumulator, + &compute_results, + compute_proof_transcript, + ne_cq, + ); + + // result_len should be <= user.max_outputs if computeQuery non-empty, otherwise <= num_subqueries + let max_res_len = gate.select( + ctx, + Constant(F::from(self.client_max_outputs() as u64)), + num_subqueries, + ne_cq, + ); + let max_res_len_p1 = gate.inc(ctx, max_res_len); + range.check_less_than(ctx, result_len, max_res_len_p1, RESULT_LEN_BITS); + // Load subquery results from custom promise call + let table = input.subquery_results.results.assign(ctx); + // If user query nonempty, then table must match `query_instances` + assert_eq!(query_instances.len() % SUBQUERY_RESULT_LEN, 0); + let user_subqueries = query_instances.chunks_exact(SUBQUERY_RESULT_LEN); + // we will force user subquery to be 0 for index >= num_subqueries. Due to re-sizing logic in ResultsRoot circuit, the table.rows might not be 0 for those indices. + let subquery_mask = + unsafe_lt_mask(ctx, gate, num_subqueries, total_subquery_capacity as usize); + for (i, it) in user_subqueries.zip_longest(&table.rows).enumerate() { + match it { + EitherOrBoth::Both(user, row) => { + let key = &row.key.0; + let out = &row.value.0; + for (&usr, &res) in zip(user, key.iter().chain(out.iter())) { + let res = gate.mul(ctx, res, subquery_mask[i]); + enforce_conditional_equality(ctx, gate, usr, res, ne_cq); + } + } + EitherOrBoth::Left(user) => { + // If for some reason we allow promise table to be shorter than user max subqueries, the extra user subqueries should all be Null + for v in user { + gate.assert_is_const(ctx, v, &Fr::ZERO); + } + } + EitherOrBoth::Right(_) => { + // It is OK to have more promised subquery results than user subqueries + break; + } + } + } + + // user results are bytes32 in hi-lo form, we need to compute keccak hash of the bytes32 concatenated + // length in bytes + let result_byte_len = gate.mul(ctx, result_len, Constant(F::from(32))); + // The compute results are the `compute_results` if non-empty computeQuery. Otherwise they are the first `result_len` output values from the subquery results table + // We account for the fact that `compute_results` and `table.rows` can have different compile-time lengths + let mut compute_results_bytes = vec![]; + for it in + compute_results.values.chunks_exact(USER_RESULT_FIELD_ELEMENTS).zip_longest(&table.rows) + { + // hi-lo form + let words = match it { + EitherOrBoth::Both(user, subquery_res) => zip_eq(user, &subquery_res.value.0) + .map(|(&user, &val)| gate.select(ctx, user, val, *ne_cq.as_ref())) + .collect_vec(), + EitherOrBoth::Left(user) => user.to_vec(), + EitherOrBoth::Right(subquery_res) => subquery_res.value.0.to_vec(), + }; + for word in &words { + // this performs the 128 bit range check: + compute_results_bytes.extend(uint_to_bytes_be(ctx, range, word, 16)); + } + } + // we checked result_len <= computeQuery.k != 0 ? user.max_outputs : num_subqueries above + // so the above selection is safe + let compute_results_hash = keccak.keccak_var_len( + ctx, + compute_results_bytes.into_iter().map(From::from).collect(), + result_byte_len, + 0, + ); + + // not optimal: recomputing spec even though PromiseLoader also uses hasher + let mut poseidon = create_hasher(); + poseidon.initialize_consts(ctx, range.gate()); + // generate promise commit from table and subquery_hashes + let results_root_poseidon = { + assert_eq!(table.rows.len(), total_subquery_capacity as usize); + get_results_root_poseidon( + ctx, + range, + &poseidon, + &table.rows, + num_subqueries, + &subquery_mask, + ) + }; + let promise_subquery_hashes = { + // this exactly matches what is done in resultsRoot component circuit. + // however `max_num_subqueries` here does not need to match the `total_subquery_capacity` in ResultsRoot circuit + let to_commit = subquery_hashes_hilo.into_iter().flatten().collect_vec(); + let len = gate.mul(ctx, num_subqueries, Constant(F::from(MAX_SUBQUERY_OUTPUTS as u64))); + poseidon.hash_var_len_array(ctx, range, &to_commit, len) + }; + + let logical_pis = LogicalPublicInstanceVerifyCompute { + accumulator, + source_chain_id, + compute_results_hash: HiLo::from_hi_lo(compute_results_hash.hi_lo()), + query_hash: HiLo::from_hi_lo(query_hash.hi_lo()), + query_schema: HiLo::from_hi_lo(query_schema.hi_lo()), + results_root_poseidon, + promise_subquery_hashes, + }; + self.payload = Some((keccak_chip, concat_proof_witness)); + + CoreBuilderOutput { + public_instances: logical_pis.into_raw(), + virtual_table: vec![], + logical_results: vec![], + } + } + + fn virtual_assign_phase1(&mut self, builder: &mut RlcCircuitBuilder) { + let (keccak, payload) = self.payload.take().unwrap(); + let gate = keccak.gate(); + let rlc = builder.rlc_chip(gate); + let rlc_pair = builder.rlc_ctx_pair(); + encode_compute_query_phase1(rlc_pair, gate, &rlc, payload); + } +} diff --git a/axiom-query/src/verify_compute/mod.rs b/axiom-query/src/verify_compute/mod.rs new file mode 100644 index 00000000..e0bfd5a4 --- /dev/null +++ b/axiom-query/src/verify_compute/mod.rs @@ -0,0 +1,16 @@ +//! # Verify Compute Circuit +//! +//! Verifies the user proof (`computeQuery`) and calculates the query hash +//! by de-commiting subquery results and subquery hashes from two public instances. +//! +//! The Axiom Aggregation Circuit **must** check that these public instances agree +//! with the public instances from the Subquery Aggregation Circuit. +//! +pub mod circuit; +/// Compute `dataQueryHash` and `queryHash` +pub mod query_hash; +pub mod types; +pub mod utils; + +#[cfg(test)] +pub mod tests; diff --git a/axiom-query/src/verify_compute/query_hash.rs b/axiom-query/src/verify_compute/query_hash.rs new file mode 100644 index 00000000..5ea85b85 --- /dev/null +++ b/axiom-query/src/verify_compute/query_hash.rs @@ -0,0 +1,400 @@ +use std::iter; + +use axiom_codec::{constants::SOURCE_CHAIN_ID_BYTES, HiLo}; +use axiom_eth::{ + halo2_base::{ + gates::{GateInstructions, RangeChip, RangeInstructions}, + safe_types::{SafeBool, SafeByte, SafeBytes32, SafeTypeChip}, + AssignedValue, Context, + QuantumCell::Constant, + }, + keccak::{types::KeccakVarLenQuery, KeccakChip}, + snark_verifier::loader::halo2::halo2_ecc::bigint::{big_is_zero, OverflowInteger}, + snark_verifier_sdk::{halo2::aggregation::AssignedTranscriptObject, BITS, LIMBS}, + utils::{uint_to_bytes_be, uint_to_bytes_le}, +}; +use itertools::Itertools; + +use crate::Field; + +pub(super) use bn254_specific::*; + +/// `subquery_hashes` has been resized with dummies to `max_num_subqueries` length (known as compile time). This represents a variable length array of hashes with true length given by `num_subqueries`. +/// +/// ## Output +/// - `data_query_hash` (bytes32) +/// - `encoded_source_chain_id` (big-endian bytes) +pub fn get_data_query_hash( + ctx: &mut Context, + keccak: &KeccakChip, + source_chain_id: AssignedValue, // u64 + subquery_hashes: &[SafeBytes32], + num_subqueries: AssignedValue, +) -> (KeccakVarLenQuery, Vec>) { + let range = keccak.range(); + let encoded_source_chain_id = + uint_to_bytes_be(ctx, range, &source_chain_id, SOURCE_CHAIN_ID_BYTES); // u64 + let encoded: Vec<_> = iter::empty() + .chain(encoded_source_chain_id.iter().map(|b| *b.as_ref())) + .chain(subquery_hashes.iter().flat_map(|subquery_hash| subquery_hash.value().to_vec())) + .collect(); + let encoded_len = range.gate.mul_add( + ctx, + Constant(F::from(32)), + num_subqueries, + Constant(F::from(SOURCE_CHAIN_ID_BYTES as u64)), + ); + ( + keccak.keccak_var_len(ctx, encoded, encoded_len, SOURCE_CHAIN_ID_BYTES), + encoded_source_chain_id, + ) +} + +// Everything involving vkey or proof are specific to BN254: +// Make module public for docs. +pub mod bn254_specific { + use axiom_codec::constants::{ENCODED_K_BYTES, USER_PROOF_LEN_BYTES, USER_RESULT_LEN_BYTES}; + use axiom_eth::{ + halo2_base::safe_types::VarLenBytesVec, + halo2curves::bn256::Fr, + rlc::{ + chip::RlcChip, + circuit::builder::RlcContextPair, + concat_array::{concat_var_fixed_array_phase0, concat_var_fixed_array_phase1}, + types::{AssignedVarLenVec, ConcatVarFixedArrayWitness}, + }, + utils::circuit_utils::bytes::encode_const_u8_to_safe_bytes, + }; + + use crate::utils::client_circuit::metadata::AxiomV2CircuitMetadata; + + use super::*; + + type F = Fr; + + /// Length of `encoded_query_schema` and `compute_proof_transcript` are known at compile time. + /// keccak(version . source_chain_id . data_query_hash . encoded_compute_query) + #[allow(clippy::too_many_arguments)] + pub fn get_query_hash_v2( + ctx: &mut Context, + keccak: &KeccakChip, + encoded_source_chain_id: &[SafeByte], // 8 big-endian bytes + data_query_hash: &KeccakVarLenQuery, + encoded_query_schema: &[SafeByte], + compute_accumulator: Option>>, + compute_results: &AssignedVarLenVec, + compute_proof_transcript: Vec, + nonempty_compute_query: SafeBool, + ) -> (KeccakVarLenQuery, ConcatVarFixedArrayWitness) { + let range = keccak.range(); + let gate = range.gate(); + let encoded_version = encode_const_u8_to_safe_bytes(ctx, axiom_codec::VERSION); + let data_query_hash = data_query_hash.output_bytes.value(); + + let (encoded_compute_query, concat_proof_witness) = encode_compute_query_phase0( + ctx, + range, + encoded_query_schema, + compute_accumulator, + compute_results, + compute_proof_transcript, + ); + // if nonempty_compute_query = false, then `encoded_compute_query` should equal `[0u8]` + let nonempty = *nonempty_compute_query.as_ref(); + let true_k = gate.mul(ctx, nonempty, encoded_compute_query.bytes()[0]); + // the minimum length of encoded_compute_query, if computeQuery is empty + let min_encoded_compute_query_len = ENCODED_K_BYTES + USER_RESULT_LEN_BYTES; + assert!(encoded_compute_query.max_len() >= min_encoded_compute_query_len); + let ne_compute_len = gate.sub( + ctx, + *encoded_compute_query.len(), + Constant(F::from(min_encoded_compute_query_len as u64)), + ); + // version . source_chain_id . data_query_hash + // uint8 . uint64 . bytes32 => 1 + 8 + 32 + let min_len = (1 + SOURCE_CHAIN_ID_BYTES + 32 + min_encoded_compute_query_len) as u64; + let encoded_len = gate.mul_add(ctx, ne_compute_len, nonempty, Constant(F::from(min_len))); + let concatenated = iter::empty() + .chain(encoded_version.iter().chain(encoded_source_chain_id).map(|b| *b.as_ref())) + .chain(data_query_hash.iter().copied()) + .chain([true_k]) + .chain(encoded_compute_query.bytes().iter().skip(1).map(|b| *b.as_ref())) + .collect_vec(); + ( + keccak.keccak_var_len(ctx, concatenated, encoded_len, min_len as usize), + concat_proof_witness, + ) + } + + /// Length of `encoded_query_schema` is assumed to be constant at compile time. + /// If `nonempty_compute_query` is false, return bytes32(0). + pub fn get_query_schema_hash( + ctx: &mut Context, + keccak: &KeccakChip, + encoded_query_schema: &[SafeByte], + nonempty_compute_query: SafeBool, + ) -> HiLo> { + let range = keccak.range(); + let encoded = encoded_query_schema.iter().map(|b| *b.as_ref()).collect(); + let query_schema = keccak.keccak_fixed_len(ctx, encoded); + let query_schema = query_schema.hi_lo(); + let query_schema = query_schema.map(|x| range.gate.mul(ctx, x, nonempty_compute_query)); + HiLo::from_hi_lo(query_schema) + } + + /// Length of `encoded_query_schema`, `compute_proof_transcript` are known at compile time. + /// `compute_results` is a variable length array of HiLo field elements (two field elements represent a `bytes32`). + /// + /// We define `compute_proof = solidityPacked(["bytes32[2]", "bytes32[]", "bytes"], [compute_accumulator, compute_results, compute_proof_transcript])` where `compute_results` is of length `result_len` (as `bytes32[]`). + /// Here `compute_accumulator` is either `[bytes32(0), bytes32(0)]` if no accumulator exists, or `[lhs, rhs]` where `lhs, rhs` are the `G1Affine` points, each compressed to `bytes32`. + /// See [axiom_codec::decoder::native::decode_compute_snark] and [axiom_codec::types::native::AxiomV2ComputeSnark]. + /// + /// We encode the concatenation of `solidityPacked(["bytes", "uint32", "bytes"], [encoded_query_schema, proof_len, compute_proof])` + /// where `proof_len` is the [u32] length of `compute_proof` in bytes. + /// + /// ## Special Note + /// - `compute_results` is actually _not_ all public instances of `proof_transcript`. We skip the instances corresponding to subquery results. + /// ## Note on endian-ness + /// - The encoding of `proof_transcript` is governed by the inherit implementations of `to_repr` + /// in `halo2curves`. These happen to be **little-endian** for BN254. + /// - _However_, the encoding of `compute_results` is chosen to match + /// [axiom_eth::snark_verifier::loader::evm::encode_calldata], where `compute_results` encodes field elements + /// as **big-endian**. + /// Since it's harder to change `halo2curves` and `snark_verifier`, + /// we will just have to deal with this inconsistency. + /// - The [u32] `proof_len` is encoded to [USER_PROOF_LEN_BYTES] bytes in **big endian** since + /// this is more compatible with Solidity packing. + pub fn encode_compute_query_phase0( + ctx: &mut Context, + range: &RangeChip, + encoded_query_schema: &[SafeByte], + compute_accumulator: Option>>, + compute_results: &AssignedVarLenVec, + compute_proof_transcript: Vec, + ) -> (VarLenBytesVec, ConcatVarFixedArrayWitness) { + let gate = range.gate(); + // compute results length in bytes + let mut compute_results_len = gate.mul(ctx, compute_results.len, Constant(F::from(32))); + // compute results conversion from HiLo to bytes + let mut compute_results = compute_results + .values + .iter() + .flat_map(|fe| uint_to_bytes_be(ctx, range, fe, 16)) + .collect_vec(); + let mut max_len = compute_results.len(); + // if `compute_accumulator` exists, encode as `bytes32 . bytes32` (`CompressedG1Affine, CompressedG1Affine`) and prepend to `user_outputs` + // else encode as `bytes32(0) . bytes32(0)` + const NUM_ACCUMULATOR_BYTES: usize = 2 * 32; + let encoded_compute_accumulator = if let Some(acc) = compute_accumulator { + // KzgAccumulator from snark-verifier consists of `lhs, rhs` which are two G1Affine points for the lhs and rhs of pairing check + let [lhs_x, lhs_y, rhs_x, rhs_y]: [_; 4] = acc + .chunks_exact(LIMBS) + .map(|limbs| { + let limbs: [_; LIMBS] = limbs.to_vec().try_into().unwrap(); + limbs + }) + .collect_vec() + .try_into() + .unwrap(); + let lhs = compress_bn254_g1_affine_point_to_bytes(ctx, range, lhs_x, lhs_y); + let rhs = compress_bn254_g1_affine_point_to_bytes(ctx, range, rhs_x, rhs_y); + [lhs, rhs].concat() + } else { + let const_zero_byte = encode_const_u8_to_safe_bytes(ctx, 0)[0]; + vec![const_zero_byte; NUM_ACCUMULATOR_BYTES] + }; + compute_results_len = + gate.add(ctx, compute_results_len, Constant(F::from(NUM_ACCUMULATOR_BYTES as u64))); + compute_results = [encoded_compute_accumulator, compute_results].concat(); + max_len += NUM_ACCUMULATOR_BYTES; + let compute_results = VarLenBytesVec::new(compute_results, compute_results_len, max_len); + let encoded_proof = encode_proof_to_bytes(ctx, range, compute_proof_transcript); + let concat_witness = concat_var_fixed_array_phase0( + ctx, + gate, + compute_results.into(), + encoded_proof.into_iter().map(From::from).collect(), + ); + // encoded_compute_accumulator . compute_results (no accumulator) . compute_proof_transcript + let compute_proof = concat_witness + .concat + .values + .iter() + .map(|byte| SafeTypeChip::unsafe_to_byte(*byte)) + .collect_vec(); + let proof_len = concat_witness.concat.len; + let encoded_pf_len = uint_to_bytes_be(ctx, range, &proof_len, USER_PROOF_LEN_BYTES); + // The full concatenation is variable length, but `encoded_query_schema`, `encoded_pf_len` are fixed length, so only variable-ness is in `compute_proof` + let concat_full = [encoded_query_schema.to_vec(), encoded_pf_len, compute_proof].concat(); + let prefix_len = encoded_query_schema.len() + USER_PROOF_LEN_BYTES; + let concat_len = gate.add(ctx, proof_len, Constant(F::from(prefix_len as u64))); + let max_len = concat_full.len(); + let concat_var = VarLenBytesVec::new(concat_full, concat_len, max_len); + /*{ + let len = concat_var.len().value().get_lower_32() as usize; + println!("encodedComputeQuery"); + let enc = get_bytes(&concat_var.bytes()[..len]); + dbg!(ethers_core::types::H256(keccak256(&enc[..]))); + }*/ + (concat_var, concat_witness) + } + + pub fn encode_compute_query_phase1( + rlc_pair: RlcContextPair, + gate: &impl GateInstructions, + rlc: &RlcChip, + concat_witness: ConcatVarFixedArrayWitness, + ) { + concat_var_fixed_array_phase1(rlc_pair, gate, rlc, concat_witness); + } + + /// Returns `solidityPacked(["uint8", "uint16", "uint8", "bytes32[]"], [k, result_len, partial_vkey_len, encoded_partial_vkey])` where `partial_vkey_len` is the length of `encoded_partial_vkey` as `bytes32[]`. + /// + /// We need the `result_len` encoded correctly even if `k = 0`. + /// + /// ## Notes + /// - `partial_vkey_len` is known at compile time. + /// - `is_aggregation` needs to be part of the `encoded_partial_vkey` because it specifies + /// whether the VerifyCompute circuit will RLC the accumulator from the public instances of the compute snark into the new accumulator of the VerifyCompute circuit. + pub fn encode_query_schema( + ctx: &mut Context, + range: &RangeChip, + k: AssignedValue, + result_len: AssignedValue, + circuit_metadata: &AxiomV2CircuitMetadata, + transcript_initial_state: AssignedValue, + preprocessed: &[AssignedValue], + ) -> Vec> { + let encoded_k = uint_to_bytes_be(ctx, range, &k, ENCODED_K_BYTES); // u8 + let encoded_result_len = uint_to_bytes_be(ctx, range, &result_len, USER_RESULT_LEN_BYTES); // u16 + let encoded_onchain_vkey = encode_onchain_vkey( + ctx, + range, + circuit_metadata, + transcript_initial_state, + preprocessed, + ); + assert_eq!(encoded_onchain_vkey.len() % 32, 0); + let onchain_vkey_len = encoded_onchain_vkey.len() / 32; + let encoded_vkey_length = + encode_const_u8_to_safe_bytes(ctx, onchain_vkey_len.try_into().unwrap()).to_vec(); + + [encoded_k, encoded_result_len, encoded_vkey_length, encoded_onchain_vkey].concat() + } + + /// Encode in virtual cells. + /// ## Output + /// - The encoding in bytes: `solidityPacked(["uint256", "bytes32", "bytes32[]"], [is_aggregation, transcript_initial_state, preprocessed])`. This is interpretted by the smart contract as `vkey: bytes32[]`. + /// - Field and curve elements are encoded in **little endian**. + /// - `is_aggregation` (boolean) is encoded as `uint256(is_aggregation)` in **big endian**. + /// - It is wasteful to use 32 bytes, but the smart contract expects `vkey` to be `bytes32[]` + /// ## Assumptions + /// - Length of `preprocessed` is assumed to be constant at compile time. + /// - `transcript_initial_state` is a **field element**. + /// - `preprocessed` is an array of field elements that represent an array of BN254 G1Affine points + /// that form the fixed commitments of the vkey. These are non-native encoded with Fq points + /// as big integers (ProperCrtUint) with [LIMBS] limbs of [BITS] bits each. + /// See [compress_bn254_g1_affine_point_to_bytes] for further details. This depends on the + /// specifics of [`snark_verifier_sdk`](axiom_eth::snark_verifier_sdk) and the exact compression method of BN254, so we keep it + /// as a private function. + pub fn encode_onchain_vkey( + ctx: &mut Context, + range: &RangeChip, + circuit_metadata: &AxiomV2CircuitMetadata, + transcript_initial_state: AssignedValue, + preprocessed: &[AssignedValue], + ) -> Vec> { + let encoded_circuit_metadata = circuit_metadata.encode().unwrap().to_fixed_bytes(); + // Load encoded_circuit_metadata as **constant** bytes32 + let encoded_circuit_metadata_const = encoded_circuit_metadata + .map(|b| SafeTypeChip::unsafe_to_byte(ctx.load_constant(F::from(b as u64)))); + + let encoded_init_state = uint_to_bytes_le(ctx, range, &transcript_initial_state, 32); + assert_eq!(preprocessed.len() % (2 * LIMBS), 0); + let encoded_preprocessed = preprocessed + .chunks_exact(2 * LIMBS) + .flat_map(|chunk| { + let x = chunk[..LIMBS].try_into().unwrap(); + let y = chunk[LIMBS..].try_into().unwrap(); + compress_bn254_g1_affine_point_to_bytes(ctx, range, x, y) + }) + .collect_vec(); + [encoded_circuit_metadata_const.to_vec(), encoded_init_state, encoded_preprocessed].concat() + } + + /// The proof transcript is given as a sequence of either [Fr] field elements or G1Affine points. + /// They are encoded in **little-endian** as described in [encode_partial_vkey]. + /// + /// ## Output + /// - Returns encoded proof bytes in **little endian** + pub fn encode_proof_to_bytes( + ctx: &mut Context, + range: &RangeChip, + proof: Vec, + ) -> Vec> { + proof + .into_iter() + .flat_map(|obj| match obj { + AssignedTranscriptObject::Scalar(scalar) => { + uint_to_bytes_le(ctx, range, &scalar, 32) + } + AssignedTranscriptObject::EcPoint(point) => { + let x = point.x().limbs().try_into().unwrap(); + let y = point.y().limbs().try_into().unwrap(); + compress_bn254_g1_affine_point_to_bytes(ctx, range, x, y) + } + }) + .collect() + } + + /// Takes a BN254 G1Affine point, which is non-native encoded with each coordinate as a + /// ProperCrtUint with [LIMBS] limbs with [BITS] bits each and compresses it to 32 bytes + /// exactly following https://github.com/axiom-crypto/halo2curves/blob/main/src/derive/curve.rs#L138 + /// + /// This relies on both the exact format of the input from [snark_verifier_sdk] and also + /// the exact compression method for BN254 in `halo2curves`, so this is a private function. + /// + /// The compression uses the fact that bn254::Fq has exactly 254 bits, which is 2 bits less than 256. + /// + /// ## Output + /// - Returns bytes in **little endian**. + /// ## Assumptions + /// - `(x, y)` is a point on BN254 G1Affine. + /// - `x, y` are in little endian limbs. + pub fn compress_bn254_g1_affine_point_to_bytes( + ctx: &mut Context, + range: &RangeChip, + x: [AssignedValue; LIMBS], + y: [AssignedValue; LIMBS], + ) -> Vec> { + let gate = range.gate(); + let x_is_zero = big_is_zero::positive(gate, ctx, OverflowInteger::new(x.to_vec(), BITS)); + let y_is_zero = big_is_zero::positive(gate, ctx, OverflowInteger::new(y.to_vec(), BITS)); + let is_identity = gate.and(ctx, x_is_zero, y_is_zero); + + // boolean + let y_last_bit = range.get_last_bit(ctx, y[0], BITS); + + // luckily right now BITS is multiple of 8, so limb conversion to bytes is a little easier + if BITS % 8 != 0 { + panic!("BITS must be a multiple of 8") + } + // even though BITS * LIMBS > 256, we know the whole bigint fits in 256 bits, so we truncate to 32 bytes + let mut x_bytes = x + .iter() + .flat_map(|limb| uint_to_bytes_le(ctx, range, limb, BITS / 8)) + .take(32) + .collect_vec(); + // last_byte is guaranteed to be 254 % 8 = 6 bits + let mut last_byte = *x_bytes.last().unwrap().as_ref(); + // sign = y_last_bit << 6 + // last_byte |= sign + last_byte = gate.mul_add(ctx, y_last_bit, Constant(Fr::from(1 << 6)), last_byte); + // if is_identity, then answer should be [0u8; 32] with last byte = 0b1000_0000 + // if is_identity, then x_bytes should be all 0s, and y_last_bit = 0, so all we have to do is |= 0b1000_0000 = 1 << 7 + last_byte = gate.mul_add(ctx, is_identity, Constant(Fr::from(1 << 7)), last_byte); + *x_bytes.last_mut().unwrap() = SafeTypeChip::unsafe_to_byte(last_byte); + + x_bytes + } +} diff --git a/axiom-query/src/verify_compute/tests/aggregation.rs b/axiom-query/src/verify_compute/tests/aggregation.rs new file mode 100644 index 00000000..8348c02b --- /dev/null +++ b/axiom-query/src/verify_compute/tests/aggregation.rs @@ -0,0 +1,259 @@ +use axiom_codec::{ + constants::USER_MAX_OUTPUTS, + decoder::native::decode_compute_snark, + encoder::native::{get_query_hash_v2, get_query_schema_hash}, + types::native::{AxiomV2ComputeQuery, AxiomV2ComputeSnark, AxiomV2DataQuery}, + utils::native::{decode_hilo_to_h256, encode_h256_to_hilo}, + HiLo, +}; +use axiom_eth::{ + halo2_base::{ + gates::circuit::CircuitBuilderStage, + halo2_proofs::{dev::MockProver, poly::commitment::ParamsProver}, + utils::fs::gen_srs, + }, + halo2curves::bn256::{Fr, G1Affine}, + snark_verifier::pcs::{ + kzg::{KzgAccumulator, LimbsEncoding}, + AccumulatorEncoding, + }, + snark_verifier_sdk::{ + gen_pk, + halo2::{ + aggregation::{AggregationCircuit, VerifierUniversality}, + gen_snark_shplonk, + }, + NativeLoader, Snark, BITS, LIMBS, SHPLONK, + }, + utils::{ + build_utils::pinning::CircuitPinningInstructions, + component::ComponentCircuit, + snark_verifier::{AggregationCircuitParams, NUM_FE_ACCUMULATOR}, + }, +}; + +use ethers_core::{types::H256, utils::keccak256}; +use itertools::Itertools; +#[cfg(test)] +use test_log::test; + +use crate::{ + components::results::types::{CircuitOutputResultsRoot, LogicOutputResultsRoot}, + utils::client_circuit::metadata::AxiomV2CircuitMetadata, + verify_compute::{ + tests::utils::dummy_compute_snark, + types::{ + CircuitInputVerifyCompute, CoreParamsVerifyCompute, LogicalPublicInstanceVerifyCompute, + }, + utils::{ + get_onchain_vk_from_protocol, verify_snark, write_onchain_vkey, UserCircuitParams, + DEFAULT_CLIENT_METADATA, DEFAULT_USER_PARAMS, + }, + }, +}; + +use super::{get_test_result, prepare_mock_circuit, prepare_prover_circuit, test_compute_circuit}; + +fn test_compute_app_snark( + k: u32, + user_params: UserCircuitParams, + subquery_results: LogicOutputResultsRoot, + result_len: u16, +) -> Snark { + let app_params = gen_srs(k); + let compute_app = test_compute_circuit(k, user_params, subquery_results, result_len as usize); + let pk = gen_pk(&app_params, &compute_app, None); + gen_snark_shplonk(&app_params, &pk, compute_app, None::<&str>) +} + +// this one doesn't have instances in special format +// Note: when `snark` has many public instances, the aggregation circuit needs to do many Poseidon hashes. There are ways to mitigate this if you only reformat the aggregation circuit's public instances to be the correct format: there is no strict requirement on the public instances of the snark to be aggregated +pub fn test_aggregation_circuit( + agg_circuit_params: AggregationCircuitParams, + snark: Snark, +) -> AggregationCircuit { + let params = gen_srs(agg_circuit_params.degree); + let mut circuit = AggregationCircuit::new::( + CircuitBuilderStage::Mock, + agg_circuit_params, + ¶ms, + [snark], + VerifierUniversality::None, + ); + circuit.expose_previous_instances(false); + circuit +} + +fn get_metadata(agg_circuit_params: AggregationCircuitParams) -> AxiomV2CircuitMetadata { + AxiomV2CircuitMetadata { + version: 0, + num_instance: vec![DEFAULT_CLIENT_METADATA.num_instance[0] + NUM_FE_ACCUMULATOR as u32], + num_challenge: vec![0], + is_aggregation: true, + num_advice_per_phase: vec![agg_circuit_params.num_advice as u16], + num_lookup_advice_per_phase: vec![agg_circuit_params.num_lookup_advice as u8], + num_rlc_columns: 0, + num_fixed: agg_circuit_params.num_fixed as u8, + max_outputs: USER_MAX_OUTPUTS as u16, + } +} + +fn get_test_input( + app_snark: Snark, + source_chain_id: u64, + subquery_results: LogicOutputResultsRoot, + result_len: u16, + agg_k: u32, +) -> (CoreParamsVerifyCompute, CircuitInputVerifyCompute) { + let agg_circuit_params = AggregationCircuitParams { + degree: agg_k, + num_advice: 20, + num_lookup_advice: 2, + num_fixed: 1, + lookup_bits: agg_k as usize - 1, + }; + let agg_params = gen_srs(agg_k); + let subquery_results = CircuitOutputResultsRoot::::try_from(subquery_results).unwrap(); + let client_metadata = get_metadata(agg_circuit_params); + + let agg_compute_snark = { + let agg_circuit = test_aggregation_circuit(agg_circuit_params, app_snark); + // let stats = agg_circuit.builder.statistics(); + // dbg!(stats.gate.total_advice_per_phase); + // dbg!(stats.gate.total_fixed); + // dbg!(stats.total_lookup_advice_per_phase); + let pk = gen_pk(&agg_params, &agg_circuit, None); + gen_snark_shplonk(&agg_params, &pk, agg_circuit, None::<&str>) + }; + dbg!(&client_metadata); + let core_params = CoreParamsVerifyCompute::new( + subquery_results.results.len(), + agg_params.get_g()[0], + client_metadata, + agg_compute_snark.protocol.preprocessed.len(), + ); + println!("agg_compute_snark.protocol.preprocessed.len(): {}", core_params.preprocessed_len()); + + ( + core_params, + CircuitInputVerifyCompute::new( + source_chain_id, + subquery_results, + true, + result_len, + agg_compute_snark, + ), + ) +} + +// this test involves creating aggregation snark with real prover so it is heavy, but we should still include it in the CI +#[test] +fn test_verify_compute_agg_mock() -> anyhow::Result<()> { + let (_input_results, data_results) = get_test_result::(); + let num_subqueries = data_results.num_subqueries; + let result_len = num_subqueries as u16; + + let compute_app_snark = + test_compute_app_snark(14, DEFAULT_USER_PARAMS, data_results.clone(), result_len); + let source_chain_id = 1; + let agg_k = 20; + let (core_params, input) = + get_test_input(compute_app_snark, source_chain_id, data_results.clone(), result_len, agg_k); + + // additional preparation for instance checks later + let subqueries = + data_results.results[..num_subqueries].iter().map(|r| r.subquery.clone()).collect(); + let agg_compute_snark = input.compute_snark(); + let data_query = AxiomV2DataQuery { source_chain_id, subqueries }; + let compute_vkey = get_onchain_vk_from_protocol( + &agg_compute_snark.protocol, + core_params.client_metadata().clone(), + ); + let agg_instances = &agg_compute_snark.instances[0]; + let KzgAccumulator { lhs, rhs } = + as AccumulatorEncoding>::from_repr( + &agg_instances[..NUM_FE_ACCUMULATOR].iter().collect_vec(), + ) + .unwrap(); + let compute_results = agg_instances[NUM_FE_ACCUMULATOR..] + .chunks(2) + .take(result_len as usize) + .map(|c| decode_hilo_to_h256(HiLo::from_hi_lo([c[0], c[1]]))) + .collect_vec(); + let compute_snark = AxiomV2ComputeSnark { + kzg_accumulator: Some((lhs, rhs)), + compute_results, + proof_transcript: agg_compute_snark.proof.clone(), + }; + let compute_proof = compute_snark.encode()?.into(); + let compute_query = AxiomV2ComputeQuery { + k: agg_k as u8, + result_len, + vkey: write_onchain_vkey(&compute_vkey).unwrap(), + compute_proof, + }; + + let circuit_k = 19u32; + let keccak_f_capacity = 200; + let circuit = prepare_mock_circuit(core_params, circuit_k as usize, keccak_f_capacity, input); + let instances = circuit.get_public_instances(); + + // check instances: + // check query hash and query schema calculation + let logic_pis = instances.other.clone(); + let LogicalPublicInstanceVerifyCompute { + query_hash, query_schema, compute_results_hash, .. + } = logic_pis.try_into()?; + let native_query_schema = + get_query_schema_hash(compute_query.k, compute_query.result_len, &compute_query.vkey)?; + assert_eq!(&query_schema, &encode_h256_to_hilo(&native_query_schema)); + let native_query_hash = get_query_hash_v2(source_chain_id, &data_query, &compute_query)?; + // dbg!(native_query_hash); + assert_eq!(&query_hash, &encode_h256_to_hilo(&native_query_hash)); + let compute_snark = + decode_compute_snark(&mut &compute_query.compute_proof[..], result_len, true)?; + let encode_results = compute_snark.compute_results.iter().map(|r| r.0.to_vec()).concat(); + let native_results_hash = H256(keccak256(encode_results)); + assert_eq!(&compute_results_hash, &encode_h256_to_hilo(&native_results_hash)); + + // actually run mockprover + MockProver::run(circuit_k, &circuit, vec![instances.into()]).unwrap().assert_satisfied(); + Ok(()) +} + +#[test] +fn test_verify_compute_agg_prover() -> anyhow::Result<()> { + let (_input_results, data_results) = get_test_result::(); + let num_subqueries = data_results.num_subqueries; + let result_len = num_subqueries as u16; + + // === create proving key for the VerifyCompute circuit === + // we test that only the aggregation circuit's shape matters, whereas the original snark to be aggregated can have different shapes + let mut user_params = DEFAULT_USER_PARAMS; + user_params.num_advice_cols += 1; + let app_k = 12; + let agg_k = 20; + let compute_app_snark = dummy_compute_snark(&gen_srs(app_k), user_params, "./data"); + let (core_params, input) = get_test_input(compute_app_snark, 0, data_results.clone(), 0, agg_k); + + let circuit_k = 19u32; + let circuit = prepare_mock_circuit(core_params, circuit_k as usize, 200, input); + + let kzg_params = gen_srs(circuit_k); + let pk = gen_pk(&kzg_params, &circuit, None); + let rlc_pinning = circuit.pinning(); + // ===== end proving key generation ==== + + let compute_app_snark = + test_compute_app_snark(14, DEFAULT_USER_PARAMS, data_results.clone(), result_len); + let (core_params, input) = + get_test_input(compute_app_snark, 1, data_results, result_len, agg_k); + + let circuit = prepare_prover_circuit(core_params, rlc_pinning, 200, input); + + let snark = gen_snark_shplonk(&kzg_params, &pk, circuit, None::<&str>); + let dk = (kzg_params.get_g()[0], kzg_params.g2(), kzg_params.s_g2()); + verify_snark(&dk.into(), &snark)?; + + Ok(()) +} diff --git a/axiom-query/src/verify_compute/tests/mod.rs b/axiom-query/src/verify_compute/tests/mod.rs new file mode 100644 index 00000000..d9b5cbd3 --- /dev/null +++ b/axiom-query/src/verify_compute/tests/mod.rs @@ -0,0 +1,455 @@ +#![allow(clippy::field_reassign_with_default)] +use std::{collections::HashMap, fs::File, panic::catch_unwind}; + +use axiom_codec::{ + constants::*, + decoder::native::decode_compute_snark, + encoder::native::{get_query_hash_v2, get_query_schema_hash}, + types::{ + field_elements::{FieldSubqueryResult, SUBQUERY_KEY_LEN}, + native::{AxiomV2ComputeQuery, AxiomV2DataQuery}, + }, + utils::native::encode_h256_to_hilo, +}; +use axiom_eth::{ + halo2_base::{ + gates::circuit::builder::BaseCircuitBuilder, + halo2_proofs::dev::MockProver, + utils::fs::{gen_srs, read_params}, + }, + halo2_proofs::poly::{commitment::ParamsProver, kzg::commitment::ParamsKZG}, + halo2curves::bn256::{Bn256, Fr}, + keccak::{ + promise::generate_keccak_shards_from_calls, + types::{ComponentTypeKeccak, OutputKeccakShard}, + }, + utils::{ + build_utils::pinning::RlcCircuitPinning, + component::{ + promise_loader::single::PromiseLoaderParams, ComponentCircuit, + ComponentPromiseResultsInMerkle, ComponentType, + }, + }, + zkevm_hashes::keccak::vanilla::keccak_packed_multi::get_num_keccak_f, +}; + +use ethers_core::{ + types::{Bytes, H256}, + utils::keccak256, +}; +use hex::FromHex; +use itertools::Itertools; +#[cfg(test)] +use test_log::test; + +use crate::{ + components::{ + dummy_rlc_circuit_params, + results::{ + self, + tests::test_capacity, + types::{ + CircuitInputResultsRootShard, CircuitOutputResultsRoot, LogicOutputResultsRoot, + }, + }, + }, + verify_compute::{ + tests::utils::{default_compute_snark, InputVerifyCompute}, + types::{CircuitInputVerifyCompute, LogicalPublicInstanceVerifyCompute}, + utils::{reconstruct_snark_from_compute_query, DEFAULT_CLIENT_METADATA}, + utils::{verify_snark, UserCircuitParams, DEFAULT_USER_PARAMS}, + }, + Field, +}; + +use super::{circuit::ComponentCircuitVerifyCompute, types::CoreParamsVerifyCompute}; + +use utils::get_base_input; + +/// needs to be large enough to fit results and data instances +pub const DUMMY_USER_K: u32 = 14; + +/// Test when computeSnark is aggregation circuit +pub mod aggregation; +/// test prove module +pub mod prove; +/// Testing specific utils +pub mod utils; + +pub fn get_test_result() -> (CircuitInputResultsRootShard, LogicOutputResultsRoot) { + let mut capacity = test_capacity(); + if capacity.total > USER_MAX_OUTPUTS { + capacity.total = USER_MAX_OUTPUTS; + } + let (input, output, _) = results::tests::get_test_input(capacity).unwrap(); + (input, output) +} + +pub fn test_compute_circuit( + k: u32, + user_params: UserCircuitParams, + subquery_results: LogicOutputResultsRoot, + result_len: usize, +) -> BaseCircuitBuilder { + let circuit_params = user_params.base_circuit_params(k as usize); + let mut builder = BaseCircuitBuilder::new(false).use_params(circuit_params); + // let range = builder.range_chip(); + + let ctx = builder.main(0); + + let mut compute_results = vec![]; + let mut data_instances = vec![]; + for result in subquery_results.results.into_iter().take(subquery_results.num_subqueries) { + let result = FieldSubqueryResult::::try_from(result).unwrap(); + let data_instance = ctx.assign_witnesses(result.to_fixed_array()); + compute_results.extend(data_instance[SUBQUERY_KEY_LEN..][..2].to_vec()); + data_instances.extend(data_instance); + } + assert!(compute_results.len() >= 2 * result_len); + compute_results.truncate(2 * result_len); + compute_results.resize_with(2 * USER_MAX_OUTPUTS, || ctx.load_witness(F::ZERO)); + + let mut assigned_instance = compute_results; + assigned_instance.extend(data_instances); + assigned_instance + .resize_with(DEFAULT_USER_PARAMS.num_instances(), || ctx.load_witness(F::ZERO)); + builder.assigned_instances[0] = assigned_instance; + + builder +} + +fn prepare_mock_circuit( + core_params: CoreParamsVerifyCompute, + k: usize, + keccak_f_capacity: usize, + input: CircuitInputVerifyCompute, +) -> ComponentCircuitVerifyCompute { + let mut rlc_params = dummy_rlc_circuit_params(k); + rlc_params.base.lookup_bits = Some(k - 1); + let loader_params = PromiseLoaderParams::new_for_one_shard(keccak_f_capacity); + let mut circuit = ComponentCircuitVerifyCompute::new(core_params, loader_params, rlc_params); + circuit.feed_input(Box::new(input)).unwrap(); + circuit.calculate_params(); + let promise_results = HashMap::from_iter([( + ComponentTypeKeccak::::get_type_id(), + ComponentPromiseResultsInMerkle::from_single_shard( + generate_keccak_shards_from_calls(&circuit, keccak_f_capacity) + .unwrap() + .into_logical_results(), + ), + )]); + circuit.fulfill_promise_results(&promise_results).unwrap(); + circuit +} + +fn prepare_prover_circuit( + core_params: CoreParamsVerifyCompute, + rlc_pinning: RlcCircuitPinning, + keccak_f_capacity: usize, + input: CircuitInputVerifyCompute, +) -> ComponentCircuitVerifyCompute { + let loader_params = PromiseLoaderParams::new_for_one_shard(keccak_f_capacity); + let circuit = ComponentCircuitVerifyCompute::prover(core_params, loader_params, rlc_pinning); + circuit.feed_input(Box::new(input)).unwrap(); + let promise_results = HashMap::from_iter([( + ComponentTypeKeccak::::get_type_id(), + ComponentPromiseResultsInMerkle::from_single_shard( + generate_keccak_shards_from_calls(&circuit, keccak_f_capacity) + .unwrap() + .into_logical_results(), + ), + )]); + circuit.fulfill_promise_results(&promise_results).unwrap(); + circuit +} + +#[test] +fn test_verify_no_compute_mock() { + let (_, subquery_results) = get_test_result::(); + let result_len = 2; // test different from numSubqueries + let num_subqueries = subquery_results.num_subqueries; + + let source_chain_id = 1; + let empty_cq = AxiomV2ComputeQuery { + k: 0, + result_len: result_len as u16, + vkey: vec![], + compute_proof: Bytes::from([]), + }; + let logic_input = + InputVerifyCompute { source_chain_id, subquery_results, compute_query: empty_cq.clone() }; + + let circuit_k = 19u32; + + let (core_params, input) = + CircuitInputVerifyCompute::reconstruct(logic_input.clone(), &gen_srs(DUMMY_USER_K)) + .unwrap(); + let circuit = prepare_mock_circuit(core_params, circuit_k as usize, 200, input); + let instances = circuit.get_public_instances(); + + let subqueries = logic_input.subquery_results.results[..num_subqueries] + .iter() + .map(|r| r.subquery.clone()) + .collect(); + let data_query = AxiomV2DataQuery { source_chain_id, subqueries }; + + // check query hash and query schema calculation + let logic_pis = instances.other.clone(); + let LogicalPublicInstanceVerifyCompute { + query_hash, query_schema, compute_results_hash, .. + } = logic_pis.try_into().unwrap(); + assert_eq!(&query_schema, &encode_h256_to_hilo(&H256::zero())); + let native_query_hash = get_query_hash_v2(source_chain_id, &data_query, &empty_cq).unwrap(); + assert_eq!(&query_hash, &encode_h256_to_hilo(&native_query_hash)); + let encode_results = logic_input.subquery_results.results[..result_len] + .iter() + .map(|r| r.value.to_vec()) + .concat(); + let native_results_hash = H256(keccak256(encode_results)); + assert_eq!(&compute_results_hash, &encode_h256_to_hilo(&native_results_hash)); + MockProver::run(circuit_k, &circuit, vec![instances.into()]).unwrap().assert_satisfied(); +} + +#[test] +fn test_verify_compute_mock() -> anyhow::Result<()> { + let (_input_results, data_results) = get_test_result::(); + let result_len = data_results.num_subqueries; + + let app_k = 14; + let app_params = gen_srs(app_k); + let logic_input = get_base_input( + &app_params, + USER_MAX_OUTPUTS, + test_compute_circuit(app_k, DEFAULT_USER_PARAMS, data_results.clone(), result_len), + data_results, + 1, + result_len, + )?; + // serde_json::to_writer(File::create("data/test/input_results_root.json")?, &input_results)?; + serde_json::to_writer(File::create("data/test/input_verify_compute.json")?, &logic_input)?; + + let circuit_k = 19u32; + + let (core_params, input) = + CircuitInputVerifyCompute::reconstruct(logic_input.clone(), &app_params)?; + let circuit = prepare_mock_circuit(core_params, circuit_k as usize, 200, input); + let instances = circuit.get_public_instances(); + + let source_chain_id = logic_input.source_chain_id; + let num_subqueries = logic_input.subquery_results.num_subqueries; + let subqueries = logic_input.subquery_results.results[..num_subqueries] + .iter() + .map(|r| r.subquery.clone()) + .collect(); + let data_query = AxiomV2DataQuery { source_chain_id, subqueries }; + let compute_query = logic_input.compute_query; + /*dbg!(data_query.keccak()); + { + let vkey = compute_query.vkey.chunks(32).map(H256::from_slice).collect_vec(); + dbg!(vkey); + } + */ + // dbg!(compute_query.compute_proof.len()); + // dbg!(&Bytes::from(compute_query.encode().unwrap())); + // dbg!(compute_query.keccak()); + + // check query hash and query schema calculation + let logic_pis = instances.other.clone(); + let LogicalPublicInstanceVerifyCompute { + query_hash, query_schema, compute_results_hash, .. + } = logic_pis.try_into()?; + let native_query_schema = + get_query_schema_hash(compute_query.k, result_len as u16, &compute_query.vkey)?; + assert_eq!(&query_schema, &encode_h256_to_hilo(&native_query_schema)); + let native_query_hash = get_query_hash_v2(source_chain_id, &data_query, &compute_query)?; + // dbg!(native_query_hash); + assert_eq!(&query_hash, &encode_h256_to_hilo(&native_query_hash)); + let compute_snark = + decode_compute_snark(&mut &compute_query.compute_proof[..], result_len as u16, false)?; + let encode_results = compute_snark.compute_results.iter().map(|r| r.0.to_vec()).concat(); + let native_results_hash = H256(keccak256(encode_results)); + assert_eq!(&compute_results_hash, &encode_h256_to_hilo(&native_results_hash)); + MockProver::run(circuit_k, &circuit, vec![instances.into()]).unwrap().assert_satisfied(); + Ok(()) +} + +#[test] +fn test_verify_compute_prover_full() -> anyhow::Result<()> { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + let (_, data_results) = get_test_result::(); + let result_len = data_results.num_subqueries; + let max_num_subqueries = data_results.results.len(); + + let app_k = 14; + let app_params = gen_srs(app_k); + let logic_input = get_base_input( + &app_params, + USER_MAX_OUTPUTS, + test_compute_circuit(app_k, DEFAULT_USER_PARAMS, data_results.clone(), result_len), + data_results, + 1, + result_len, + )?; + let mut f = File::create(format!("{cargo_manifest_dir}/data/test/input_verify_compute.json",))?; + serde_json::to_writer(&mut f, &logic_input)?; + let res = catch_unwind(|| { + prove::verify_compute_prover( + logic_input.clone(), + max_num_subqueries, + "verify_compute", + None, + 200, + ) + .unwrap() + }); + std::fs::remove_file(format!("{cargo_manifest_dir}/data/test/verify_compute.pk")).ok(); + std::fs::remove_file(format!("{cargo_manifest_dir}/data/test/verify_compute.snark")).ok(); + let (snark, _, _) = res.unwrap(); + let dk = (app_params.get_g()[0], app_params.g2(), app_params.s_g2()); + verify_snark(&dk.into(), &snark)?; + + Ok(()) +} + +#[test] +#[ignore = "integration test"] +fn test_verify_compute_prepare_for_agg() -> anyhow::Result<()> { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + let data_results: LogicOutputResultsRoot = serde_json::from_reader(File::open(format!( + "{cargo_manifest_dir}/data/test/output_result_root_for_agg.json", + ))?)?; + let result_len = 2; + + let app_k = 14; + let app_params = read_params(app_k); + let logic_input = get_base_input( + &app_params, + USER_MAX_OUTPUTS, + test_compute_circuit(app_k, DEFAULT_USER_PARAMS, data_results.clone(), result_len), + data_results, + 1, + result_len, + )?; + serde_json::to_writer( + File::create(format!("{cargo_manifest_dir}/data/test/input_verify_compute_for_agg.json"))?, + &logic_input, + )?; + + let circuit_k = 19u32; + let (core_params, input) = + CircuitInputVerifyCompute::reconstruct(logic_input.clone(), &app_params)?; + let keccak_cap = 200; + let circuit = prepare_mock_circuit(core_params, circuit_k as usize, keccak_cap, input); + let keccak_shard = generate_keccak_shards_from_calls(&circuit, keccak_cap)?; + serde_json::to_writer( + File::create(format!( + "{cargo_manifest_dir}/data/test/verify_compute_promise_results_keccak_for_agg.json" + ))?, + &keccak_shard, + )?; + Ok(()) +} + +#[test] +#[ignore = "integration test"] +fn test_merge_keccak_shards_for_agg() -> anyhow::Result<()> { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + let header_keccak: OutputKeccakShard = serde_json::from_reader(File::open(format!( + "{cargo_manifest_dir}/data/test/header_promise_results_keccak_for_agg.json" + ))?)?; + let results_root_keccak: OutputKeccakShard = serde_json::from_reader(File::open(format!( + "{cargo_manifest_dir}/data/test/results_root_promise_results_keccak_for_agg.json" + ))?)?; + let verify_compute_keccak: OutputKeccakShard = serde_json::from_reader(File::open(format!( + "{cargo_manifest_dir}/data/test/verify_compute_promise_results_keccak_for_agg.json" + ))?)?; + let responses = std::iter::empty() + .chain(header_keccak.responses) + .chain(results_root_keccak.responses) + .chain(verify_compute_keccak.responses) + .collect::>(); + let mut used_cap = 0; + for r in &responses { + used_cap += get_num_keccak_f(r.0.len()); + } + assert!(used_cap <= 200); + let merged = OutputKeccakShard { responses, capacity: 200 }; + serde_json::to_writer( + File::create(format!( + "{cargo_manifest_dir}/data/test/promise_results_keccak_for_agg.json" + ))?, + &merged, + )?; + Ok(()) +} + +#[test] +#[ignore = "integration test"] +fn test_verify_compute_prover_for_agg() -> anyhow::Result<()> { + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + let logic_input: InputVerifyCompute = serde_json::from_reader(File::open(format!( + "{cargo_manifest_dir}/data/test/input_verify_compute_for_agg.json" + ))?)?; + let promise_keccak: OutputKeccakShard = serde_json::from_reader(File::open(format!( + "{cargo_manifest_dir}/data/test/promise_results_keccak_for_agg.json" + ))?)?; + let keccak_cap = promise_keccak.capacity; + prove::verify_compute_prover( + logic_input.clone(), + logic_input.subquery_results.results.len(), + "verify_compute_for_agg", + Some(promise_keccak), + keccak_cap, + )?; + Ok(()) +} + +#[test] +fn test_circuit_metadata_encode() { + assert_eq!( + DEFAULT_CLIENT_METADATA.encode().unwrap().as_bytes(), + &Vec::from_hex("0001000009000100000004010000010080000000000000000000000000000000").unwrap() + ); +} + +impl CircuitInputVerifyCompute { + /// **Assumptions:** + /// - The generator `params_for_dummy.get_g()[0]` should match that of the trusted setup used to generate `input.compute_query` if there is a compute query. + /// - If there is no compute query (so compute_query.k == 0), then a dummy compute snark is generated using `params_for_dummy` with [DEFAULT_CLIENT_METADATA]. + pub fn reconstruct( + input: InputVerifyCompute, + params_for_dummy: &ParamsKZG, + ) -> anyhow::Result<(CoreParamsVerifyCompute, Self)> { + let InputVerifyCompute { source_chain_id, subquery_results, compute_query } = input; + + let compute_query_result_len = compute_query.result_len; + let nonempty_compute_query = compute_query.k != 0; + let (compute_snark, client_metadata) = if compute_query.k == 0 { + (default_compute_snark(params_for_dummy), DEFAULT_CLIENT_METADATA.clone()) + } else { + reconstruct_snark_from_compute_query(subquery_results.clone(), compute_query)? + }; + let subquery_results = CircuitOutputResultsRoot::try_from(subquery_results)?; + let circuit_params = CoreParamsVerifyCompute::new( + subquery_results.results.len(), + params_for_dummy.get_g()[0], + client_metadata, + compute_snark.protocol.preprocessed.len(), + ); + println!( + "compute_snark.protocol.preprocessed.len(): {}", + circuit_params.preprocessed_len() + ); + + Ok(( + circuit_params, + Self::new( + source_chain_id, + subquery_results, + nonempty_compute_query, + compute_query_result_len, + compute_snark, + ), + )) + } +} diff --git a/axiom-query/src/verify_compute/tests/prove.rs b/axiom-query/src/verify_compute/tests/prove.rs new file mode 100644 index 00000000..ebc323ad --- /dev/null +++ b/axiom-query/src/verify_compute/tests/prove.rs @@ -0,0 +1,112 @@ +use std::{collections::HashMap, fs}; + +use axiom_codec::constants::USER_MAX_OUTPUTS; +use axiom_eth::{ + halo2_base::utils::fs::gen_srs, + keccak::{ + promise::generate_keccak_shards_from_calls, + types::{ComponentTypeKeccak, OutputKeccakShard}, + }, + snark_verifier_sdk::{halo2::gen_snark_shplonk, Snark}, + utils::{ + build_utils::pinning::{PinnableCircuit, RlcCircuitPinning}, + component::{ + promise_loader::single::PromiseLoaderParams, ComponentCircuit, + ComponentPromiseResultsInMerkle, ComponentType, + }, + }, +}; + +use crate::{ + components::results::types::LogicOutputResultsRoot, + verify_compute::{ + circuit::ComponentCircuitVerifyCompute, + tests::{prepare_mock_circuit, utils::get_base_input, DUMMY_USER_K}, + utils::default_compute_circuit, + }, +}; + +use super::{super::types::CircuitInputVerifyCompute, InputVerifyCompute}; + +pub fn verify_compute_prover( + mut logic_input: InputVerifyCompute, + max_num_subqueries: usize, + name: &str, + promise_keccak: Option, + keccak_capacity: usize, +) -> anyhow::Result<(Snark, RlcCircuitPinning, OutputKeccakShard)> { + type I = CircuitInputVerifyCompute; + + let default_params = gen_srs(DUMMY_USER_K); + let output_results = LogicOutputResultsRoot { + results: vec![Default::default(); max_num_subqueries], + subquery_hashes: vec![Default::default(); max_num_subqueries], + num_subqueries: 0, + }; + let input = get_base_input( + &default_params, + USER_MAX_OUTPUTS, + default_compute_circuit(14), + output_results, + 0, + 0, + )?; + let (core_params, input) = I::reconstruct(input, &default_params)?; + + let circuit_k = 19u32; + + let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR"); + fs::create_dir_all(format!("{cargo_manifest_dir}/configs/test")).unwrap(); + fs::create_dir_all(format!("{cargo_manifest_dir}/data/test")).unwrap(); + let pinning_path = format!("{cargo_manifest_dir}/configs/test/{name}.json"); + let pk_path = format!("{cargo_manifest_dir}/data/test/{name}.pk"); + let params = gen_srs(circuit_k); + + let circuit = + prepare_mock_circuit(core_params.clone(), circuit_k as usize, keccak_capacity, input); + let (pk, pinning) = circuit.create_pk(¶ms, pk_path, pinning_path)?; + + let loader_params = PromiseLoaderParams::new_for_one_shard(keccak_capacity); + #[cfg(all(feature = "keygen", not(debug_assertions)))] + { + use crate::keygen::shard::CircuitIntentVerifyCompute; + use axiom_eth::halo2_base::utils::halo2::KeygenCircuitIntent; + use axiom_eth::halo2_proofs::{plonk::keygen_vk, SerdeFormat}; + // check keygen + let intent = CircuitIntentVerifyCompute { + core_params, + loader_params: loader_params.clone(), + k: circuit_k, + lookup_bits: circuit_k as usize - 1, + }; + let circuit = intent.build_keygen_circuit(); + let vk = keygen_vk(¶ms, &circuit)?; + if pk.get_vk().to_bytes(SerdeFormat::RawBytes) != vk.to_bytes(SerdeFormat::RawBytes) { + panic!("vk mismatch"); + } + } + + let first = logic_input.subquery_results.results[0].clone(); + logic_input.subquery_results.results.resize(max_num_subqueries, first); + let first = logic_input.subquery_results.subquery_hashes[0]; + logic_input.subquery_results.subquery_hashes.resize(max_num_subqueries, first); + let (core_params, input) = I::reconstruct(logic_input, &default_params)?; + let circuit = + ComponentCircuitVerifyCompute::prover(core_params, loader_params, pinning.clone()); + circuit.feed_input(Box::new(input)).unwrap(); + if let Some(promise_keccak) = &promise_keccak { + assert_eq!(promise_keccak.capacity, keccak_capacity); + } + let promise_keccak = promise_keccak + .unwrap_or_else(|| generate_keccak_shards_from_calls(&circuit, keccak_capacity).unwrap()); + let promise_results = HashMap::from_iter([( + ComponentTypeKeccak::::get_type_id(), + ComponentPromiseResultsInMerkle::from_single_shard( + promise_keccak.clone().into_logical_results(), + ), + )]); + circuit.fulfill_promise_results(&promise_results).unwrap(); + + let snark_path = format!("{cargo_manifest_dir}/data/test/{name}.snark"); + Ok((gen_snark_shplonk(¶ms, &pk, circuit, Some(snark_path)), pinning, promise_keccak)) +} diff --git a/axiom-query/src/verify_compute/tests/utils.rs b/axiom-query/src/verify_compute/tests/utils.rs new file mode 100644 index 00000000..1e05e513 --- /dev/null +++ b/axiom-query/src/verify_compute/tests/utils.rs @@ -0,0 +1,138 @@ +use std::path::{Path, PathBuf}; + +use axiom_codec::{ + constants::USER_INSTANCE_COLS, + types::native::{AxiomV2ComputeQuery, AxiomV2ComputeSnark}, + utils::native::decode_hilo_to_h256, + HiLo, +}; +use axiom_eth::{ + halo2_base::{ + gates::circuit::builder::BaseCircuitBuilder, + halo2_proofs::{ + halo2curves::bn256::{Bn256, Fr}, + poly::{commitment::Params, kzg::commitment::ParamsKZG}, + }, + }, + halo2_proofs::{plonk::Circuit, poly::commitment::ParamsProver}, + rlc::circuit::RlcCircuitParams, + snark_verifier::pcs::kzg::KzgDecidingKey, + snark_verifier_sdk::{ + gen_pk, + halo2::{gen_snark_shplonk, read_snark}, + Snark, + }, +}; +use ethers_core::types::Bytes; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::{ + components::results::types::LogicOutputResultsRoot, + verify_compute::utils::{ + dummy_compute_circuit, get_metadata_from_protocol, get_onchain_vk_from_vk, + write_onchain_vkey, UserCircuitParams, DEFAULT_USER_PARAMS, + }, +}; + +// For now we assume LogicOutputResultsRoot knows the true number of ordered subqueries +// This might change if we have multiple pages of LogicOutputResultsRoot + +#[derive(Clone, Debug, Serialize, Deserialize, Hash)] +#[serde(rename_all = "camelCase")] +pub struct InputVerifyCompute { + pub source_chain_id: u64, + pub subquery_results: LogicOutputResultsRoot, + pub compute_query: AxiomV2ComputeQuery, +} + +// We do not directly convert to CircuitInputVerifyCompute because we need +// to test the reconstruction from AxiomV2ComputeQuery back into CircuitInputVerifyCompute + +/// Prepares input for `client_circuit` that is created with [BaseCircuitBuilder]. +/// `client_circuit` is [BaseCircuitBuilder] populated with witnesses and fixed/copy constraints. +pub fn get_base_input( + params: &ParamsKZG, + max_outputs: usize, + client_circuit: BaseCircuitBuilder, + subquery_results: LogicOutputResultsRoot, + source_chain_id: u64, + result_len: usize, +) -> anyhow::Result { + assert!(!client_circuit.witness_gen_only()); + let client_circuit_params = client_circuit.params(); + let pk = gen_pk(params, &client_circuit, None); + let compute_snark = gen_snark_shplonk(params, &pk, client_circuit, None::<&str>); + + let client_metadata = get_metadata_from_protocol( + &compute_snark.protocol, + RlcCircuitParams { base: client_circuit_params, num_rlc_columns: 0 }, + max_outputs, + )?; + + let onchain_vk = get_onchain_vk_from_vk(pk.get_vk(), client_metadata); + let vkey = write_onchain_vkey(&onchain_vk)?; + + let instances = &compute_snark.instances; + assert_eq!(instances.len(), USER_INSTANCE_COLS); + let instances = &instances[0]; + let compute_results = instances + .iter() + .chunks(2) + .into_iter() + .take(result_len) + .map(|hilo| { + let hilo = hilo.collect_vec(); + assert_eq!(hilo.len(), 2); + decode_hilo_to_h256(HiLo::from_hi_lo([*hilo[0], *hilo[1]])) + }) + .collect(); + let compute_snark = AxiomV2ComputeSnark { + kzg_accumulator: None, + compute_results, + proof_transcript: compute_snark.proof, + }; + let compute_proof = Bytes::from(compute_snark.encode().unwrap()); + let compute_query = AxiomV2ComputeQuery { + k: params.k() as u8, + result_len: result_len as u16, + vkey, + compute_proof, + }; + Ok(InputVerifyCompute { source_chain_id, subquery_results, compute_query }) +} + +/// Create a dummy snark that **will verify** successfully. +pub fn dummy_compute_snark( + kzg_params: &ParamsKZG, + user_params: UserCircuitParams, + cache_dir: impl AsRef, +) -> Snark { + // tag for caching the dummy + let tag = { + // UserCircuitParams and KzgDecidingKey are enough to tag the dummy snark; we don't need `k` + let mut hasher = blake3::Hasher::new(); + hasher.update(&serde_json::to_vec(&user_params).unwrap()); + // hash num instance in case we change the format + hasher.update(&user_params.num_instances().to_be_bytes()); + let dk: KzgDecidingKey = + (kzg_params.get_g()[0], kzg_params.g2(), kzg_params.s_g2()).into(); + hasher.update(&serde_json::to_vec(&dk).unwrap()); + let id = hasher.finalize(); + cache_dir.as_ref().join(format!("{id}.snark")) + }; + if let Ok(snark) = read_snark(&tag) { + return snark; + } + let circuit = dummy_compute_circuit(user_params, kzg_params.k()); + let pk = gen_pk(kzg_params, &circuit, None); + gen_snark_shplonk(kzg_params, &pk, circuit, Some(tag)) +} + +pub fn default_compute_snark(params: &ParamsKZG) -> Snark { + let mut cache_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + cache_dir.push("data"); + cache_dir.push("default_compute_snark"); + std::fs::create_dir_all(&cache_dir).unwrap(); + dummy_compute_snark(params, DEFAULT_USER_PARAMS, &cache_dir) +} diff --git a/axiom-query/src/verify_compute/types.rs b/axiom-query/src/verify_compute/types.rs new file mode 100644 index 00000000..2bf6bf4f --- /dev/null +++ b/axiom-query/src/verify_compute/types.rs @@ -0,0 +1,256 @@ +use std::iter; + +use anyhow::bail; +use axiom_codec::{types::field_elements::FlattenedSubqueryResult, HiLo}; +use axiom_eth::{ + halo2_base::AssignedValue, + halo2curves::bn256::{Fr, G1Affine}, + impl_flatten_conversion, + snark_verifier_sdk::{halo2::gen_dummy_snark_from_protocol, Snark, SHPLONK}, + utils::{ + build_utils::dummy::DummyFrom, + component::{ + circuit::{CoreBuilderOutputParams, CoreBuilderParams}, + types::LogicalEmpty, + ComponentType, ComponentTypeId, LogicalResult, + }, + snark_verifier::NUM_FE_ACCUMULATOR, + }, +}; +use getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; + +use crate::{ + components::results::{table::SubqueryResultsTable, types::CircuitOutputResultsRoot}, + utils::client_circuit::{metadata::AxiomV2CircuitMetadata, vkey::OnchainVerifyingKey}, +}; + +/// Identifier for the component type of Verify Compute Circuit +pub struct ComponentTypeVerifyCompute; + +/// Configuration parameters for Verify Compute Circuit that determine +/// the circuit, **independent** of the variable inputs. +/// +/// Even when `nonempty_compute_query == false` (no compute query), +/// the `circuit_params.client_metadata` needs to be set to a valid +/// client circuit configuration. +#[derive(Clone, Debug, Default, Serialize, Deserialize, Getters, CopyGetters)] +pub struct CoreParamsVerifyCompute { + /// Capacity: max number of subquery results + #[getset(get_copy = "pub")] + subquery_results_capacity: usize, + /// Succinct verifying key should be the generator `g()[0]` of the KZG trusted setup used to generate the vkey. + #[getset(get_copy = "pub")] + svk: G1Affine, // Svk type doesn't derive Serialize + /// Client circuit on-chain vkey + #[getset(get = "pub")] + client_metadata: AxiomV2CircuitMetadata, + /// Length of `preprocessed` in `PlonkProtocol` + #[getset(get_copy = "pub")] + preprocessed_len: usize, +} + +impl CoreParamsVerifyCompute { + pub fn new( + subquery_results_capacity: usize, + svk: G1Affine, + client_metadata: AxiomV2CircuitMetadata, + preprocessed_len: usize, + ) -> Self { + Self { subquery_results_capacity, svk, client_metadata, preprocessed_len } + } +} +impl CoreBuilderParams for CoreParamsVerifyCompute { + /// No component output + fn get_output_params(&self) -> CoreBuilderOutputParams { + CoreBuilderOutputParams::new(vec![]) + } +} + +/// Logic inputs to Verify Compute Circuit +/// Deserialization is specialized to [Fr] for now. +/// +/// ## Compute Snark +/// The Verify Compute Circuit should only depend on the number of columns and custom gates / lookup arguments +/// of `compute_snark`, not on the fixed commitments or domain size `2^k`. +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct CircuitInputVerifyCompute { + /// Chain ID of the chain the EVM data is from + pub source_chain_id: u64, + /// Used for lookups, length may be padded with dummy subqueries + pub subquery_results: CircuitOutputResultsRoot, + /// If `nonempty_compute_query == false`, then `compute_snark` is a dummy snark. + pub nonempty_compute_query: bool, + /// The number of user results + pub result_len: u16, + /// The client snark. + /// + /// When there is no compute query (`nonempty_compute_query == false`), + /// this must be a dummy snark matching `circuit_params.client_metadata` that will still verify. + #[getset(get = "pub")] + pub(super) compute_snark: Snark, +} + +impl CircuitInputVerifyCompute { + /// If `nonempty_compute_query == false`, then `compute_snark` must be a dummy snark that will verify. + pub fn new( + source_chain_id: u64, + subquery_results: CircuitOutputResultsRoot, + nonempty_compute_query: bool, + result_len: u16, + compute_snark: Snark, + ) -> Self { + Self { + source_chain_id, + subquery_results, + nonempty_compute_query, + result_len, + compute_snark, + } + } +} + +impl DummyFrom for CircuitInputVerifyCompute { + fn dummy_from(core_params: CoreParamsVerifyCompute) -> Self { + let subquery_results_capacity = core_params.subquery_results_capacity(); + let onchain_vk = OnchainVerifyingKey { + circuit_metadata: core_params.client_metadata().clone(), + transcript_initial_state: Default::default(), + preprocessed: vec![G1Affine::default(); core_params.preprocessed_len()], + }; + // k is loaded as witness so it shouldn't matter + let k = 7; + let protocol = onchain_vk.into_plonk_protocol(k).unwrap(); + let compute_snark = gen_dummy_snark_from_protocol::(protocol); + let results = SubqueryResultsTable { + rows: vec![FlattenedSubqueryResult::default(); subquery_results_capacity], + }; + let subquery_hashes = vec![HiLo::default(); subquery_results_capacity]; + + let subquery_results = + CircuitOutputResultsRoot { results, subquery_hashes, num_subqueries: 0 }; + Self::new(0, subquery_results, true, 0, compute_snark) + } +} + +pub(super) const NUM_LOGICAL_INSTANCE_WITHOUT_ACC: usize = 1 + 2 + 2 + 2 + 1 + 1; +pub(super) const NUM_LOGICAL_INSTANCE: usize = + NUM_FE_ACCUMULATOR + NUM_LOGICAL_INSTANCE_WITHOUT_ACC; +const NUM_BITS_PER_FE: [usize; NUM_LOGICAL_INSTANCE] = get_num_bits_per_fe(); +// 9999 means that the public instance takes a whole witness +// Accumulators *must* take whole witnesses. +const fn get_num_bits_per_fe() -> [usize; NUM_LOGICAL_INSTANCE] { + let mut bits_per = [9999; NUM_LOGICAL_INSTANCE]; + bits_per[NUM_FE_ACCUMULATOR] = 64; + bits_per[NUM_FE_ACCUMULATOR + 1] = 128; + bits_per[NUM_FE_ACCUMULATOR + 2] = 128; + bits_per[NUM_FE_ACCUMULATOR + 3] = 128; + bits_per[NUM_FE_ACCUMULATOR + 4] = 128; + bits_per[NUM_FE_ACCUMULATOR + 5] = 128; + bits_per[NUM_FE_ACCUMULATOR + 6] = 128; + bits_per +} +/// The public instances of the circuit, **excluding** the component owned instances +/// for output commit and promise commit. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LogicalPublicInstanceVerifyCompute { + pub accumulator: Vec, + pub source_chain_id: T, + pub compute_results_hash: HiLo, + pub query_hash: HiLo, + pub query_schema: HiLo, + pub results_root_poseidon: T, + pub promise_subquery_hashes: T, +} +/// [LogicalPublicInstanceVerifyCompute] with `accumulator` removed. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LogicalPisVerifyComputeWithoutAccumulator { + pub source_chain_id: T, + pub compute_results_hash: HiLo, + pub query_hash: HiLo, + pub query_schema: HiLo, + pub results_root_poseidon: T, + pub promise_subquery_hashes: T, +} + +type F = Fr; +/// Verify Compute has no virtual table as output +impl ComponentType for ComponentTypeVerifyCompute { + type InputValue = LogicalEmpty; + type InputWitness = LogicalEmpty>; + type OutputValue = LogicalEmpty; + type OutputWitness = LogicalEmpty>; + type LogicalInput = LogicalEmpty; + + fn get_type_id() -> ComponentTypeId { + "axiom-query:ComponentTypeVerifyCompute".to_string() + } + fn logical_result_to_virtual_rows_impl( + _ins: &LogicalResult, + ) -> Vec<(Self::InputValue, Self::OutputValue)> { + unreachable!() + } + fn logical_input_to_virtual_rows_impl(_li: &Self::LogicalInput) -> Vec { + unreachable!() + } +} + +// ============== LogicalPublicInstanceVerifyCompute ============== +impl LogicalPublicInstanceVerifyCompute { + pub fn flatten(self) -> Vec { + iter::empty() + .chain(self.accumulator) + .chain(Some(self.source_chain_id)) + .chain(self.compute_results_hash.hi_lo()) + .chain(self.query_hash.hi_lo()) + .chain(self.query_schema.hi_lo()) + .chain([self.results_root_poseidon, self.promise_subquery_hashes]) + .collect() + } +} + +impl TryFrom> for LogicalPublicInstanceVerifyCompute { + type Error = anyhow::Error; + + fn try_from(mut value: Vec) -> anyhow::Result { + if value.len() != NUM_LOGICAL_INSTANCE { + bail!("wrong number of logical public instances") + }; + let accumulator = value.drain(..NUM_FE_ACCUMULATOR).collect(); + let drained: LogicalPisVerifyComputeWithoutAccumulator = value.try_into().unwrap(); + Ok(Self { + accumulator, + source_chain_id: drained.source_chain_id, + compute_results_hash: drained.compute_results_hash, + query_hash: drained.query_hash, + query_schema: drained.query_schema, + results_root_poseidon: drained.results_root_poseidon, + promise_subquery_hashes: drained.promise_subquery_hashes, + }) + } +} +impl TryFrom> for LogicalPisVerifyComputeWithoutAccumulator { + type Error = anyhow::Error; + + fn try_from(value: Vec) -> anyhow::Result { + if value.len() != NUM_LOGICAL_INSTANCE_WITHOUT_ACC { + bail!("wrong number of logical public instances without accumulator") + }; + let source_chain_id = value[0]; + let compute_results_hash = HiLo::from_hi_lo([value[1], value[2]]); + let query_hash = HiLo::from_hi_lo([value[3], value[4]]); + let query_schema = HiLo::from_hi_lo([value[5], value[6]]); + let results_root_poseidon = value[7]; + let promise_subquery_hashes = value[8]; + Ok(Self { + source_chain_id, + compute_results_hash, + query_hash, + query_schema, + results_root_poseidon, + promise_subquery_hashes, + }) + } +} + +impl_flatten_conversion!(LogicalPublicInstanceVerifyCompute, NUM_BITS_PER_FE); diff --git a/axiom-query/src/verify_compute/utils.rs b/axiom-query/src/verify_compute/utils.rs new file mode 100644 index 00000000..f1fcc67c --- /dev/null +++ b/axiom-query/src/verify_compute/utils.rs @@ -0,0 +1,333 @@ +use std::{hash::Hash, io::Write}; + +use anyhow::{anyhow, bail}; +use axiom_codec::{ + constants::{ + USER_ADVICE_COLS, USER_FIXED_COLS, USER_INSTANCE_COLS, USER_LOOKUP_ADVICE_COLS, + USER_MAX_OUTPUTS, USER_MAX_SUBQUERIES, USER_RESULT_FIELD_ELEMENTS, + }, + decoder::native::decode_compute_snark, + types::{ + field_elements::SUBQUERY_RESULT_LEN, + native::{AxiomV2ComputeQuery, AxiomV2ComputeSnark}, + }, + utils::writer::{write_curve_compressed, write_field_le}, + HiLo, +}; +use axiom_eth::{ + halo2_base::{ + gates::circuit::{builder::BaseCircuitBuilder, BaseCircuitParams}, + halo2_proofs::{ + halo2curves::{ + bn256::{Bn256, Fr}, + ff::PrimeField, + serde::SerdeObject, + CurveAffine, + }, + plonk::VerifyingKey, + }, + utils::ScalarField, + }, + halo2curves::{bn256::G1Affine, ff::Field as _}, + rlc::circuit::RlcCircuitParams, + snark_verifier::{ + pcs::kzg::KzgDecidingKey, + system::halo2::transcript_initial_state, + util::arithmetic::fe_to_limbs, + verifier::{ + plonk::{PlonkProof, PlonkProtocol}, + SnarkVerifier, + }, + }, + snark_verifier_sdk::{ + halo2::{aggregation::AggregationCircuit, PoseidonTranscript, POSEIDON_SPEC}, + CircuitExt, NativeLoader, PlonkVerifier, Snark, BITS, LIMBS, SHPLONK, + }, +}; +use ethers_core::types::H256; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::{ + components::results::types::{CircuitOutputResultsRoot, LogicOutputResultsRoot}, + utils::client_circuit::{metadata::AxiomV2CircuitMetadata, vkey::OnchainVerifyingKey}, + Field, +}; + +/// Need to provide RlcCircuitParams for additional context, otherwise you have +/// to parse the RlcCircuitParams data from the custom gate information in `protocol` +pub fn get_metadata_from_protocol( + protocol: &PlonkProtocol, + rlc_params: RlcCircuitParams, + max_outputs: usize, +) -> anyhow::Result { + let num_advice_per_phase = + rlc_params.base.num_advice_per_phase.iter().map(|x| *x as u16).collect(); + let num_lookup_advice_per_phase = + rlc_params.base.num_lookup_advice_per_phase.iter().map(|x| *x as u8).collect(); + let num_rlc_columns = rlc_params.num_rlc_columns as u16; + let num_fixed = rlc_params.base.num_fixed as u8; + let mut metadata = AxiomV2CircuitMetadata { + version: 0, + num_advice_per_phase, + num_lookup_advice_per_phase, + num_rlc_columns, + num_fixed, + max_outputs: max_outputs as u16, + ..Default::default() + }; + + if protocol.num_instance.len() != 1 { + bail!("Only one instance column supported right now"); + } + metadata.num_instance = protocol.num_instance.iter().map(|&x| x as u32).collect(); + let mut num_challenge_incl_system = protocol.num_challenge.clone(); + // This `num_challenge` counts only the challenges used inside the circuit - it excludes challenges that are part of the halo2 system. + // The full challenges, which is what `plonk_protocol.num_challenge` stores, is: + // ```ignore + // [ + // my_phase0_challenges, + // ... + // [..my_phasen_challenges, theta], + // [beta, gamma], + // [alpha], + // ] + // ``` + if num_challenge_incl_system.pop() != Some(1) { + bail!("last challenge must be [alpha]"); + } + if num_challenge_incl_system.pop() != Some(2) { + bail!("second last challenge must be [beta, gamma]"); + } + let last_challenge = num_challenge_incl_system.last_mut(); + if last_challenge.is_none() { + bail!("num_challenge must have at least 3 challenges"); + } + let last_challenge = last_challenge.unwrap(); + if *last_challenge == 0 { + bail!("third last challenge must include theta"); + } + *last_challenge -= 1; + let num_challenge: Vec = num_challenge_incl_system.iter().map(|x| *x as u8).collect(); + if num_challenge != vec![0] && num_challenge != vec![1, 0] { + log::debug!("num_challenge: {:?}", num_challenge); + bail!("Only phase0 BaseCircuitBuilder or phase0+1 RlcCircuitBuilder supported right now"); + } + metadata.num_challenge = num_challenge; + + metadata.is_aggregation = if protocol.accumulator_indices.is_empty() { + false + } else { + if protocol.accumulator_indices.len() != 1 + || protocol.accumulator_indices[0] != AggregationCircuit::accumulator_indices().unwrap() + { + bail!("invalid accumulator indices"); + } + true + }; + + Ok(metadata) +} + +/// Reference implementation. Actually done by axiom-sdk-client +pub fn write_onchain_vkey(vkey: &OnchainVerifyingKey) -> anyhow::Result> +where + C: CurveAffine + SerdeObject, + C::Scalar: Field + SerdeObject, +{ + let metadata = vkey.circuit_metadata.encode()?; + + let tmp = C::Repr::default(); + let compressed_curve_bytes = tmp.as_ref().len(); + let tmp = ::Repr::default(); + let field_bytes = tmp.as_ref().len(); + let mut writer = + Vec::with_capacity(field_bytes + vkey.preprocessed.len() * compressed_curve_bytes); + + writer.write_all(&metadata.to_fixed_bytes())?; + write_field_le(&mut writer, vkey.transcript_initial_state)?; + for &point in &vkey.preprocessed { + write_curve_compressed(&mut writer, point)?; + } + Ok(writer.chunks_exact(32).map(H256::from_slice).collect()) +} + +/// Requires additional context about the Axiom circuit, in the form of the `circuit_metadata`. +pub fn get_onchain_vk_from_vk( + vk: &VerifyingKey, + circuit_metadata: AxiomV2CircuitMetadata, +) -> OnchainVerifyingKey { + let preprocessed = vk + .fixed_commitments() + .iter() + .chain(vk.permutation().commitments().iter()) + .cloned() + .map(Into::into) + .collect(); + let transcript_initial_state = transcript_initial_state(vk); + OnchainVerifyingKey { circuit_metadata, preprocessed, transcript_initial_state } +} + +pub fn get_onchain_vk_from_protocol( + protocol: &PlonkProtocol, + circuit_metadata: AxiomV2CircuitMetadata, +) -> OnchainVerifyingKey { + let preprocessed = protocol.preprocessed.clone(); + let transcript_initial_state = protocol.transcript_initial_state.unwrap(); + OnchainVerifyingKey { circuit_metadata, preprocessed, transcript_initial_state } +} + +pub fn reconstruct_snark_from_compute_query( + subquery_results: LogicOutputResultsRoot, + compute_query: AxiomV2ComputeQuery, +) -> anyhow::Result<(Snark, AxiomV2CircuitMetadata)> { + let subquery_results = CircuitOutputResultsRoot::::try_from(subquery_results)?; + let vkey = compute_query.vkey.into_iter().flat_map(|u| u.0).collect_vec(); + let mut reader = &vkey[..]; + let onchain_vk = OnchainVerifyingKey::::read(&mut reader)?; + let client_metadata = onchain_vk.circuit_metadata.clone(); + let k = compute_query.k as usize; + let protocol = onchain_vk.into_plonk_protocol(k)?; + + // === Begin reconstruct proof transcript: === + if client_metadata.num_instance.len() != 1 { + bail!("Only one instance column supported right now"); + } + let num_instance = client_metadata.num_instance[0] as usize; + + // We assume that the true number of user requested subqueries is `num_subqueries` + let num_subqueries = subquery_results.num_subqueries; + let result_len = compute_query.result_len as usize; + let max_outputs = client_metadata.max_outputs as usize; + if result_len > max_outputs { + bail!("user_output_len exceeds user max outputs"); + } + // compute proof only has the user outputs, not the user subquery requests + let mut reader = &compute_query.compute_proof[..]; + let AxiomV2ComputeSnark { compute_results, proof_transcript, kzg_accumulator } = + decode_compute_snark( + &mut reader, + compute_query.result_len, + client_metadata.is_aggregation, + )?; + let mut instance = Vec::with_capacity(num_instance); + if let Some((lhs, rhs)) = kzg_accumulator { + instance.extend( + [lhs.x, lhs.y, rhs.x, rhs.y].into_iter().flat_map(fe_to_limbs::<_, Fr, LIMBS, BITS>), + ); + } + let mut compute_results = + compute_results.into_iter().flat_map(|out| HiLo::from(out).hi_lo()).collect_vec(); + // safety check that user outputs are hardcoded to HiLo for now + assert_eq!(compute_results.len(), result_len * USER_RESULT_FIELD_ELEMENTS); + compute_results + .resize((client_metadata.max_outputs as usize) * USER_RESULT_FIELD_ELEMENTS, Fr::ZERO); + instance.extend(compute_results); + + // fill in public instances corresponding to subqueries + for result in &subquery_results.results.rows[..num_subqueries] { + instance.extend(result.to_fixed_array()); + } + if instance.len() > num_instance { + bail!("Num subqueries exceeds num_instance limit"); + } + instance.resize(num_instance, Fr::ZERO); + let snark = Snark::new(protocol, vec![instance], proof_transcript); + Ok((snark, client_metadata)) +} + +/// This verifies snark with poseidon transcript and **importantly** also checks the +/// kzg accumulator from the public instances, if `snark` is aggregation circuit +pub fn verify_snark(dk: &KzgDecidingKey, snark: &Snark) -> anyhow::Result<()> { + let mut transcript = + PoseidonTranscript::::from_spec(snark.proof(), POSEIDON_SPEC.clone()); + let proof: PlonkProof<_, _, SHPLONK> = + PlonkVerifier::read_proof(dk, &snark.protocol, &snark.instances, &mut transcript) + .map_err(|_| anyhow!("Failed to read PlonkProof"))?; + PlonkVerifier::verify(dk, &snark.protocol, &snark.instances, &proof) + .map_err(|_| anyhow!("PlonkVerifier failed"))?; + Ok(()) +} + +lazy_static::lazy_static! { + pub static ref DEFAULT_CLIENT_METADATA: AxiomV2CircuitMetadata = AxiomV2CircuitMetadata { + version: 0, + num_advice_per_phase: vec![USER_ADVICE_COLS as u16], + num_lookup_advice_per_phase: vec![USER_LOOKUP_ADVICE_COLS as u8], + num_rlc_columns: 0, + num_fixed: USER_FIXED_COLS as u8, + num_instance: vec![ + (USER_MAX_OUTPUTS * USER_RESULT_FIELD_ELEMENTS + USER_MAX_SUBQUERIES * SUBQUERY_RESULT_LEN) + as u32, + ], + num_challenge: vec![0], + max_outputs: USER_MAX_OUTPUTS as u16, + is_aggregation: false, + }; +} + +/// Fully describes the configuration of a user provided circuit written using [`halo2_base`](axiom_eth::halo2_base) or [`snark_verifier_sdk`](axiom_eth::snark_verifier_sdk). +#[derive(Clone, Copy, Debug, Hash, Serialize, Deserialize)] +pub struct UserCircuitParams { + pub num_advice_cols: usize, + pub num_lookup_advice_cols: usize, + pub num_fixed_cols: usize, + /// Max number of bytes32 the user can output. + /// This will be `2 * USER_MAX_OUTPUTS` field elements as public instances. + pub max_outputs: usize, + /// Maximum number of subqueries a user can request. + pub max_subqueries: usize, +} + +impl UserCircuitParams { + /// Total public instances of the user circuit. + /// We start with + /// - user outputs (bytes32 in HiLo, 2 field elements each) and then + /// - add the "flattened" user subqueries with results + /// + /// Currently we assume user circuit has a single instance column. + pub fn num_instances(&self) -> usize { + self.max_outputs * USER_RESULT_FIELD_ELEMENTS + self.max_subqueries * SUBQUERY_RESULT_LEN + } + + pub fn base_circuit_params(&self, k: usize) -> BaseCircuitParams { + BaseCircuitParams { + k, + num_advice_per_phase: vec![self.num_advice_cols], + num_lookup_advice_per_phase: vec![self.num_lookup_advice_cols], + num_fixed: self.num_fixed_cols, + lookup_bits: Some(k - 1), + num_instance_columns: USER_INSTANCE_COLS, + } + } +} + +pub const DEFAULT_USER_PARAMS: UserCircuitParams = UserCircuitParams { + num_advice_cols: USER_ADVICE_COLS, + num_lookup_advice_cols: USER_LOOKUP_ADVICE_COLS, + num_fixed_cols: USER_FIXED_COLS, + max_outputs: USER_MAX_OUTPUTS, + max_subqueries: USER_MAX_SUBQUERIES, +}; + +/// Creates a default snark for a [axiom_eth::halo2_base] circuit with a fixed configuration, +/// using the given trusted setup. The log2 domain size `params.k()` can be variable. +/// Used to get fixed constraint and gate information. +pub fn dummy_compute_circuit( + user_params: UserCircuitParams, + k: u32, +) -> BaseCircuitBuilder { + let circuit_params = user_params.base_circuit_params(k as usize); + let mut builder = BaseCircuitBuilder::new(false).use_params(circuit_params); + + let ctx = builder.main(0); + let dummy_instances = ctx.assign_witnesses(vec![F::ZERO; user_params.num_instances()]); + assert_eq!(builder.assigned_instances.len(), USER_INSTANCE_COLS); + builder.assigned_instances[0] = dummy_instances; + + builder +} + +pub fn default_compute_circuit(k: u32) -> BaseCircuitBuilder { + dummy_compute_circuit(DEFAULT_USER_PARAMS, k) +} diff --git a/rust-toolchain b/rust-toolchain index 51ab4759..ee2d639b 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -nightly-2022-10-28 \ No newline at end of file +nightly-2023-08-12 \ No newline at end of file diff --git a/trusted_setup_s3.sh b/trusted_setup_s3.sh new file mode 100644 index 00000000..5d23075a --- /dev/null +++ b/trusted_setup_s3.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +for k in {5..23} # 25} +do + wget "https://axiom-crypto.s3.amazonaws.com/challenge_0085/kzg_bn254_${k}.srs" +done + +mv *.srs params/