# Hash-based Signature Aggregation [toc] ## 1. Hash-based Signature Parameter We use Poseidon2 instantiation with the following parameter: - $\textbf{Lifetime}\ L = 2^{20}$ - $\textbf{Encoding}\ = \mathsf{TSW}$ - $w = 2$ - $\delta = 1$ The implementation of this specific instantiation could be found [here](https://github.com/b-wagn/hash-sig/blob/5121db48534fcc4cd5cd18a7fd03987021c4c338/src/signature/generalized_xmss/instantiations_poseidon.rs#L678C18-L679). ## 2. Approach - zkVM ### 2.1. With `succinctlabs/sp1` (based on `plonky3`) - Source code: https://github.com/han0110/hash-sig-agg/tree/main/zkvm/sp1 ### 2.2. With `openvm-org/openvm` (based on `plonky3`) - Source code: https://github.com/han0110/hash-sig-agg/tree/main/zkvm/openvm ## 3. Approach - Circuit ### 3.1. With `plonky3` - Source code: https://github.com/han0110/hash-sig-agg/tree/feature/use-p3-uni-stark-ext/circuit #### 3.1.1. Introduction In order to handle non-uniform computations efficiently, we split the circuit into multiple AIR traces and use lookup to connect input/output between each other. AIR traces are: - `Main` - `Chain` - `MerkleTree` - `Decomposition` - `RangeCheck` ![hash-sig-agg-overview](https://hackmd.io/_uploads/ByG7B6-cJe.png) ##### 3.1.1.1. `Main` The `Main` is mainly used to synchronize other AIR traces, it does 3 things: 1. Send `(sig_idx, msg_hash)` to `Decomposition` to make sure the `x` are decomposed from `msg_hash`. 2. Send `(sig_idx, parameter)` to `Chain` to make sure it's using the same `parameter` as others. 3. Send `(sig_idx, parameter, msg_hash, merkle_root)` to `MerkleTree` to make sure the `msg_hash` is computed from the same `parameter` as others, and at the same time send to expected `merkle_root` to be checked for `sigs[sig_idx]`. And `parameter` and `merkle_root` parts are the statement of this aggregation circuit, verifier can calculate their polynomial evaluations in $O(n\log n)$ tiem by FFT. ![hash-sig-agg-main](https://hackmd.io/_uploads/SydOra-5Jl.png) ##### 3.1.1.2. `Chain` The `Chain` receives `x` (`msg_hash` chunks) from `Decomposition`, computes rest of the chain if the `one_time_sig_i` of `i`-th chain is not yet the end, then sends the `one_time_pk_i` to `MerkleTree`. We ignore the chain if it's already in the end, so the number of rows for a signature could be constant (equal to the target sum). A sample trace with `x = (0, 2, 1, 3, 3, 0, ..., 1, 3, 3)` might look like: ![hash-sig-agg-chain](https://hackmd.io/_uploads/S1mEB6Zqkl.png) ##### 3.1.1.3. `MerkleTree` The `MerkleTree` receives 2 things: 1. It receives `one_time_pk_i` where `x_i` is not yet end from `Chain`, and hashes them with `one_time_pk_i` where `x_i` is already end into `merkle_leaf_hash`, and then computes the `merkle_root` to be used latter. 2. It receives `(parameter, merkle_root, msg_hash)` as synchronization, where the `merkle_root` is computed from `merkle_leaf_hash` and `siblings`, `parameter` is used in all tweakable hashes, and `msg_hash` is computed separately in the next adjacent row. A sample trace with `epoch = 0b0...01` might look like: ![hash-sig-agg-merkle-tree](https://hackmd.io/_uploads/Sy54rpZcyx.png) ##### 3.1.1.4. `Decomposition` The `Decomposition` receives `msg_hash` as `values`, and calculates <br> `(((values[4] * P) + values[3]) * P + ...) * P + values[0]`, then decompose it into base <code>2<sup>w</sup></code> chunks and send non-ending chunk to `Chain`. A sample trace with `x = (0, 2, 1, 3, 3, 0, ..., 1, 3, 3)` might look like: ![hash-sig-agg-decomposition](https://hackmd.io/_uploads/BJ9EiPEqke.png) ##### 3.1.1.5. `RangeCheck` The `RangeCheck` is filled by range <code>0..2<sup>12</sup></code> to receives `limb` from `Decomposition` as range check. ##### 3.1.1.6. Other gadgets used in the chips See https://hackmd.io/@tcoratger/SyWbmVPckx for more details. #### 3.1.2. Benchmark - Machine spec - CPU: i9-13900K - RAM: 64GB (2x Crucial DDR5 5600 32GB Dual Channel) - OS: Linux 5.15.0-124-generic x86-64 - FRI Parameter: - Extension field bits: `124` (degree-4 extension of KoalaBear) - Number of queries: `256 / R` - Merkle tree hash: Poseidon2 The benchmark aggregates 8192 signatures, to reproduce please run the following script in `<repo>/circuit`: ```sh sh bench.sh python3 render_table.py ``` | `R` | `T` | `PT` | `TP` | `PS` | `PM` | `VT` | | - | - | - | - | - | - | - | | `1` | `4` | `5.33 s` | `1536` | `1.68 MB` | `5.76 GB` | `94.04 ms` | | `1` | `8` | `3.00 s` | `2729` | `1.68 MB` | `5.78 GB` | `99.68 ms` | | `1` | `16` | `2.65 s` | `3086` | `1.68 MB` | `5.17 GB` | `94.55 ms` | | `1` | `24` | `2.19 s` | `3745` | `1.68 MB` | `5.93 GB` | `96.09 ms` | | | | | | | | | | `2` | `4` | `8.59 s` | `953` | `930.96 kB` | `8.91 GB` | `49.09 ms` | | `2` | `8` | `4.98 s` | `1644` | `933.96 kB` | `8.81 GB` | `59.41 ms` | | `2` | `16` | `4.24 s` | `1932` | `933.71 kB` | `8.97 GB` | `72.45 ms` | | `2` | `24` | `3.36 s` | `2439` | `933.96 kB` | `12.48 GB` | `50.08 ms` | | | | | | | | | | `3` | `4` | `14.99 s` | `546` | `672.30 kB` | `16.07 GB` | `53.88 ms` | | `3` | `8` | `8.75 s` | `936` | `671.24 kB` | `16.27 GB` | `35.08 ms` | | `3` | `16` | `7.22 s` | `1134` | `670.96 kB` | `16.18 GB` | `40.86 ms` | | `3` | `24` | `6.17 s` | `1327` | `671.02 kB` | `16.21 GB` | `34.79 ms` | <small>`R`: Log 1/rate `T`: Threads `PT`: Proving time `TP`: Throughput (sig/s) `PS`: Proof size `PM`: Peak proving memory `VT`: Verifying time</small>